diff --git a/.gitea/workflows/deb.yml b/.gitea/workflows/deb.yml index 89067dcb..bcf7c1b8 100644 --- a/.gitea/workflows/deb.yml +++ b/.gitea/workflows/deb.yml @@ -119,10 +119,15 @@ jobs: run: | apt-get update # python3 is used by scripts/ci/gitea-release.sh for the stable-tag release attach. - # libvulkan-dev: /usr/include/vulkan/vulkan.h for the client's pf-ffvk bindgen - # (FFmpeg's hwcontext_vulkan.h includes it). + # No libvulkan-dev: nothing here compiles or links against Vulkan (ash dlopens + # libvulkan and pf-vkdecode binds nothing at build time), so neither the compile nor + # dpkg-shlibdeps — which resolves DT_NEEDED sonames only — ever asks for it. The + # client's `Depends: libvulkan1` is added by hand in packaging/debian/build-client-deb.sh + # precisely because a dlopen is invisible to shlibdeps. + # No libav*-dev: the client links no FFmpeg since M10 (§6 of + # design/client-native-decode.md). apt-get install -y --no-install-recommends dpkg-dev python3 \ - libgtk-4-dev libadwaita-1-dev libsdl3-dev libvulkan-dev + libgtk-4-dev libadwaita-1-dev libsdl3-dev # Share ci.yml's cache keys so the release build reuses its registry + target artifacts. - name: Cache keys diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml index 8cb826d6..003f4491 100644 --- a/.gitea/workflows/docker.yml +++ b/.gitea/workflows/docker.yml @@ -148,7 +148,12 @@ jobs: -t "$CI_REGISTRY_PUSH/${{ matrix.image }}:latest" \ ci + # Gated like Build/Push: only the docker CLI needs this login (Reconcile and Tag-for-release + # authenticate via curl -u), so a cache-hit job with nothing to push must not be able to fail + # on a login it never uses — proven on run 16013, where a host with a misconfigured daemon + # failed exactly here on a hit=true leg. - name: Log in to the LAN registry + if: steps.exists.outputs.hit == 'false' run: | echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY_PUSH" -u ci --password-stdin env: @@ -236,7 +241,9 @@ jobs: -t "$CI_REGISTRY_PUSH/$IMAGE:latest" \ . + # Same gate as the builders job above: the login only serves Push. - name: Log in to the LAN registry + if: steps.exists.outputs.hit == 'false' run: | echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY_PUSH" -u ci --password-stdin env: diff --git a/.gitea/workflows/flatpak.yml b/.gitea/workflows/flatpak.yml index 7497c877..d7c8a3f8 100644 --- a/.gitea/workflows/flatpak.yml +++ b/.gitea/workflows/flatpak.yml @@ -34,7 +34,10 @@ on: # The flatpak is the CLIENT — only rebuild when the client/core/manifest change, not on every # design/host push (this is a heavy flatpak-builder run). Tags (v*, the client release) build too. # The bundle ships BOTH client binaries (shell + Vulkan session), so every crate in either - # binary's dependency closure must be listed here. + # binary's dependency closure must be listed here — including the native decode rungs, or a + # commit that only touches the decoder never rebuilds the bundle and the Deck canary quietly + # stops tracking it. pf-dxvadec is absent on purpose: it is `cfg(windows)` in pf-client-core + # and never enters the Linux closure (windows.yml / windows-msix.yml carry it instead). paths: - 'clients/linux/**' - 'clients/session/**' @@ -42,6 +45,9 @@ on: - 'crates/pf-client-core/**' - 'crates/pf-presenter/**' - 'crates/pf-console-ui/**' + - 'crates/pf-bitstream/**' + - 'crates/pf-vkdecode/**' + - 'crates/pf-vaadec/**' - 'packaging/flatpak/**' - 'Cargo.lock' - '.gitea/workflows/flatpak.yml' @@ -128,7 +134,7 @@ jobs: # authselect trigger fires — so this line alone was never the fix for the failures # below. See the retry.sh bump for the real cause. sed -i 's/resolve \[!UNAVAIL=return\] //' /etc/nsswitch.conf - # Flathub provides the GNOME runtime/SDK + the rust-stable + ffmpeg-full extensions. + # Flathub provides the GNOME runtime/SDK + the rust-stable and llvm20 extensions. # # ROOT CAUSE (confirmed 2026-07-11 by watching a live run on home-runner-1): this is # NOT a deterministic nsswitch/DNS-config bug. gitea-runner-fleet on home-runner-1 is @@ -147,7 +153,7 @@ jobs: git config --global --add safe.directory "$PWD" # This job was the fleet's single heaviest network consumer: every run re-downloaded - # the GNOME runtime + SDK + llvm/rust/ffmpeg extensions (multi-GB from Flathub) and + # the GNOME runtime + SDK + llvm/rust extensions (multi-GB from Flathub) and # every crate source. Both live in well-defined directories, both are idempotently # verified/extended by the steps below, and the central cache server restores them # at LAN speed — so cache them. Keyed on what actually pins them: the manifest tree @@ -251,7 +257,8 @@ jobs: # or TCP dial costs a backoff-retry instead of the whole (long) compile: # 1) --install-deps-only pulls everything the manifest declares from Flathub: the # GNOME 50 runtime/SDK + the rust-stable (//25.08, rustc 1.96) and llvm20 SDK - # extensions, plus the runtime's auto codecs-extra (HEVC libavcodec). + # extensions. (No codec extension: the client links no FFmpeg — see the + # manifest header.) # 2) --download-only fetches every source (all crates in cargo-sources.json) into # the .flatpak-builder state dir. Both are resumable/idempotent, so re-running # after a partial failure is safe and cheap. diff --git a/.gitea/workflows/rpm.yml b/.gitea/workflows/rpm.yml index 94c3dec7..1769e661 100644 --- a/.gitea/workflows/rpm.yml +++ b/.gitea/workflows/rpm.yml @@ -96,9 +96,12 @@ jobs: - name: Prep run: | git config --global --add safe.directory "$PWD" - # vulkan-headers: the client's pf-ffvk crate runs bindgen over FFmpeg's - # libavutil/hwcontext_vulkan.h (#include ). - dnf -y install gtk4-devel libadwaita-devel SDL3-devel vulkan-headers + # No vulkan-headers: nothing in the workspace compiles against the system Vulkan headers. + # The host's Vulkan encode hand-rolls its structs, pyrowave-sys bindgens its own vendored + # copy, and both host and client reach Vulkan through ash, which dlopens the loader. (The + # HDR gamescope leg further down does need them, and pulls them itself via `dnf builddep + # gamescope`.) Matches packaging/rpm/punktfunk.spec, which dropped its BuildRequires too. + dnf -y install gtk4-devel libadwaita-devel SDL3-devel # sysext build (packaging/bazzite/build-sysext.sh): squashfs + SELinux labeling. dnf -y install squashfs-tools cpio libselinux-utils selinux-policy-targeted # Fedora's own gamescope, for its RUNTIME libraries only — never shipped, never run. The diff --git a/.gitea/workflows/windows-host.yml b/.gitea/workflows/windows-host.yml index 65050e73..e380bf11 100644 --- a/.gitea/workflows/windows-host.yml +++ b/.gitea/workflows/windows-host.yml @@ -141,8 +141,10 @@ jobs: # observed on a clean build on this very runner (2026-07-17). No-op for compliant # projects (libvpl-sys pins 3.13+). "CMAKE_POLICY_VERSION_MINIMUM=3.5" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - # FFMPEG_DIR: the same BtbN lgpl-shared x64 tree the Windows CLIENT links against (provisioned - # by scripts/ci/provision-windows-punktfunk-extras.ps1). The host's AMD/Intel AMF/QSV encode backend + # FFMPEG_DIR: the BtbN lgpl-shared x64 tree, provisioned by + # scripts/ci/provision-windows-punktfunk-extras.ps1. The CLIENT used to link it too; since M10 + # it links no libav* at all (windows.yml sets no FFMPEG_DIR), so this tree is the HOST's alone + # and the provisioning step keeps fetching it for that reason. The host's AMD/Intel AMF/QSV encode backend # (--features amf-qsv) link-imports avcodec/avutil/swscale from it; pack-host-installer.ps1 # then bundles its bin\*.dll into the installer. LIBCLANG_PATH is in the runner daemon env. if (-not $env:FFMPEG_DIR) { diff --git a/.gitea/workflows/windows-msix.yml b/.gitea/workflows/windows-msix.yml index ec9ce8cc..59b1d9d2 100644 --- a/.gitea/workflows/windows-msix.yml +++ b/.gitea/workflows/windows-msix.yml @@ -1,13 +1,17 @@ # Build the punktfunk Windows client as signed MSIX packages (x64 + ARM64) and publish them to # Gitea's generic package registry, so Windows boxes can download + install a real package (Start # tile, clean install/uninstall) instead of a loose exe. Runs on a self-hosted windows-amd64 -# runner (host mode; the MSVC/WinUI toolchain comes from unom/infra's windows-runner/, FFmpeg +# runner (host mode; the MSVC/WinUI toolchain comes from unom/infra's windows-runner/, the rest # self-provisions via the "Ensure Windows toolchain" step below, same as windows.yml) — the # Windows SDK's makeappx/signtool are baked into the runner's daemon env. # # Both arches come off the ONE x64 runner: x86_64 natively, aarch64 cross-compiled (the x64 MSVC -# toolset has the ARM64 cross compiler; the matrix points FFMPEG_DIR at the ARM64 FFmpeg tree). See -# windows.yml for the cross-build rationale + the BOM/MAX_PATH runner gotchas. +# toolset has the ARM64 cross compiler). See windows.yml for the cross-build rationale + the +# BOM/MAX_PATH runner gotchas. +# +# NO FFmpeg since M10 (design/client-native-decode.md §6): the client decodes natively, so the +# package carries no libav* DLLs and this workflow sets no FFMPEG_DIR. The host installer +# (windows-host.yml) is unchanged. # # Registry (public, unom org): https://git.unom.io/unom/-/packages (generic group) # Packaging internals: clients/windows/packaging/README.md. @@ -49,7 +53,9 @@ on: - 'crates/pf-client-core/**' - 'crates/pf-presenter/**' - 'crates/pf-console-ui/**' - - 'crates/pf-ffvk/**' + - 'crates/pf-bitstream/**' + - 'crates/pf-vkdecode/**' + - 'crates/pf-dxvadec/**' - 'Cargo.lock' - 'Cargo.toml' - '.gitea/workflows/windows-msix.yml' @@ -80,12 +86,10 @@ jobs: include: - arch: x64 target: x86_64-pc-windows-msvc - ffmpeg: C:\Users\Public\ffmpeg td: C:\t session_flags: '' - arch: arm64 target: aarch64-pc-windows-msvc - ffmpeg: C:\Users\Public\ffmpeg-arm64 td: C:\t-a64 # No skia-binaries prebuilt for aarch64-pc-windows-msvc: the session ships # without the Skia console UI on ARM64 (streaming unaffected) — flip when @@ -94,7 +98,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Ensure Windows toolchain (WDK, FFmpeg, Inno Setup, ARM64 target) + - name: Ensure Windows toolchain (WDK, Inno Setup, ARM64 target) shell: pwsh run: ./scripts/ci/ensure-windows-toolchain.ps1 @@ -102,12 +106,9 @@ jobs: shell: pwsh run: | # CARGO_TARGET_DIR (per-arch, short) dodges the MAX_PATH wall in the CMake-from-source - # crates (see windows.yml). FFMPEG_DIR selects the arch's import libs + is read by - # pack-msix.ps1 for the runtime DLLs. All via GITHUB_ENV. + # crates (see windows.yml). No FFMPEG_DIR: nothing in this package links libav* (M10), + # and pack-msix.ps1 no longer copies runtime DLLs from one. "CARGO_TARGET_DIR=${{ matrix.td }}" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - "FFMPEG_DIR=${{ matrix.ffmpeg }}" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - # pf-ffvk's bindgen needs Vulkan headers (arch-independent; provisioned alongside FFmpeg). - "PF_FFVK_VULKAN_INCLUDE=C:\Users\Public\vulkan-headers\include" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 rustup target add ${{ matrix.target }} $pf = & "$env:GITHUB_WORKSPACE/scripts/ci/pf-version.ps1" # single source of truth: base is one minor ahead of the latest stable tag $parts = if ($env:GITHUB_REF -like 'refs/tags/v*') { diff --git a/.gitea/workflows/windows.yml b/.gitea/workflows/windows.yml index c3e6c877..d6d4d7e3 100644 --- a/.gitea/workflows/windows.yml +++ b/.gitea/workflows/windows.yml @@ -1,26 +1,29 @@ # Windows client CI — runs on a self-hosted windows-amd64 runner (host mode; the generic runner + -# toolchain come from unom/infra's windows-runner/; punktfunk's own extras - FFmpeg, -# Vulkan-Headers, WDK, Inno Setup, the ARM64 rustup target - self-provision via the "Ensure -# Windows toolchain" step below, a fast no-op once already present, so any runner with that label -# works with no manual dispatch step first). Build + clippy + fmt + test BOTH client binaries: -# the WinUI 3 shell (windows-reactor + WASAPI + SDL3) and the punktfunk-session Vulkan client -# (pf-presenter/pf-client-core/pf-console-ui/pf-ffvk — every stream runs in it, spawned by the +# toolchain come from unom/infra's windows-runner/; punktfunk's own extras - WDK, Inno Setup, +# the ARM64 rustup target - self-provision via the "Ensure Windows toolchain" step below, a fast +# no-op once already present, so any runner with that label works with no manual dispatch step +# first). Build + clippy + fmt + test BOTH client binaries: the WinUI 3 shell +# (windows-reactor + WASAPI + SDL3) and the punktfunk-session Vulkan client +# (pf-presenter/pf-client-core/pf-console-ui — every stream runs in it, spawned by the # shell). ARM64 note: rust-skia publishes no aarch64-pc-windows-msvc prebuilt binaries, so the # session builds --no-default-features there (no Skia console UI; streaming is unaffected) — # flip when skia-binaries adds the target. # +# NO FFmpeg here since M10 (design/client-native-decode.md §6): the client decodes with +# pf-vkdecode / pf-dxvadec / openh264+rav1d and links no libav* at all, so this workflow sets +# no FFMPEG_DIR, no PF_FFVK_VULKAN_INCLUDE and prepends nothing to PATH. The provisioning +# script still fetches the FFmpeg trees because the HOST keeps FFmpeg — windows-host.yml's +# `amf-qsv` leg link-imports them. +# # Two architectures from ONE x64 runner: x86_64-pc-windows-msvc natively and # aarch64-pc-windows-msvc by cross-compiling. The x64 MSVC toolset ships an ARM64 cross compiler # (VC\Tools\MSVC\\bin\Hostx64\arm64\cl.exe) and aarch64-pc-windows-msvc is a tier-2 Rust # target with host tools, so no ARM64 runner is needed — the cc/cmake crates pick the ARM64 # compiler from the target triple (SDL3 + libopus build-from-source cross-compile fine). The one -# arch-specific external dep is FFmpeg's import libs: the runner keeps an x64 tree at -# C:\Users\Public\ffmpeg and an ARM64 tree at C:\Users\Public\ffmpeg-arm64 (both FFmpeg 7.x / -# avcodec-61); the matrix points FFMPEG_DIR at the right one. aarch64 can't *run* on the x64 host, -# so fmt + test run only for x64. +# thing the aarch64 build can't do is *run* on the x64 host, so fmt + test run only for x64. # -# The MSVC/WinUI/FFmpeg toolchain (cargo/rustup on ASCII paths, NASM, CMake, LLVM, the x64 FFmpeg, -# CARGO_HOME, CMAKE_POLICY_VERSION_MINIMUM, …) is baked into the runner's daemon env. Per-checkout +# The MSVC/WinUI toolchain (cargo/rustup on ASCII paths, NASM, CMake, LLVM, CARGO_HOME, +# CMAKE_POLICY_VERSION_MINIMUM, …) is baked into the runner's daemon env. Per-checkout # / per-arch vars are set in a step: # - CARGO_TARGET_DIR=C:\t… the runner's host workdir is buried deep under # C:\Windows\System32\config\systemprofile\.cache\act\\hostexecutor\, @@ -29,7 +32,6 @@ # can't create its .tlog (DirectoryNotFoundException -> MSB6003). A short # root keeps every nested path well under the limit (per-arch so the two # matrix legs don't share a target dir). -# - FFMPEG_DIR per-arch FFmpeg import libs (x64 vs arm64 tree). # # Steps use `shell: pwsh` (PowerShell 7) deliberately: Windows PowerShell 5.1's # `Out-File -Encoding utf8` prepends a UTF-8 BOM that corrupts the first GITHUB_ENV line (that @@ -55,7 +57,9 @@ on: - 'crates/pf-client-core/**' - 'crates/pf-presenter/**' - 'crates/pf-console-ui/**' - - 'crates/pf-ffvk/**' + - 'crates/pf-bitstream/**' + - 'crates/pf-vkdecode/**' + - 'crates/pf-dxvadec/**' - 'Cargo.lock' - 'Cargo.toml' - '.gitea/workflows/windows.yml' @@ -67,7 +71,9 @@ on: - 'crates/pf-client-core/**' - 'crates/pf-presenter/**' - 'crates/pf-console-ui/**' - - 'crates/pf-ffvk/**' + - 'crates/pf-bitstream/**' + - 'crates/pf-vkdecode/**' + - 'crates/pf-dxvadec/**' - 'Cargo.lock' - 'Cargo.toml' - '.gitea/workflows/windows.yml' @@ -110,7 +116,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Ensure Windows toolchain (WDK, FFmpeg, Inno Setup, ARM64 target) + - name: Ensure Windows toolchain (WDK, Inno Setup, ARM64 target) shell: pwsh run: ./scripts/ci/ensure-windows-toolchain.ps1 @@ -120,21 +126,13 @@ jobs: # Per-arch short target root (dodges MAX_PATH; keeps the two legs from sharing target\). $td = if ('${{ matrix.target }}' -eq 'aarch64-pc-windows-msvc') { 'C:\t-a64' } else { 'C:\t' } "CARGO_TARGET_DIR=$td" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - # Per-arch FFmpeg import libs (provision-windows-punktfunk-extras.ps1 fetches both). - $ff = if ('${{ matrix.target }}' -eq 'aarch64-pc-windows-msvc') { 'C:\Users\Public\ffmpeg-arm64' } else { 'C:\Users\Public\ffmpeg' } - "FFMPEG_DIR=$ff" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - # pf-ffvk's bindgen needs Vulkan headers (arch-independent; provisioned alongside FFmpeg). - "PF_FFVK_VULKAN_INCLUDE=C:\Users\Public\vulkan-headers\include" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - # $ff\bin on PATH too (not just FFMPEG_DIR, which only satisfies the linker): the test - # binary needs the actual DLLs to load at runtime. Set here rather than relying on the - # daemon's own env (project-env.ps1) - on a freshly cloned/registered runner the daemon - # starts before this job's "Ensure Windows toolchain" step ever writes that file, so its - # PATH doesn't include this yet on a first run (confirmed live: STATUS_DLL_NOT_FOUND). - "$ff\bin" | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8 + # No FFMPEG_DIR / PF_FFVK_VULKAN_INCLUDE / PATH prepend: the client links no libav* + # since M10 (see this file's header), so nothing here needs import libs or runtime DLLs. + # The HOST still does — windows-host.yml sets them for its amf-qsv leg. rustup target add ${{ matrix.target }} rustc --version cargo --version - Write-Output "target ${{ matrix.target }} target-dir $td ffmpeg $ff" + Write-Output "target ${{ matrix.target }} target-dir $td" # Both client binaries. ARM64: no skia-binaries prebuilt for the target, so the session # drops its `ui` feature there (pf-console-ui excluded; --no-default-features is a no-op @@ -152,7 +150,10 @@ jobs: - name: Clippy (-D warnings) shell: pwsh run: | - $pkgs = @('-p','punktfunk-client-windows','-p','punktfunk-client-session','-p','punktfunk-cli','-p','pf-client-core','-p','pf-presenter','-p','pf-ffvk') + # Every crate in the `paths:` trigger above is named here: `cargo clippy -p X` BUILDS a + # dependency but only LINTS the packages it is given, so a decode crate that starts the + # run but is missing from this list would be gated by nothing. + $pkgs = @('-p','punktfunk-client-windows','-p','punktfunk-client-session','-p','punktfunk-cli','-p','pf-client-core','-p','pf-presenter','-p','pf-bitstream','-p','pf-vkdecode','-p','pf-dxvadec') $sf = @() if ('${{ matrix.target }}' -eq 'aarch64-pc-windows-msvc') { $sf = @('--no-default-features') } else { $pkgs += @('-p','pf-console-ui') } cargo clippy @pkgs --all-targets @sf --target ${{ matrix.target }} -- -D warnings @@ -160,9 +161,9 @@ jobs: - name: Rustfmt check if: matrix.target == 'x86_64-pc-windows-msvc' shell: pwsh - run: cargo fmt -p punktfunk-client-windows -p punktfunk-client-session -p punktfunk-cli -p pf-client-core -p pf-presenter -p pf-console-ui -p pf-ffvk -- --check + run: cargo fmt -p punktfunk-client-windows -p punktfunk-client-session -p punktfunk-cli -p pf-client-core -p pf-presenter -p pf-console-ui -p pf-dxvadec -- --check - name: Test if: matrix.target == 'x86_64-pc-windows-msvc' shell: pwsh - run: cargo test -p punktfunk-client-windows -p punktfunk-client-session -p punktfunk-cli -p pf-client-core -p pf-presenter -p pf-console-ui -p pf-ffvk --target ${{ matrix.target }} + run: cargo test -p punktfunk-client-windows -p punktfunk-client-session -p punktfunk-cli -p pf-client-core -p pf-presenter -p pf-console-ui -p pf-dxvadec --target ${{ matrix.target }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf664795..a0c3d914 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,9 +46,11 @@ sudo apt install build-essential clang libclang-dev pkg-config cmake \ libvulkan-dev ``` -(The last two groups are the Linux client shell and `pf-ffvk`; skip them only if you never build -those crates. `scripts/bootstrap-ubuntu.sh` sets up an Ubuntu **capture-test host** — NVIDIA, Sway, -PipeWire — and is not a substitute for the list above.) +(The last two groups are the Linux client shell and the Vulkan session presenter; skip them only +if you never build those crates. `libvulkan-dev` is for the LOADER's pkg-config/soname — ash +dlopens it, and the client links no FFmpeg at all, so no libav*-dev appears here. +`scripts/bootstrap-ubuntu.sh` sets up an Ubuntu **capture-test host** — NVIDIA, Sway, PipeWire — +and is not a substitute for the list above.) ## Before you push diff --git a/Cargo.lock b/Cargo.lock index 487469a7..8401cb07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" dependencies = [ "android_log-sys", - "env_filter", + "env_filter 0.1.4", "log", ] @@ -204,6 +204,12 @@ dependencies = [ "syn", ] +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "async-broadcast" version = "0.7.2" @@ -341,6 +347,26 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atomig" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0f41f4bb89f5c6450325e283fb78c4a3d042181b54f3855ee2f872919f9863" +dependencies = [ + "atomig-macro", +] + +[[package]] +name = "atomig-macro" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49c98dba06b920588de7d63f6acc23f1e6a9fade5fd6198e564506334fb5a4f5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "audiopus_sys" version = "0.2.2" @@ -446,7 +472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "annotate-snippets", - "bitflags", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -475,6 +501,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.0" @@ -538,6 +570,12 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -556,7 +594,7 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95" dependencies = [ - "bitflags", + "bitflags 2.13.0", "cairo-sys-rs", "glib", "libc", @@ -885,6 +923,15 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "cros-codecs" +version = "0.0.5" +dependencies = [ + "env_logger", + "log", + "serde_json", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -991,6 +1038,37 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "der" version = "0.7.10" @@ -1101,6 +1179,29 @@ dependencies = [ "regex", ] +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter 2.0.0", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1193,7 +1294,7 @@ version = "8.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c4bd5ab1ac61f29c634df1175d350ded29cf74c3c6d4f7030431a5ae3c7d5d" dependencies = [ - "bitflags", + "bitflags 2.13.0", "ffmpeg-sys-next", "libc", ] @@ -1584,7 +1685,7 @@ version = "0.22.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a" dependencies = [ - "bitflags", + "bitflags 2.13.0", "futures-channel", "futures-core", "futures-executor", @@ -1777,7 +1878,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", - "zerocopy", + "zerocopy 0.8.52", ] [[package]] @@ -2127,6 +2228,42 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "jni" version = "0.21.1" @@ -2291,7 +2428,7 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6b8cfa2a7656627b4c92c6b9ef929433acd673d5ab3708cda1b18478ac00df4" dependencies = [ - "bitflags", + "bitflags 2.13.0", "cc", "convert_case", "cookie-factory", @@ -2486,6 +2623,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "706bf8a5e8c8ddb99128c3291d31bd21f4bcde17f0f4c20ec678d85c74faa149" dependencies = [ + "jobserver", "log", ] @@ -2493,7 +2631,7 @@ dependencies = [ name = "ndk" version = "0.9.0" dependencies = [ - "bitflags", + "bitflags 2.13.0", "jni-sys 0.3.1", "log", "ndk-sys", @@ -2517,7 +2655,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "libc", @@ -2530,7 +2668,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "libc", @@ -2848,6 +2986,14 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pf-bitstream" +version = "0.24.0" +dependencies = [ + "cros-codecs", + "tracing", +] + [[package]] name = "pf-capture" version = "0.24.0" @@ -2876,19 +3022,26 @@ dependencies = [ "anyhow", "ash", "async-channel", - "ffmpeg-next", + "libc", + "libloading", "mdns-sd", + "openh264", "opus", - "pf-ffvk", + "pf-bitstream", + "pf-dxvadec", "pf-update-check", + "pf-vaadec", + "pf-vkdecode", "pipewire", "punktfunk-core", "pyrowave-sys", "rand 0.9.4", + "rav1d", "rustls", "sdl3", "serde", "serde_json", + "sha2", "tracing", "ureq", "wasapi", @@ -2935,6 +3088,16 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "pf-dxvadec" +version = "0.24.0" +dependencies = [ + "cros-codecs", + "pf-bitstream", + "pf-vkdecode", + "tracing", +] + [[package]] name = "pf-encode" version = "0.24.0" @@ -2959,15 +3122,6 @@ dependencies = [ "windows 0.62.2 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "pf-ffvk" -version = "0.24.0" -dependencies = [ - "ash", - "bindgen", - "pkg-config", -] - [[package]] name = "pf-frame" version = "0.24.0" @@ -3042,7 +3196,7 @@ dependencies = [ "ash", "async-channel", "pf-client-core", - "pf-ffvk", + "pf-vkdecode", "punktfunk-core", "sdl3", "tracing", @@ -3069,13 +3223,22 @@ dependencies = [ "ureq", ] +[[package]] +name = "pf-vaadec" +version = "0.24.0" +dependencies = [ + "cros-codecs", + "pf-bitstream", + "pf-vkdecode", +] + [[package]] name = "pf-vdisplay" version = "0.24.0" dependencies = [ "anyhow", "ashpd", - "bitflags", + "bitflags 2.13.0", "bytemuck", "futures-util", "hex", @@ -3102,6 +3265,17 @@ dependencies = [ "x11rb", ] +[[package]] +name = "pf-vkdecode" +version = "0.24.0" +dependencies = [ + "ash", + "cros-codecs", + "pf-bitstream", + "sha2", + "tracing", +] + [[package]] name = "pf-win-display" version = "0.24.0" @@ -3153,7 +3327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9688b89abf11d756499f7c6190711d6dbe5a3acdb30c8fbf001d6596d06a8d44" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.13.0", "libc", "libspa", "libspa-sys", @@ -3207,7 +3381,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags", + "bitflags 2.13.0", "crc32fast", "fdeflate", "flate2", @@ -3251,6 +3425,21 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -3272,7 +3461,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy", + "zerocopy 0.8.52", ] [[package]] @@ -3311,7 +3500,7 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bit-set", "bit-vec", - "bitflags", + "bitflags 2.13.0", "num-traits", "rand 0.9.4", "rand_chacha 0.9.0", @@ -3388,7 +3577,6 @@ name = "punktfunk-client-windows" version = "0.24.0" dependencies = [ "async-channel", - "ffmpeg-next", "mdns-sd", "pf-client-core", "punktfunk-core", @@ -3431,7 +3619,7 @@ dependencies = [ "tokio", "tracing", "windows-sys 0.59.0", - "zerocopy", + "zerocopy 0.8.52", "zeroize", ] @@ -3726,6 +3914,36 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rav1d" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1932f060d5e7bd49dc9f8b272c1dc5e9ce0ffe141c28be900265d3989b36c9ed" +dependencies = [ + "assert_matches", + "atomig", + "bitflags 2.13.0", + "cc", + "cfg-if", + "libc", + "nasm-rs", + "parking_lot", + "paste", + "raw-cpuid", + "strum", + "to_method", + "zerocopy 0.7.35", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.0", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3777,7 +3995,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.0", ] [[package]] @@ -3934,7 +4152,7 @@ version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323" dependencies = [ - "bitflags", + "bitflags 2.13.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -3973,7 +4191,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys", @@ -4127,7 +4345,7 @@ version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25bd22eb1bbc9137e914022b4994ed35591eea0884e9e3e98e6d9895cad6e1d2" dependencies = [ - "bitflags", + "bitflags 2.13.0", "libc", "sdl3-image-sys", "sdl3-mixer-sys", @@ -4222,7 +4440,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.13.0", "core-foundation", "core-foundation-sys", "libc", @@ -4428,7 +4646,7 @@ version = "0.87.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f7d94f3e7537c71ad4cf132eb26e3be8c8a886ed3649c4525c089041fc312b2" dependencies = [ - "bitflags", + "bitflags 2.13.0", "lazy_static", "skia-bindings", ] @@ -4521,6 +4739,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -4724,6 +4964,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "to_method" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" + [[package]] name = "tokio" version = "1.52.3" @@ -5301,7 +5547,7 @@ version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags", + "bitflags 2.13.0", "rustix", "wayland-backend", "wayland-scanner", @@ -5313,7 +5559,7 @@ version = "0.32.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" dependencies = [ - "bitflags", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -5325,7 +5571,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" dependencies = [ - "bitflags", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5338,7 +5584,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5690,7 +5936,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" dependencies = [ - "bitflags", + "bitflags 2.13.0", "widestring", "windows-sys 0.52.0", ] @@ -6182,13 +6428,34 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + [[package]] name = "zerocopy" version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.8.52", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8ee7bd85..4a6fde25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,12 @@ members = [ "crates/punktfunk-host", "crates/punktfunk-host/vendor/usbip-sim", "crates/punktfunk-tray", + "crates/pf-bitstream", + "crates/pf-bitstream/vendor/cros-codecs", "crates/pf-client-core", "crates/pf-clipboard", "crates/pf-presenter", "crates/pf-console-ui", - "crates/pf-ffvk", "crates/pf-driver-proto", "crates/pf-paths", "crates/pf-update", @@ -23,6 +24,9 @@ members = [ "crates/pf-capture", "crates/pf-inject", "crates/pf-vdisplay", + "crates/pf-vkdecode", + "crates/pf-dxvadec", + "crates/pf-vaadec", "crates/pyrowave-sys", "crates/libvpl-sys", "clients/probe", diff --git a/README.md b/README.md index 6b9395a5..8ce46f51 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,9 @@ mid-stream mode renegotiation and a wall-clock skew handshake so latency stays v Both run from **one process**: bare `punktfunk-host serve` is the **secure native-only default** (`punktfunk/1` + the management API/web console), and `serve --gamestream` additionally enables the GameStream/Moonlight-compat planes (opt-in, trusted-LAN only — GameStream has inherent on-path -weaknesses). The host is managed through a REST API and web console. Builds against FFmpeg 7 or 8. +weaknesses). The host is managed through a REST API and web console. The **host** builds against +FFmpeg 7 or 8; the **clients** link no FFmpeg at all — they decode natively (Vulkan Video, DXVA, +VAAPI, VideoToolbox, MediaCodec, openh264 + rav1d). What works where: **[the support matrix](https://docs.punktfunk.unom.io/docs/support-matrix)** · where it's heading: **[the roadmap](https://docs.punktfunk.unom.io/docs/roadmap)**. @@ -187,10 +189,13 @@ and the [docs site](https://docs.punktfunk.unom.io). crates/ punktfunk-core/ protocol · FEC · pacing · crypto · QUIC control plane — the C ABI (lib + cdylib + staticlib) punktfunk-host/ the host (Linux + Windows): virtual displays · capture · encode · input · GameStream · punktfunk/1 · mgmt - pf-client-core/ shared client plumbing (Linux + Windows): session pump · FFmpeg decode · audio · SDL3 gamepads · trust · discovery + pf-client-core/ shared client plumbing (Linux + Windows): session pump · native decode ladder · audio · SDL3 gamepads · trust · discovery pf-presenter/ Vulkan session presenter: SDL3 window · ash swapchain · frame present · input capture pf-console-ui/ Skia console UI for the session client: gamepad shell · stats OSD · pairing · on-screen keyboard - pf-ffvk/ FFmpeg Vulkan hwcontext bindings (AVVkFrame) for Vulkan Video decode on the presenter's device + pf-bitstream/ H.264 / H.265 / AV1 bitstream parsing + per-AU decode plans — the one parser every native rung submits from + pf-vkdecode/ native Vulkan Video decode (H.264 / H.265 / AV1) on the presenter's own device + pf-dxvadec/ native DXVA buffer layouts + AuPlan → picparams conversion (the Windows D3D11VA rung) + pf-vaadec/ native libva buffer layouts + AuPlan → picparams conversion (the Linux VAAPI rung) pf-driver-proto/ host ↔ pf-vdisplay driver contract: control IOCTLs + IDD-push frame transport (no_std) punktfunk-tray/ host tray icon (Windows notification area / Linux StatusNotifierItem) clients/ @@ -245,9 +250,10 @@ additional terms or conditions. See [CONTRIBUTING.md](CONTRIBUTING.md). Punktfunk's own source is MIT/Apache-2.0. Shipped binaries additionally link third-party components under their own (permissive) licenses — see [`THIRD-PARTY-NOTICES.txt`](THIRD-PARTY-NOTICES.txt) -(regenerate with `scripts/gen-third-party-notices.sh`). The Windows host and client builds also -bundle FFmpeg under the **LGPL v2.1+** (dynamically linked, replaceable DLLs; the license text and -notice ship in the installed `licenses/` folder). +(regenerate with `scripts/gen-third-party-notices.sh`). The Windows **host** build also +bundles FFmpeg under the **LGPL v2.1+** (dynamically linked, replaceable DLLs; the license text and +notice ship in the installed `licenses/` folder). The **clients** bundle no FFmpeg — they link +none. ### Trademarks diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index fecd7fe9..94ce4664 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -7,7 +7,7 @@ below. Each is distributed under its own permissive license; the full license te follow the manifest. This file is generated by scripts/gen-third-party-notices.py (or `cargo about`, see about.toml) — do not edit by hand. -Total third-party crates: 566 +Total third-party crates: 596 ---------------------------------------------------------------------------- VENDORED THIRD-PARTY SOURCE (inside first-party crates) @@ -43,6 +43,7 @@ MANIFEST (crate version — SPDX license — source) asn1-rs 0.6.2 — MIT OR Apache-2.0 — https://github.com/rusticata/asn1-rs.git asn1-rs-derive 0.5.1 — MIT OR Apache-2.0 — https://github.com/rusticata/asn1-rs.git asn1-rs-impl 0.2.0 — MIT/Apache-2.0 — https://github.com/rusticata/asn1-rs.git + assert_matches 1.5.0 — MIT/Apache-2.0 — https://github.com/murarth/assert_matches async-broadcast 0.7.2 — MIT OR Apache-2.0 — https://github.com/smol-rs/async-broadcast async-channel 2.5.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-channel async-executor 1.14.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-executor @@ -54,6 +55,8 @@ MANIFEST (crate version — SPDX license — source) async-task 4.7.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-task async-trait 0.1.89 — MIT OR Apache-2.0 — https://github.com/dtolnay/async-trait atomic-waker 1.1.2 — Apache-2.0 OR MIT — https://github.com/smol-rs/atomic-waker + atomig 0.4.3 — MIT/Apache-2.0 — https://github.com/LukasKalbertodt/atomig/ + atomig-macro 0.4.0 — MIT/Apache-2.0 — https://github.com/LukasKalbertodt/atomig/ audiopus_sys 0.2.2 — ISC — https://github.com/lakelezz/audiopus_sys.git autocfg 1.5.1 — Apache-2.0 OR MIT — https://github.com/cuviper/autocfg axum 0.8.9 — MIT — https://github.com/tokio-rs/axum @@ -64,6 +67,7 @@ MANIFEST (crate version — SPDX license — source) bindgen 0.72.1 — BSD-3-Clause — https://github.com/rust-lang/rust-bindgen bit-set 0.8.0 — Apache-2.0 OR MIT — https://github.com/contain-rs/bit-set bit-vec 0.8.0 — Apache-2.0 OR MIT — https://github.com/contain-rs/bit-vec + bitflags 1.3.2 — MIT/Apache-2.0 — https://github.com/bitflags/bitflags bitflags 2.13.0 — MIT OR Apache-2.0 — https://github.com/bitflags/bitflags block-buffer 0.10.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils block-padding 0.3.3 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils @@ -71,6 +75,7 @@ MANIFEST (crate version — SPDX license — source) bumpalo 3.20.3 — MIT OR Apache-2.0 — https://github.com/fitzgen/bumpalo bytemuck 1.25.0 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/bytemuck bytemuck_derive 1.10.2 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/bytemuck + byteorder 1.5.0 — Unlicense OR MIT — https://github.com/BurntSushi/byteorder byteorder-lite 0.1.0 — Unlicense OR MIT — https://github.com/image-rs/byteorder-lite bytes 1.12.0 — MIT — https://github.com/tokio-rs/bytes cairo-rs 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core @@ -108,13 +113,19 @@ MANIFEST (crate version — SPDX license — source) crc32fast 1.5.0 — MIT OR Apache-2.0 — https://github.com/srijs/rust-crc32fast criterion 0.5.1 — Apache-2.0 OR MIT — https://github.com/bheisler/criterion.rs criterion-plot 0.5.0 — MIT/Apache-2.0 — https://github.com/bheisler/criterion.rs + crossbeam-deque 0.8.6 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam + crossbeam-epoch 0.9.20 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam crossbeam-utils 0.8.21 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam crunchy 0.2.4 — MIT — https://github.com/eira-fransham/crunchy crypto-common 0.1.7 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits ctr 0.9.2 — MIT OR Apache-2.0 — https://github.com/RustCrypto/block-modes + cudarc 0.16.6 — MIT OR Apache-2.0 — https://github.com/coreylowman/cudarc curve25519-dalek 4.1.3 — BSD-3-Clause — https://github.com/dalek-cryptography/curve25519-dalek/tree/main/curve25519-dalek curve25519-dalek-derive 0.1.1 — MIT/Apache-2.0 — https://github.com/dalek-cryptography/curve25519-dalek data-encoding 2.11.0 — MIT — https://github.com/ia0/data-encoding + defmt 1.1.1 — MIT OR Apache-2.0 — https://github.com/knurling-rs/defmt + defmt-macros 1.1.1 — MIT OR Apache-2.0 — https://github.com/knurling-rs/defmt + defmt-parser 1.0.0 — MIT OR Apache-2.0 — https://github.com/knurling-rs/defmt der 0.7.10 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/der der-parser 9.0.0 — MIT/Apache-2.0 — https://github.com/rusticata/der-parser.git deranged 0.5.8 — MIT OR Apache-2.0 — https://github.com/jhpratt/deranged @@ -126,6 +137,8 @@ MANIFEST (crate version — SPDX license — source) enumflags2 0.7.12 — MIT OR Apache-2.0 — https://github.com/meithecatte/enumflags2 enumflags2_derive 0.7.12 — MIT OR Apache-2.0 — https://github.com/meithecatte/enumflags2 env_filter 0.1.4 — MIT OR Apache-2.0 — https://github.com/rust-cli/env_logger + env_filter 2.0.0 — MIT OR Apache-2.0 — https://github.com/rust-cli/env_logger + env_logger 0.11.11 — MIT OR Apache-2.0 — https://github.com/rust-cli/env_logger equivalent 1.0.2 — Apache-2.0 OR MIT — https://github.com/indexmap-rs/equivalent errno 0.3.14 — MIT OR Apache-2.0 — https://github.com/lambda-fairy/rust-errno event-listener 5.4.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/event-listener @@ -221,6 +234,9 @@ MANIFEST (crate version — SPDX license — source) itertools 0.10.5 — MIT/Apache-2.0 — https://github.com/rust-itertools/itertools itertools 0.13.0 — MIT OR Apache-2.0 — https://github.com/rust-itertools/itertools itoa 1.0.18 — MIT OR Apache-2.0 — https://github.com/dtolnay/itoa + jiff 0.2.35 — Unlicense OR MIT — https://github.com/BurntSushi/jiff + jiff-core 0.1.0 — Unlicense OR MIT — https://github.com/BurntSushi/jiff + jiff-static 0.2.35 — Unlicense OR MIT — https://github.com/BurntSushi/jiff jni 0.21.1 — MIT/Apache-2.0 — https://github.com/jni-rs/jni-rs jni-sys 0.3.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys jni-sys 0.4.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys @@ -273,6 +289,7 @@ MANIFEST (crate version — SPDX license — source) num_cpus 1.17.0 — MIT OR Apache-2.0 — https://github.com/seanmonstar/num_cpus num_enum 0.7.6 — BSD-3-Clause OR MIT OR Apache-2.0 — https://github.com/illicitonion/num_enum num_enum_derive 0.7.6 — BSD-3-Clause OR MIT OR Apache-2.0 — https://github.com/illicitonion/num_enum + nvidia-video-codec-sdk 0.4.0 — MIT — https://github.com/ViliamVadocz/nvidia-video-codec-sdk oid-registry 0.7.1 — MIT OR Apache-2.0 — https://github.com/rusticata/oid-registry.git once_cell 1.21.4 — MIT OR Apache-2.0 — https://github.com/matklad/once_cell once_cell_polyfill 1.70.2 — MIT OR Apache-2.0 — https://github.com/polyfill-rs/once_cell_polyfill @@ -304,6 +321,8 @@ MANIFEST (crate version — SPDX license — source) polling 3.11.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/polling poly1305 0.8.0 — Apache-2.0 OR MIT — https://github.com/RustCrypto/universal-hashes polyval 0.6.2 — Apache-2.0 OR MIT — https://github.com/RustCrypto/universal-hashes + portable-atomic 1.14.0 — Apache-2.0 OR MIT — https://github.com/taiki-e/portable-atomic + portable-atomic-util 0.2.7 — Apache-2.0 OR MIT — https://github.com/taiki-e/portable-atomic-util potential_utf 0.1.5 — Unicode-3.0 — https://github.com/unicode-org/icu4x powerfmt 0.2.0 — MIT OR Apache-2.0 — https://github.com/jhpratt/powerfmt ppv-lite86 0.2.21 — MIT OR Apache-2.0 — https://github.com/cryptocorrosion/cryptocorrosion @@ -327,7 +346,11 @@ MANIFEST (crate version — SPDX license — source) rand_core 0.6.4 — MIT OR Apache-2.0 — https://github.com/rust-random/rand rand_core 0.9.5 — MIT OR Apache-2.0 — https://github.com/rust-random/rand rand_xorshift 0.4.0 — MIT OR Apache-2.0 — https://github.com/rust-random/rngs + rav1d 1.1.0 — BSD-2-Clause — https://github.com/memorysafety/rav1d + raw-cpuid 11.6.0 — MIT — https://github.com/gz/rust-cpuid raw-window-handle 0.6.2 — MIT OR Apache-2.0 OR Zlib — https://github.com/rust-windowing/raw-window-handle + rayon 1.12.0 — MIT OR Apache-2.0 — https://github.com/rayon-rs/rayon + rayon-core 1.13.0 — MIT OR Apache-2.0 — https://github.com/rayon-rs/rayon rcgen 0.13.2 — MIT OR Apache-2.0 — https://github.com/rustls/rcgen readme-rustdocifier 0.1.1 — MIT — https://github.com/malaire/readme-rustdocifier redox_syscall 0.5.18 — MIT — https://gitlab.redox-os.org/redox-os/syscall @@ -404,6 +427,8 @@ MANIFEST (crate version — SPDX license — source) sqlite-wasm-rs 0.5.5 — MIT — https://github.com/Spxg/sqlite-wasm-rs stable_deref_trait 1.2.1 — MIT OR Apache-2.0 — https://github.com/storyyeller/stable_deref_trait strsim 0.11.1 — MIT — https://github.com/rapidfuzz/strsim-rs + strum 0.26.3 — MIT — https://github.com/Peternator7/strum + strum_macros 0.26.4 — MIT — https://github.com/Peternator7/strum subtle 2.6.1 — BSD-3-Clause — https://github.com/dalek-cryptography/subtle syn 2.0.118 — MIT OR Apache-2.0 — https://github.com/dtolnay/syn sync_wrapper 1.0.2 — Apache-2.0 — https://github.com/Actyx/sync_wrapper @@ -425,6 +450,7 @@ MANIFEST (crate version — SPDX license — source) tinytemplate 1.2.1 — Apache-2.0 OR MIT — https://github.com/bheisler/TinyTemplate tinyvec 1.11.0 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/tinyvec tinyvec_macros 0.1.1 — MIT OR Apache-2.0 OR Zlib — https://github.com/Soveu/tinyvec_macros + to_method 1.1.0 — CC0-1.0 — https://github.com/whentze/to_method tokio 1.52.3 — MIT — https://github.com/tokio-rs/tokio tokio-macros 2.7.0 — MIT — https://github.com/tokio-rs/tokio tokio-rustls 0.26.4 — MIT OR Apache-2.0 — https://github.com/rustls/tokio-rustls @@ -449,6 +475,7 @@ MANIFEST (crate version — SPDX license — source) tracing-log 0.2.0 — MIT — https://github.com/tokio-rs/tracing tracing-subscriber 0.3.23 — MIT — https://github.com/tokio-rs/tracing typenum 1.20.1 — MIT OR Apache-2.0 — https://github.com/paholg/typenum + uac-host 0.1.0 — MIT OR Apache-2.0 — https://github.com/unom-io/usbfs-iso uds_windows 1.2.1 — MIT — https://github.com/haraldh/rust_uds_windows unarray 0.1.4 — MIT OR Apache-2.0 — https://github.com/cameron1024/unarray unicode-ident 1.0.24 — (MIT OR Apache-2.0) AND Unicode-3.0 — https://github.com/dtolnay/unicode-ident @@ -458,6 +485,7 @@ MANIFEST (crate version — SPDX license — source) untrusted 0.9.0 — ISC — https://github.com/briansmith/untrusted ureq 2.12.1 — MIT OR Apache-2.0 — https://github.com/algesten/ureq url 2.5.8 — MIT OR Apache-2.0 — https://github.com/servo/rust-url + usbfs-iso 0.1.0 — MIT OR Apache-2.0 — https://github.com/unom-io/usbfs-iso utf8_iter 1.0.4 — Apache-2.0 OR MIT — https://github.com/hsivonen/utf8_iter utf8parse 0.2.2 — Apache-2.0 OR MIT — https://github.com/alacritty/vte utoipa 5.5.0 — MIT OR Apache-2.0 — https://github.com/juhaku/utoipa @@ -575,7 +603,9 @@ MANIFEST (crate version — SPDX license — source) zbus 5.16.0 — MIT — https://github.com/z-galaxy/zbus/ zbus_macros 5.16.0 — MIT — https://github.com/z-galaxy/zbus/ zbus_names 4.3.2 — MIT — https://github.com/z-galaxy/zbus/ + zerocopy 0.7.35 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy zerocopy 0.8.52 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy + zerocopy-derive 0.7.35 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy zerocopy-derive 0.8.52 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy zerofrom 0.1.8 — Unicode-3.0 — https://github.com/unicode-org/icu4x zerofrom-derive 0.1.7 — Unicode-3.0 — https://github.com/unicode-org/icu4x @@ -595,7 +625,9 @@ Crates whose package did not embed a license file (SPDX + source only) ---------------------------------------------------------------------------- anes 0.1.6 — MIT OR Apache-2.0 — https://github.com/zrzka/anes-rs asn1-rs-impl 0.2.0 — MIT/Apache-2.0 — https://github.com/rusticata/asn1-rs.git + atomig-macro 0.4.0 — MIT/Apache-2.0 — https://github.com/LukasKalbertodt/atomig/ cookie-factory 0.3.3 — MIT — https://github.com/rust-bakery/cookie-factory + defmt-parser 1.0.0 — MIT OR Apache-2.0 — https://github.com/knurling-rs/defmt ffmpeg-sys-next 8.1.0 — WTFPL — https://github.com/zmwangx/rust-ffmpeg-sys jni-sys-macros 0.4.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys ndk 0.9.0 — MIT OR Apache-2.0 — https://github.com/rust-mobile/ndk @@ -612,6 +644,8 @@ Crates whose package did not embed a license file (SPDX + source only) skia-bindings 0.87.0 — MIT — https://github.com/rust-skia/rust-skia skia-safe 0.87.0 — MIT — https://github.com/rust-skia/rust-skia test_reactor 0.0.0 — UNKNOWN + uac-host 0.1.0 — MIT OR Apache-2.0 — https://github.com/unom-io/usbfs-iso + usbfs-iso 0.1.0 — MIT OR Apache-2.0 — https://github.com/unom-io/usbfs-iso valuable 0.1.1 — MIT — https://github.com/tokio-rs/valuable winapi-i686-pc-windows-gnu 0.4.0 — MIT/Apache-2.0 — https://github.com/retep998/winapi-rs winapi-x86_64-pc-windows-gnu 0.4.0 — MIT/Apache-2.0 — https://github.com/retep998/winapi-rs @@ -845,7 +879,7 @@ limitations under the License. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: adler2 2.0.1, anyhow 1.0.103, async-channel 2.5.0, async-executor 1.14.0, async-io 2.6.0, async-lock 3.4.2, async-process 2.5.0, async-recursion 1.1.1, async-signal 0.2.14, async-task 4.7.1, async-trait 0.1.89, atomic-waker 1.1.2, blocking 1.6.2, concurrent-queue 2.5.0, curve25519-dalek-derive 0.1.1, displaydoc 0.2.6, endi 1.1.1, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, flume 0.12.0, fs-err 3.3.0, futures-lite 2.6.1, hermit-abi 0.5.2, is-terminal 0.4.17, itoa 1.0.18, khronos-egl 6.0.0, linux-raw-sys 0.12.1, minimal-lexical 0.2.1, num_enum 0.7.6, num_enum_derive 0.7.6, once_cell 1.21.4, ordered-stream 0.2.0, parking 2.2.1, paste 1.0.15, pastey 0.2.3, pin-project-lite 0.2.17, piper 0.2.5, polling 3.11.0, prettyplease 0.2.37, proc-macro-crate 3.5.0, proc-macro2 1.0.106, quote 1.0.46, reis 0.6.1, relm4 0.11.0, relm4-css 0.11.0, relm4-macros 0.11.0, rsa 0.9.10, rustc-hash 2.1.2, rustix 1.1.4, rustversion 1.0.22, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, serde_path_to_error 0.1.20, serde_repr 0.1.20, syn 2.0.118, system-deps 7.0.8, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1, zmij 1.0.21, zvariant_utils 3.4.0 +The following license (LICENSE-MIT) applies to: adler2 2.0.1, anyhow 1.0.103, async-channel 2.5.0, async-executor 1.14.0, async-io 2.6.0, async-lock 3.4.2, async-process 2.5.0, async-recursion 1.1.1, async-signal 0.2.14, async-task 4.7.1, async-trait 0.1.89, atomic-waker 1.1.2, blocking 1.6.2, concurrent-queue 2.5.0, cudarc 0.16.6, curve25519-dalek-derive 0.1.1, displaydoc 0.2.6, endi 1.1.1, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, flume 0.12.0, fs-err 3.3.0, futures-lite 2.6.1, hermit-abi 0.5.2, is-terminal 0.4.17, itoa 1.0.18, khronos-egl 6.0.0, linux-raw-sys 0.12.1, minimal-lexical 0.2.1, num_enum 0.7.6, num_enum_derive 0.7.6, once_cell 1.21.4, ordered-stream 0.2.0, parking 2.2.1, paste 1.0.15, pastey 0.2.3, pin-project-lite 0.2.17, piper 0.2.5, polling 3.11.0, portable-atomic 1.14.0, portable-atomic-util 0.2.7, prettyplease 0.2.37, proc-macro-crate 3.5.0, proc-macro2 1.0.106, quote 1.0.46, reis 0.6.1, relm4 0.11.0, relm4-css 0.11.0, relm4-macros 0.11.0, rsa 0.9.10, rustc-hash 2.1.2, rustix 1.1.4, rustversion 1.0.22, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, serde_path_to_error 0.1.20, serde_repr 0.1.20, syn 2.0.118, system-deps 7.0.8, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1, zmij 1.0.21, zvariant_utils 3.4.0 ---------------------------------------------------------------------------- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -1170,7 +1204,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (COPYING) applies to: aho-corasick 1.1.4, memchr 2.8.2, same-file 1.0.6, walkdir 2.5.0, winapi-util 0.1.11 +The following license (COPYING) applies to: aho-corasick 1.1.4, byteorder 1.5.0, jiff 0.2.35, jiff-core 0.1.0, jiff-static 0.2.35, memchr 2.8.2, same-file 1.0.6, walkdir 2.5.0, winapi-util 0.1.11 ---------------------------------------------------------------------------- This project is dual-licensed under the Unlicense and MIT licenses. @@ -1178,7 +1212,7 @@ You may use this code under the terms of either license. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: aho-corasick 1.1.4, byteorder-lite 0.1.0, memchr 2.8.2, walkdir 2.5.0 +The following license (LICENSE-MIT) applies to: aho-corasick 1.1.4, byteorder 1.5.0, byteorder-lite 0.1.0, jiff 0.2.35, jiff-core 0.1.0, jiff-static 0.2.35, memchr 2.8.2, walkdir 2.5.0 ---------------------------------------------------------------------------- The MIT License (MIT) @@ -1204,7 +1238,7 @@ THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (UNLICENSE) applies to: aho-corasick 1.1.4, byteorder-lite 0.1.0, ksni 0.3.5, memchr 2.8.2, same-file 1.0.6, walkdir 2.5.0, winapi-util 0.1.11 +The following license (UNLICENSE) applies to: aho-corasick 1.1.4, byteorder 1.5.0, byteorder-lite 0.1.0, jiff 0.2.35, jiff-core 0.1.0, jiff-static 0.2.35, ksni 0.3.5, memchr 2.8.2, same-file 1.0.6, walkdir 2.5.0, winapi-util 0.1.11 ---------------------------------------------------------------------------- This is free and unencumbered software released into the public domain. @@ -1693,7 +1727,7 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: annotate-snippets 0.11.5, anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, crc32fast 1.5.0, env_filter 0.1.4, fallible-iterator 0.3.0, fallible-streaming-iterator 0.1.9, hex 0.4.3, is_terminal_polyfill 1.70.2, jni-sys 0.3.1, jni-sys 0.4.1, once_cell_polyfill 1.70.2, quick-error 1.2.3, serde_spanned 0.6.9, serde_spanned 1.1.1, toml 0.8.23, toml 0.9.12+spec-1.1.0, toml 1.1.2+spec-1.1.0, toml_datetime 0.6.11, toml_datetime 0.7.5+spec-1.1.0, toml_datetime 1.1.1+spec-1.1.0, toml_edit 0.22.27, toml_edit 0.25.12+spec-1.1.0, toml_parser 1.1.2+spec-1.1.0, toml_write 0.1.2, toml_writer 1.1.1+spec-1.1.0, winapi 0.3.9 +The following license (LICENSE-APACHE) applies to: annotate-snippets 0.11.5, anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, crc32fast 1.5.0, env_filter 0.1.4, env_filter 2.0.0, env_logger 0.11.11, fallible-iterator 0.3.0, fallible-streaming-iterator 0.1.9, hex 0.4.3, is_terminal_polyfill 1.70.2, jni-sys 0.3.1, jni-sys 0.4.1, once_cell_polyfill 1.70.2, quick-error 1.2.3, serde_spanned 0.6.9, serde_spanned 1.1.1, toml 0.8.23, toml 0.9.12+spec-1.1.0, toml 1.1.2+spec-1.1.0, toml_datetime 0.6.11, toml_datetime 0.7.5+spec-1.1.0, toml_datetime 1.1.1+spec-1.1.0, toml_edit 0.22.27, toml_edit 0.25.12+spec-1.1.0, toml_parser 1.1.2+spec-1.1.0, toml_write 0.1.2, toml_writer 1.1.1+spec-1.1.0, winapi 0.3.9 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -1899,7 +1933,7 @@ Apache License ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: annotate-snippets 0.11.5, anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, env_filter 0.1.4, is_terminal_polyfill 1.70.2, once_cell_polyfill 1.70.2, serde_spanned 0.6.9, serde_spanned 1.1.1, toml 0.8.23, toml 0.9.12+spec-1.1.0, toml 1.1.2+spec-1.1.0, toml_datetime 0.6.11, toml_datetime 0.7.5+spec-1.1.0, toml_datetime 1.1.1+spec-1.1.0, toml_edit 0.22.27, toml_edit 0.25.12+spec-1.1.0, toml_parser 1.1.2+spec-1.1.0, toml_write 0.1.2, toml_writer 1.1.1+spec-1.1.0 +The following license (LICENSE-MIT) applies to: annotate-snippets 0.11.5, anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, env_filter 0.1.4, env_filter 2.0.0, env_logger 0.11.11, is_terminal_polyfill 1.70.2, once_cell_polyfill 1.70.2, serde_spanned 0.6.9, serde_spanned 1.1.1, toml 0.8.23, toml 0.9.12+spec-1.1.0, toml 1.1.2+spec-1.1.0, toml_datetime 0.6.11, toml_datetime 0.7.5+spec-1.1.0, toml_datetime 1.1.1+spec-1.1.0, toml_edit 0.22.27, toml_edit 0.25.12+spec-1.1.0, toml_parser 1.1.2+spec-1.1.0, toml_write 0.1.2, toml_writer 1.1.1+spec-1.1.0 ---------------------------------------------------------------------------- Copyright (c) Individual contributors @@ -1923,7 +1957,7 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: anyhow 1.0.103, async-trait 0.1.89, fastbloom 0.14.1, itoa 1.0.18, libc 0.2.186, num_enum 0.7.6, num_enum_derive 0.7.6, paste 1.0.15, pastey 0.2.3, prettyplease 0.2.37, proc-macro2 1.0.106, quote 1.0.46, relm4 0.11.0, relm4-css 0.11.0, relm4-macros 0.11.0, rustc-hash 2.1.2, rustversion 1.0.22, ryu 1.0.23, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, serde_path_to_error 0.1.20, serde_repr 0.1.20, serde_urlencoded 0.7.1, syn 2.0.118, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, utf8parse 0.2.2 +The following license (LICENSE-APACHE) applies to: anyhow 1.0.103, async-trait 0.1.89, cudarc 0.16.6, fastbloom 0.14.1, itoa 1.0.18, libc 0.2.186, num_enum 0.7.6, num_enum_derive 0.7.6, paste 1.0.15, pastey 0.2.3, prettyplease 0.2.37, proc-macro2 1.0.106, quote 1.0.46, relm4 0.11.0, relm4-css 0.11.0, relm4-macros 0.11.0, rustc-hash 2.1.2, rustversion 1.0.22, ryu 1.0.23, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, serde_path_to_error 0.1.20, serde_repr 0.1.20, serde_urlencoded 0.7.1, syn 2.0.118, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, utf8parse 0.2.2 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -2232,7 +2266,7 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: asn1-rs 0.6.2, asn1-rs-derive 0.5.1, async-channel 2.5.0, async-executor 1.14.0, async-io 2.6.0, async-lock 3.4.2, async-process 2.5.0, async-recursion 1.1.1, async-signal 0.2.14, async-task 4.7.1, atomic-waker 1.1.2, autocfg 1.5.1, base64 0.22.1, bitflags 2.13.0, blocking 1.6.2, bumpalo 3.20.3, cast 0.3.0, cc 1.2.65, cexpr 0.6.0, cfg-if 1.0.4, cmake 0.1.58, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, der-parser 9.0.0, displaydoc 0.2.6, either 1.16.0, equivalent 1.0.2, errno 0.3.14, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, filetime 0.2.29, find-msvc-tools 0.1.9, fixedbitset 0.5.7, flate2 1.1.9, fnv 1.0.7, form_urlencoded 1.2.2, fs-err 3.3.0, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glob 0.3.3, hashbrown 0.16.1, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, httparse 1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.0, itertools 0.10.5, itertools 0.13.0, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, khronos-egl 6.0.0, lazy_static 1.5.0, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, mime 0.3.17, nasm-rs 0.3.2, num-bigint 0.4.6, num-bigint-dig 0.8.6, num-derive 0.4.2, num-integer 0.1.46, num-iter 0.1.45, num-traits 0.2.19, num_cpus 1.17.0, oid-registry 0.7.1, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, ordered-stream 0.2.0, parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5, pkg-config 0.3.33, png 0.18.1, polling 3.11.0, proptest 1.11.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, roxmltree 0.21.1, rsa 0.9.10, rustc_version 0.4.1, rusticata-macros 4.1.0, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, stable_deref_trait 1.2.1, system-deps 7.0.8, tar 0.4.46, tempfile 3.27.0, thread_local 1.1.9, tinytemplate 1.2.1, unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, uuid 1.23.4, vcpkg 0.2.15, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, weezl 0.1.12, wit-bindgen 0.57.1, x509-parser 0.16.0, xattr 1.6.1 +The following license (LICENSE-APACHE) applies to: asn1-rs 0.6.2, asn1-rs-derive 0.5.1, assert_matches 1.5.0, async-channel 2.5.0, async-executor 1.14.0, async-io 2.6.0, async-lock 3.4.2, async-process 2.5.0, async-recursion 1.1.1, async-signal 0.2.14, async-task 4.7.1, atomic-waker 1.1.2, autocfg 1.5.1, base64 0.22.1, bitflags 1.3.2, bitflags 2.13.0, blocking 1.6.2, bumpalo 3.20.3, cast 0.3.0, cc 1.2.65, cexpr 0.6.0, cfg-if 1.0.4, cmake 0.1.58, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, der-parser 9.0.0, displaydoc 0.2.6, either 1.16.0, equivalent 1.0.2, errno 0.3.14, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, filetime 0.2.29, find-msvc-tools 0.1.9, fixedbitset 0.5.7, flate2 1.1.9, fnv 1.0.7, form_urlencoded 1.2.2, fs-err 3.3.0, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glob 0.3.3, hashbrown 0.16.1, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, httparse 1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.0, itertools 0.10.5, itertools 0.13.0, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, khronos-egl 6.0.0, lazy_static 1.5.0, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, mime 0.3.17, nasm-rs 0.3.2, num-bigint 0.4.6, num-bigint-dig 0.8.6, num-derive 0.4.2, num-integer 0.1.46, num-iter 0.1.45, num-traits 0.2.19, num_cpus 1.17.0, oid-registry 0.7.1, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, ordered-stream 0.2.0, parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5, pkg-config 0.3.33, png 0.18.1, polling 3.11.0, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, roxmltree 0.21.1, rsa 0.9.10, rustc_version 0.4.1, rusticata-macros 4.1.0, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, stable_deref_trait 1.2.1, system-deps 7.0.8, tar 0.4.46, tempfile 3.27.0, thread_local 1.1.9, tinytemplate 1.2.1, unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, uuid 1.23.4, vcpkg 0.2.15, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, weezl 0.1.12, wit-bindgen 0.57.1, x509-parser 0.16.0, xattr 1.6.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -2467,6 +2501,36 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: assert_matches 1.5.0 +---------------------------------------------------------------------------- +Copyright (c) 2016 Murarth + +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 following license (LICENSE-APACHE) applies to: async-broadcast 0.7.2 ---------------------------------------------------------------------------- @@ -2738,6 +2802,242 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: atomig 0.4.3, bit-set 0.8.0, bit-vec 0.8.0, cfg-expr 0.20.8, defmt 1.1.1, defmt-macros 1.1.1, downcast-rs 1.2.1, hashlink 0.12.0, minimal-lexical 0.2.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: atomig 0.4.3 +---------------------------------------------------------------------------- +Copyright (c) 2016 Project Developers + +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 following license (LICENSE.md) applies to: audiopus_sys 0.2.2 ---------------------------------------------------------------------------- @@ -2984,212 +3284,6 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: bit-set 0.8.0, bit-vec 0.8.0, cfg-expr 0.20.8, downcast-rs 1.2.1, hashlink 0.12.0, minimal-lexical 0.2.1 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ---------------------------------------------------------------------------- The following license (LICENSE-MIT) applies to: bit-set 0.8.0, bit-vec 0.8.0 ---------------------------------------------------------------------------- @@ -3221,7 +3315,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: bitflags 2.13.0, glob 0.3.3, log 0.4.33, num-bigint 0.4.6, num-bigint-dig 0.8.6, num-derive 0.4.2, num-integer 0.1.46, num-iter 0.1.45, num-traits 0.2.19, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11 +The following license (LICENSE-MIT) applies to: bitflags 1.3.2, bitflags 2.13.0, glob 0.3.3, log 0.4.33, num-bigint 0.4.6, num-bigint-dig 0.8.6, num-derive 0.4.2, num-integer 0.1.46, num-iter 0.1.45, num-traits 0.2.19, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11 ---------------------------------------------------------------------------- Copyright (c) 2014 The Rust Project Developers @@ -4969,7 +5063,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: crossbeam-utils 0.8.21 +The following license (LICENSE-MIT) applies to: crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21 ---------------------------------------------------------------------------- The MIT License (MIT) @@ -5153,6 +5247,36 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: defmt 1.1.1, defmt-macros 1.1.1 +---------------------------------------------------------------------------- +Copyright (c) Ferrous Systems + +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 following license (LICENSE-MIT) applies to: der 0.7.10, pkcs8 0.10.2 ---------------------------------------------------------------------------- @@ -5848,7 +5972,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: fdeflate 0.3.7, field-offset 0.3.6, half 2.7.1, image 0.25.10, miniz_oxide 0.8.9, num-conv 0.2.2, pin-project-lite 0.2.17, raw-window-handle 0.6.2, sync_wrapper 1.0.2, time 0.3.51, time-core 0.1.9, time-macros 0.2.30, widestring 1.2.1 +The following license (LICENSE-APACHE) applies to: fdeflate 0.3.7, field-offset 0.3.6, half 2.7.1, image 0.25.10, miniz_oxide 0.8.9, num-conv 0.2.2, pin-project-lite 0.2.17, portable-atomic 1.14.0, portable-atomic-util 0.2.7, raw-window-handle 0.6.2, sync_wrapper 1.0.2, time 0.3.51, time-core 0.1.9, time-macros 0.2.30, widestring 1.2.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -8076,7 +8200,7 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: lazy_static 1.5.0 +The following license (LICENSE-MIT) applies to: lazy_static 1.5.0, rayon 1.12.0, rayon-core 1.13.0 ---------------------------------------------------------------------------- Copyright (c) 2010 The Rust Project Developers @@ -10121,6 +10245,18 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: nvidia-video-codec-sdk 0.4.0 +---------------------------------------------------------------------------- +Copyright 2023 Viliam Vadocz + +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 following license (LICENSE-MIT) applies to: oorandom 11.1.5 ---------------------------------------------------------------------------- @@ -11397,6 +11533,61 @@ APPENDIX: How to apply the Apache License to your work. identification within third-party archives. +---------------------------------------------------------------------------- +The following license (COPYING) applies to: rav1d 1.1.0 +---------------------------------------------------------------------------- +Copyright © 2018-2019, VideoLAN and dav1d authors +Copyright © 2023-2024, VideoLAN, dav1d authors, and Internet Security Research Group +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE.md) applies to: raw-cpuid 11.6.0 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Gerd Zellweger + +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 following license (LICENSE-MIT.md) applies to: raw-window-handle 0.6.2 ---------------------------------------------------------------------------- @@ -13264,6 +13455,32 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: strum 0.26.3, strum_macros 0.26.4 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2019 Peter Glotfelty + +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 following license (LICENSE) applies to: subtle 2.6.1 ---------------------------------------------------------------------------- @@ -13690,6 +13907,132 @@ freely, subject to the following restrictions: 3. This notice may not be removed or altered from any source distribution. +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: to_method 1.1.0 +---------------------------------------------------------------------------- +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. + + ---------------------------------------------------------------------------- The following license (LICENSE) applies to: tokio 1.52.3, tokio-util 0.7.18 ---------------------------------------------------------------------------- @@ -16105,7 +16448,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: zerocopy 0.8.52, zerocopy-derive 0.8.52 +The following license (LICENSE-APACHE) applies to: zerocopy 0.7.35, zerocopy 0.8.52, zerocopy-derive 0.7.35, zerocopy-derive 0.8.52 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -16311,7 +16654,7 @@ Apache License ---------------------------------------------------------------------------- -The following license (LICENSE-BSD) applies to: zerocopy 0.8.52, zerocopy-derive 0.8.52 +The following license (LICENSE-BSD) applies to: zerocopy 0.7.35, zerocopy 0.8.52, zerocopy-derive 0.7.35, zerocopy-derive 0.8.52 ---------------------------------------------------------------------------- Copyright 2019 The Fuchsia Authors. @@ -16340,7 +16683,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: zerocopy 0.8.52, zerocopy-derive 0.8.52 +The following license (LICENSE-MIT) applies to: zerocopy 0.7.35, zerocopy 0.8.52, zerocopy-derive 0.7.35, zerocopy-derive 0.8.52 ---------------------------------------------------------------------------- Copyright 2023 The Fuchsia Authors diff --git a/about.toml b/about.toml index becd44f7..618b733f 100644 --- a/about.toml +++ b/about.toml @@ -4,10 +4,22 @@ # cargo about generate about.hbs > THIRD-PARTY-NOTICES.txt # (or use scripts/gen-third-party-notices.sh) # # `accepted` is the allow-list of SPDX licenses permitted in the dependency tree. CI fails if a crate -# carries anything not listed here — which is exactly the regression guard we want against a copyleft -# dependency silently entering the linked set. All entries +# carries anything not listed here — the regression guard against a copyleft dependency silently +# entering the linked set. All entries # below are permissive / attribution-only; deliberately NO GPL/LGPL/AGPL/MPL-link/SSPL/EPL. # +# ⚠ KNOW THE LIMIT OF THIS GATE. cargo-about walks the CARGO graph, so it sees CRATES. A native +# library linked through a permissively-licensed `-sys` crate is INVISIBLE to it, licence and all. +# FFmpeg is precisely that shape: `ffmpeg-sys-next` is WTFPL and passes cleanly, while the LGPL +# libavcodec/libavutil/swscale it link-imports — and which the Windows host installer bundles as +# DLLs — never appear in the harvest at all. This gate did not catch FFmpeg entering the tree and +# would not catch the next such library. Copyleft arriving as C behind a -sys crate is a REVIEW +# question, not a CI one; the LGPL obligations we do carry are discharged by hand (the notice files +# and the replaceable-DLL linkage, see packaging/windows/punktfunk-host.iss). +# +# Since M10 this is a HOST-only concern: the client links no FFmpeg, so for every client artifact +# the crate graph and the linked set finally coincide and the gate means what it appears to mean. +# # The dependency-free fallback is scripts/gen-third-party-notices.py (reads the cargo registry cache), # which is what produced the committed baseline when cargo-about is unavailable offline. diff --git a/ci/arch-ci.Dockerfile b/ci/arch-ci.Dockerfile index 4263212d..5eb243ae 100644 --- a/ci/arch-ci.Dockerfile +++ b/ci/arch-ci.Dockerfile @@ -15,6 +15,11 @@ FROM docker.io/library/archlinux:base-devel # One transaction: the main build/runtime deps (first list) + the gamescope companion's # deps (second list) — both copied verbatim from what arch.yml installed in-job, where # they now no-op as `--needed` guards. +# vulkan-headers rides the first list only because arch.yml's copy does; the package it actually +# serves is the gamescope companion (packaging/gamescope/PKGBUILD makedepends). punktfunk itself +# needs no system Vulkan headers — pyrowave-sys bindgens its own vendored copy and ash dlopens the +# loader — but arch.yml builds gamescope with `makepkg -d`, so an absent makedepend would not be +# reported as a missing dependency, only as a compile failure. Keep it. RUN pacman -Syu --noconfirm --needed \ git nodejs rust clang cmake ninja nasm pkgconf python vulkan-headers \ gtk4 libadwaita sdl3 ffmpeg pipewire wayland libxkbcommon opus libei \ diff --git a/ci/fedora-rpm.Dockerfile b/ci/fedora-rpm.Dockerfile index b2fdf183..112c7c26 100644 --- a/ci/fedora-rpm.Dockerfile +++ b/ci/fedora-rpm.Dockerfile @@ -27,8 +27,10 @@ RUN dnf -y install \ mesa-libGL-devel mesa-libgbm-devel \ # punktfunk-client link deps (GTK4 shell + SDL3 gamepads) gtk4-devel libadwaita-devel SDL3-devel \ - # pf-ffvk bindgen over libavutil/hwcontext_vulkan.h needs - vulkan-headers \ + # No vulkan-headers: nothing in the workspace compiles against the system Vulkan headers + # (pyrowave-sys bindgens its own vendored copy; host and client both reach Vulkan through + # ash, which dlopens the loader), and packaging/rpm/punktfunk.spec BuildRequires none. + # rpm.yml's HDR gamescope leg needs them and pulls them with `dnf builddep gamescope`. && dnf clean all # bun — both the BUILD tool and the RUNTIME for the punktfunk-web console (`bun run build` -> the diff --git a/ci/rust-ci-arm64cross.Dockerfile b/ci/rust-ci-arm64cross.Dockerfile index 6bd4f109..cf92add4 100644 --- a/ci/rust-ci-arm64cross.Dockerfile +++ b/ci/rust-ci-arm64cross.Dockerfile @@ -29,15 +29,16 @@ RUN sed -i 's|^Types: deb$|Types: deb\nArchitectures: amd64|' /etc/apt/sources.l && dpkg --add-architecture arm64 # 2. The cross toolchain + every arm64 dev lib the client links. Mirrors the client half of -# rust-ci.Dockerfile's list (FFmpeg, PipeWire, Opus, SDL3, GTK4/libadwaita, xkbcommon, -# Vulkan headers for pf-ffvk's bindgen over hwcontext_vulkan.h). +# rust-ci.Dockerfile's list (FFmpeg, PipeWire, Opus, SDL3, GTK4/libadwaita, xkbcommon). No +# Vulkan dev package: nothing compiles or links against Vulkan — ash dlopens the loader, and +# pyrowave-sys bindgens its own vendored headers. RUN apt-get update && apt-get install -y --no-install-recommends \ crossbuild-essential-arm64 \ libavcodec-dev:arm64 libavformat-dev:arm64 libavutil-dev:arm64 libswscale-dev:arm64 \ libavfilter-dev:arm64 libavdevice-dev:arm64 \ libpipewire-0.3-dev:arm64 libopus-dev:arm64 \ libsdl3-dev:arm64 libgtk-4-dev:arm64 libadwaita-1-dev:arm64 \ - libwayland-dev:arm64 libxkbcommon-dev:arm64 libvulkan-dev:arm64 \ + libwayland-dev:arm64 libxkbcommon-dev:arm64 \ && rm -rf /var/lib/apt/lists/* # 3. The Rust target — installed against the toolchain the WORKSPACE pins, not the image's diff --git a/ci/rust-ci.Dockerfile b/ci/rust-ci.Dockerfile index 0ddb45ec..a4114ccc 100644 --- a/ci/rust-ci.Dockerfile +++ b/ci/rust-ci.Dockerfile @@ -22,8 +22,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libgl-dev libegl-dev libgbm-dev \ # punktfunk-client-linux (GTK4/libadwaita shell, SDL3 gamepads) libgtk-4-dev libadwaita-1-dev libsdl3-dev \ - # pf-ffvk (bindgen over libavutil/hwcontext_vulkan.h needs ) - libvulkan-dev \ + # No libvulkan-dev: nothing in the workspace compiles or links against Vulkan (pyrowave-sys + # bindgens its own vendored headers, and both host and client reach Vulkan through ash, which + # dlopens the loader), so neither the build nor deb.yml's dpkg-shlibdeps ever asks for it. && rm -rf /var/lib/apt/lists/* # bun — builds the punktfunk-web console in deb.yml (which runs the web build in THIS image). diff --git a/clients/android/app/src/main/assets/THIRD-PARTY-NOTICES.txt b/clients/android/app/src/main/assets/THIRD-PARTY-NOTICES.txt index fecd7fe9..7ec104a4 100644 --- a/clients/android/app/src/main/assets/THIRD-PARTY-NOTICES.txt +++ b/clients/android/app/src/main/assets/THIRD-PARTY-NOTICES.txt @@ -7,7 +7,9 @@ below. Each is distributed under its own permissive license; the full license te follow the manifest. This file is generated by scripts/gen-third-party-notices.py (or `cargo about`, see about.toml) — do not edit by hand. -Total third-party crates: 566 +Scope: the Rust crates linked by punktfunk-client-android — not the whole punktfunk workspace. + +Total third-party crates: 274 ---------------------------------------------------------------------------- VENDORED THIRD-PARTY SOURCE (inside first-party crates) @@ -23,7 +25,6 @@ VENDORED THIRD-PARTY SOURCE (inside first-party crates) ---------------------------------------------------------------------------- MANIFEST (crate version — SPDX license — source) ---------------------------------------------------------------------------- - adler2 2.0.1 — 0BSD OR MIT OR Apache-2.0 — https://github.com/oyvindln/adler2 aead 0.5.2 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits aes 0.8.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/block-ciphers aes-gcm 0.10.3 — Apache-2.0 OR MIT — https://github.com/RustCrypto/AEADs @@ -31,57 +32,25 @@ MANIFEST (crate version — SPDX license — source) android_log-sys 0.3.2 — MIT OR Apache-2.0 — https://github.com/rust-mobile/android_log-sys-rs android_logger 0.14.1 — MIT OR Apache-2.0 — https://github.com/rust-mobile/android_logger-rs anes 0.1.6 — MIT OR Apache-2.0 — https://github.com/zrzka/anes-rs - annotate-snippets 0.11.5 — MIT OR Apache-2.0 — https://github.com/rust-lang/annotate-snippets-rs anstream 1.0.0 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git anstyle 1.0.14 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git anstyle-parse 1.0.0 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git anstyle-query 1.1.5 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git anstyle-wincon 3.0.11 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git - anyhow 1.0.103 — MIT OR Apache-2.0 — https://github.com/dtolnay/anyhow - ash 0.38.0+1.3.281 — MIT OR Apache-2.0 — https://github.com/ash-rs/ash - ashpd 0.13.12 — MIT — https://github.com/bilelmoussaoui/ashpd - asn1-rs 0.6.2 — MIT OR Apache-2.0 — https://github.com/rusticata/asn1-rs.git - asn1-rs-derive 0.5.1 — MIT OR Apache-2.0 — https://github.com/rusticata/asn1-rs.git - asn1-rs-impl 0.2.0 — MIT/Apache-2.0 — https://github.com/rusticata/asn1-rs.git - async-broadcast 0.7.2 — MIT OR Apache-2.0 — https://github.com/smol-rs/async-broadcast - async-channel 2.5.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-channel - async-executor 1.14.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-executor - async-io 2.6.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-io - async-lock 3.4.2 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-lock - async-process 2.5.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-process - async-recursion 1.1.1 — MIT OR Apache-2.0 — https://github.com/dcchut/async-recursion - async-signal 0.2.14 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-signal - async-task 4.7.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-task - async-trait 0.1.89 — MIT OR Apache-2.0 — https://github.com/dtolnay/async-trait - atomic-waker 1.1.2 — Apache-2.0 OR MIT — https://github.com/smol-rs/atomic-waker audiopus_sys 0.2.2 — ISC — https://github.com/lakelezz/audiopus_sys.git autocfg 1.5.1 — Apache-2.0 OR MIT — https://github.com/cuviper/autocfg - axum 0.8.9 — MIT — https://github.com/tokio-rs/axum - axum-core 0.5.6 — MIT — https://github.com/tokio-rs/axum - axum-server 0.8.0 — MIT — https://github.com/programatik29/axum-server base64 0.22.1 — MIT OR Apache-2.0 — https://github.com/marshallpierce/rust-base64 - base64ct 1.8.3 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats - bindgen 0.72.1 — BSD-3-Clause — https://github.com/rust-lang/rust-bindgen bit-set 0.8.0 — Apache-2.0 OR MIT — https://github.com/contain-rs/bit-set bit-vec 0.8.0 — Apache-2.0 OR MIT — https://github.com/contain-rs/bit-vec bitflags 2.13.0 — MIT OR Apache-2.0 — https://github.com/bitflags/bitflags block-buffer 0.10.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils block-padding 0.3.3 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils - blocking 1.6.2 — Apache-2.0 OR MIT — https://github.com/smol-rs/blocking bumpalo 3.20.3 — MIT OR Apache-2.0 — https://github.com/fitzgen/bumpalo - bytemuck 1.25.0 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/bytemuck - bytemuck_derive 1.10.2 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/bytemuck - byteorder-lite 0.1.0 — Unlicense OR MIT — https://github.com/image-rs/byteorder-lite bytes 1.12.0 — MIT — https://github.com/tokio-rs/bytes - cairo-rs 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core - cairo-sys-rs 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core cast 0.3.0 — MIT OR Apache-2.0 — https://github.com/japaric/cast.rs - cbc 0.1.2 — MIT OR Apache-2.0 — https://github.com/RustCrypto/block-modes cbindgen 0.29.4 — MPL-2.0 — https://github.com/mozilla/cbindgen cc 1.2.65 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs cesu8 1.1.0 — Apache-2.0/MIT — https://github.com/emk/cesu8-rs - cexpr 0.6.0 — Apache-2.0/MIT — https://github.com/jethrogb/rust-cexpr - cfg-expr 0.20.8 — MIT OR Apache-2.0 — https://github.com/EmbarkStudios/cfg-expr cfg-if 1.0.4 — MIT OR Apache-2.0 — https://github.com/rust-lang/cfg-if cfg_aliases 0.2.1 — MIT — https://github.com/katharostech/cfg_aliases chacha20 0.9.1 — Apache-2.0 OR MIT — https://github.com/RustCrypto/stream-ciphers @@ -90,136 +59,65 @@ MANIFEST (crate version — SPDX license — source) ciborium-io 0.2.2 — Apache-2.0 — https://github.com/enarx/ciborium ciborium-ll 0.2.2 — Apache-2.0 — https://github.com/enarx/ciborium cipher 0.4.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits - clang-sys 1.8.1 — Apache-2.0 — https://github.com/KyleMayes/clang-sys clap 4.6.1 — MIT OR Apache-2.0 — https://github.com/clap-rs/clap clap_builder 4.6.0 — MIT OR Apache-2.0 — https://github.com/clap-rs/clap clap_lex 1.1.0 — MIT OR Apache-2.0 — https://github.com/clap-rs/clap cmake 0.1.58 — MIT OR Apache-2.0 — https://github.com/rust-lang/cmake-rs - color_quant 1.1.0 — MIT — https://github.com/image-rs/color_quant.git colorchoice 1.0.5 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git combine 4.6.7 — MIT — https://github.com/Marwes/combine - concurrent-queue 2.5.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/concurrent-queue const-oid 0.9.6 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/const-oid - convert_case 0.8.0 — MIT — https://github.com/rutrum/convert-case - cookie-factory 0.3.3 — MIT — https://github.com/rust-bakery/cookie-factory core-foundation 0.10.1 — MIT OR Apache-2.0 — https://github.com/servo/core-foundation-rs core-foundation-sys 0.8.7 — MIT OR Apache-2.0 — https://github.com/servo/core-foundation-rs cpufeatures 0.2.17 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils - crc32fast 1.5.0 — MIT OR Apache-2.0 — https://github.com/srijs/rust-crc32fast criterion 0.5.1 — Apache-2.0 OR MIT — https://github.com/bheisler/criterion.rs criterion-plot 0.5.0 — MIT/Apache-2.0 — https://github.com/bheisler/criterion.rs + crossbeam-deque 0.8.6 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam + crossbeam-epoch 0.9.20 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam crossbeam-utils 0.8.21 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam crunchy 0.2.4 — MIT — https://github.com/eira-fransham/crunchy crypto-common 0.1.7 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits ctr 0.9.2 — MIT OR Apache-2.0 — https://github.com/RustCrypto/block-modes curve25519-dalek 4.1.3 — BSD-3-Clause — https://github.com/dalek-cryptography/curve25519-dalek/tree/main/curve25519-dalek curve25519-dalek-derive 0.1.1 — MIT/Apache-2.0 — https://github.com/dalek-cryptography/curve25519-dalek - data-encoding 2.11.0 — MIT — https://github.com/ia0/data-encoding - der 0.7.10 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/der - der-parser 9.0.0 — MIT/Apache-2.0 — https://github.com/rusticata/der-parser.git deranged 0.5.8 — MIT OR Apache-2.0 — https://github.com/jhpratt/deranged digest 0.10.7 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits - displaydoc 0.2.6 — MIT OR Apache-2.0 — https://github.com/yaahc/displaydoc - downcast-rs 1.2.1 — MIT/Apache-2.0 — https://github.com/marcianx/downcast-rs either 1.16.0 — MIT OR Apache-2.0 — https://github.com/rayon-rs/either - endi 1.1.1 — MIT — https://github.com/zeenix/endi - enumflags2 0.7.12 — MIT OR Apache-2.0 — https://github.com/meithecatte/enumflags2 - enumflags2_derive 0.7.12 — MIT OR Apache-2.0 — https://github.com/meithecatte/enumflags2 env_filter 0.1.4 — MIT OR Apache-2.0 — https://github.com/rust-cli/env_logger equivalent 1.0.2 — Apache-2.0 OR MIT — https://github.com/indexmap-rs/equivalent errno 0.3.14 — MIT OR Apache-2.0 — https://github.com/lambda-fairy/rust-errno - event-listener 5.4.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/event-listener - event-listener-strategy 0.5.4 — Apache-2.0 OR MIT — https://github.com/smol-rs/event-listener-strategy - fallible-iterator 0.3.0 — MIT/Apache-2.0 — https://github.com/sfackler/rust-fallible-iterator - fallible-streaming-iterator 0.1.9 — MIT/Apache-2.0 — https://github.com/sfackler/fallible-streaming-iterator fastbloom 0.14.1 — MIT OR Apache-2.0 — https://github.com/tomtomwombat/fastbloom/ fastrand 2.4.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/fastrand - fdeflate 0.3.7 — MIT OR Apache-2.0 — https://github.com/image-rs/fdeflate - ffmpeg-next 8.1.0 — WTFPL — https://github.com/zmwangx/rust-ffmpeg - ffmpeg-sys-next 8.1.0 — WTFPL — https://github.com/zmwangx/rust-ffmpeg-sys fiat-crypto 0.2.9 — MIT OR Apache-2.0 OR BSD-1-Clause — https://github.com/mit-plv/fiat-crypto - field-offset 0.3.6 — MIT OR Apache-2.0 — https://github.com/Diggsey/rust-field-offset - filetime 0.2.29 — MIT/Apache-2.0 — https://github.com/alexcrichton/filetime find-msvc-tools 0.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs fixedbitset 0.5.7 — MIT OR Apache-2.0 — https://github.com/petgraph/fixedbitset - flate2 1.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/flate2-rs flume 0.12.0 — Apache-2.0/MIT — https://github.com/zesterer/flume fnv 1.0.7 — Apache-2.0 / MIT — https://github.com/servo/rust-fnv foldhash 0.2.0 — Zlib — https://github.com/orlp/foldhash - form_urlencoded 1.2.2 — MIT OR Apache-2.0 — https://github.com/servo/rust-url - fragile 2.1.0 — Apache-2.0 — https://github.com/mitsuhiko/fragile - fs-err 3.3.0 — MIT OR Apache-2.0 — https://github.com/andrewhickman/fs-err - futures 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs futures-channel 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs futures-core 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs - futures-executor 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs futures-io 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs - futures-lite 2.6.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/futures-lite futures-macro 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs futures-sink 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs futures-task 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs futures-util 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs - gdk-pixbuf 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core - gdk-pixbuf-sys 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core - gdk4 0.11.2 — MIT — https://github.com/gtk-rs/gtk4-rs - gdk4-sys 0.11.2 — MIT — https://github.com/gtk-rs/gtk4-rs generic-array 0.14.7 — MIT — https://github.com/fizyk20/generic-array.git - gethostname 1.1.0 — Apache-2.0 — https://codeberg.org/swsnr/gethostname.rs.git getrandom 0.2.17 — MIT OR Apache-2.0 — https://github.com/rust-random/getrandom getrandom 0.3.4 — MIT OR Apache-2.0 — https://github.com/rust-random/getrandom getrandom 0.4.3 — MIT OR Apache-2.0 — https://github.com/rust-random/getrandom ghash 0.5.1 — Apache-2.0 OR MIT — https://github.com/RustCrypto/universal-hashes - gif 0.14.2 — MIT OR Apache-2.0 — https://github.com/image-rs/image-gif - gio 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core - gio-sys 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core - glib 0.22.7 — MIT — https://github.com/gtk-rs/gtk-rs-core - glib-build-tools 0.22.8 — MIT — https://github.com/gtk-rs/gtk-rs-core - glib-macros 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core - glib-sys 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core - glob 0.3.3 — MIT OR Apache-2.0 — https://github.com/rust-lang/glob - gobject-sys 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core - graphene-rs 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core - graphene-sys 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core - gsk4 0.11.1 — MIT — https://github.com/gtk-rs/gtk4-rs - gsk4-sys 0.11.1 — MIT — https://github.com/gtk-rs/gtk4-rs - gtk4 0.11.3 — MIT — https://github.com/gtk-rs/gtk4-rs - gtk4-macros 0.11.0 — MIT — https://github.com/gtk-rs/gtk4-rs - gtk4-sys 0.11.3 — MIT — https://github.com/gtk-rs/gtk4-rs - h2 0.4.15 — MIT — https://github.com/hyperium/h2 half 2.7.1 — MIT OR Apache-2.0 — https://github.com/VoidStarKat/half-rs - hashbrown 0.16.1 — MIT OR Apache-2.0 — https://github.com/rust-lang/hashbrown hashbrown 0.17.1 — MIT OR Apache-2.0 — https://github.com/rust-lang/hashbrown - hashlink 0.12.0 — MIT OR Apache-2.0 — https://github.com/djc/hashlink heck 0.5.0 — MIT OR Apache-2.0 — https://github.com/withoutboats/heck hermit-abi 0.5.2 — MIT OR Apache-2.0 — https://github.com/hermit-os/hermit-rs - hex 0.4.3 — MIT OR Apache-2.0 — https://github.com/KokaKiwi/rust-hex hkdf 0.12.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/KDFs/ hmac 0.12.1 — MIT OR Apache-2.0 — https://github.com/RustCrypto/MACs - http 1.4.2 — MIT OR Apache-2.0 — https://github.com/hyperium/http - http-body 1.0.1 — MIT — https://github.com/hyperium/http-body - http-body-util 0.1.3 — MIT — https://github.com/hyperium/http-body - httparse 1.10.1 — MIT OR Apache-2.0 — https://github.com/seanmonstar/httparse - httpdate 1.0.3 — MIT OR Apache-2.0 — https://github.com/pyfisch/httpdate - hyper 1.10.1 — MIT — https://github.com/hyperium/hyper - hyper-util 0.1.20 — MIT — https://github.com/hyperium/hyper-util - icu_collections 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x - icu_locale_core 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x - icu_normalizer 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x - icu_normalizer_data 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x - icu_properties 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x - icu_properties_data 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x - icu_provider 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x - idna 1.1.0 — MIT OR Apache-2.0 — https://github.com/servo/rust-url/ - idna_adapter 1.2.2 — Apache-2.0 OR MIT — https://github.com/hsivonen/idna_adapter if-addrs 0.13.4 — MIT OR BSD-3-Clause — https://github.com/messense/if-addrs if-addrs 0.15.0 — MIT OR BSD-3-Clause — https://github.com/messense/if-addrs - image 0.25.10 — MIT OR Apache-2.0 — https://github.com/image-rs/image indexmap 2.14.0 — Apache-2.0 OR MIT — https://github.com/indexmap-rs/indexmap inout 0.1.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils is-terminal 0.4.17 — MIT — https://github.com/sunfishcode/is-terminal is_terminal_polyfill 1.70.2 — MIT OR Apache-2.0 — https://github.com/polyfill-rs/is_terminal_polyfill itertools 0.10.5 — MIT/Apache-2.0 — https://github.com/rust-itertools/itertools - itertools 0.13.0 — MIT OR Apache-2.0 — https://github.com/rust-itertools/itertools itoa 1.0.18 — MIT OR Apache-2.0 — https://github.com/dtolnay/itoa jni 0.21.1 — MIT/Apache-2.0 — https://github.com/jni-rs/jni-rs jni-sys 0.3.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys @@ -227,107 +125,54 @@ MANIFEST (crate version — SPDX license — source) jni-sys-macros 0.4.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys jobserver 0.1.34 — MIT OR Apache-2.0 — https://github.com/rust-lang/jobserver-rs js-sys 0.3.103 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/js-sys - khronos-egl 6.0.0 — MIT/Apache-2.0 — https://github.com/timothee-haudebourg/khronos-egl - ksni 0.3.5 — Unlicense — https://github.com/iovxw/ksni - lazy_static 1.5.0 — MIT OR Apache-2.0 — https://github.com/rust-lang-nursery/lazy-static.rs - libadwaita 0.9.1 — MIT — https://gitlab.gnome.org/World/Rust/libadwaita-rs - libadwaita-sys 0.9.1 — MIT — https://gitlab.gnome.org/World/Rust/libadwaita-rs libc 0.2.186 — MIT OR Apache-2.0 — https://github.com/rust-lang/libc - libloading 0.8.9 — ISC — https://github.com/nagisa/rust_libloading/ libm 0.2.16 — MIT — https://github.com/rust-lang/compiler-builtins - libspa 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs - libspa-sys 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs - libsqlite3-sys 0.38.1 — MIT — https://github.com/rusqlite/rusqlite linux-raw-sys 0.12.1 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/sunfishcode/linux-raw-sys - litemap 0.8.2 — Unicode-3.0 — https://github.com/unicode-org/icu4x lock_api 0.4.14 — MIT OR Apache-2.0 — https://github.com/Amanieu/parking_lot log 0.4.33 — MIT OR Apache-2.0 — https://github.com/rust-lang/log lru-slab 0.1.2 — MIT OR Apache-2.0 OR Zlib — https://github.com/Ralith/lru-slab - mac_address 1.1.8 — MIT OR Apache-2.0 — https://github.com/rep-nop/mac_address - matchers 0.2.0 — MIT — https://github.com/hawkw/matchers - matchit 0.8.4 — MIT AND BSD-3-Clause — https://github.com/ibraheemdev/matchit mdns-sd 0.20.1 — Apache-2.0 OR MIT — https://github.com/keepsimple1/mdns-sd memchr 2.8.2 — Unlicense OR MIT — https://github.com/BurntSushi/memchr - memmap2 0.9.11 — MIT OR Apache-2.0 — https://github.com/RazrFalcon/memmap2-rs - memoffset 0.9.1 — MIT — https://github.com/Gilnaa/memoffset - mime 0.3.17 — MIT OR Apache-2.0 — https://github.com/hyperium/mime - minimal-lexical 0.2.1 — MIT/Apache-2.0 — https://github.com/Alexhuszagh/minimal-lexical - miniz_oxide 0.8.9 — MIT OR Zlib OR Apache-2.0 — https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide mio 1.2.1 — MIT — https://github.com/tokio-rs/mio - moxcms 0.8.1 — BSD-3-Clause OR Apache-2.0 — https://github.com/awxkee/moxcms.git - nasm-rs 0.3.2 — MIT OR Apache-2.0 — https://github.com/medek/nasm-rs ndk 0.9.0 — MIT OR Apache-2.0 — https://github.com/rust-mobile/ndk ndk-sys 0.6.0+11769913 — MIT OR Apache-2.0 — https://github.com/rust-mobile/ndk - nix 0.29.0 — MIT — https://github.com/nix-rust/nix - nix 0.30.1 — MIT — https://github.com/nix-rust/nix - nom 7.1.3 — MIT — https://github.com/Geal/nom - nom 8.0.0 — MIT — https://github.com/rust-bakery/nom - nu-ansi-term 0.50.3 — MIT — https://github.com/nushell/nu-ansi-term - num-bigint 0.4.6 — MIT OR Apache-2.0 — https://github.com/rust-num/num-bigint - num-bigint-dig 0.8.6 — MIT/Apache-2.0 — https://github.com/dignifiedquire/num-bigint num-conv 0.2.2 — MIT OR Apache-2.0 — https://github.com/jhpratt/num-conv - num-derive 0.4.2 — MIT OR Apache-2.0 — https://github.com/rust-num/num-derive - num-integer 0.1.46 — MIT OR Apache-2.0 — https://github.com/rust-num/num-integer - num-iter 0.1.45 — MIT OR Apache-2.0 — https://github.com/rust-num/num-iter num-traits 0.2.19 — MIT OR Apache-2.0 — https://github.com/rust-num/num-traits - num_cpus 1.17.0 — MIT OR Apache-2.0 — https://github.com/seanmonstar/num_cpus num_enum 0.7.6 — BSD-3-Clause OR MIT OR Apache-2.0 — https://github.com/illicitonion/num_enum num_enum_derive 0.7.6 — BSD-3-Clause OR MIT OR Apache-2.0 — https://github.com/illicitonion/num_enum - oid-registry 0.7.1 — MIT OR Apache-2.0 — https://github.com/rusticata/oid-registry.git once_cell 1.21.4 — MIT OR Apache-2.0 — https://github.com/matklad/once_cell once_cell_polyfill 1.70.2 — MIT OR Apache-2.0 — https://github.com/polyfill-rs/once_cell_polyfill oorandom 11.1.5 — MIT — https://hg.sr.ht/~icefox/oorandom opaque-debug 0.3.1 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils - openh264 0.9.3 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs - openh264-sys2 0.9.6 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs openssl-probe 0.2.1 — MIT OR Apache-2.0 — https://github.com/rustls/openssl-probe opus 0.3.1 — MIT/Apache-2.0 — https://github.com/SpaceManiac/opus-rs - ordered-stream 0.2.0 — MIT OR Apache-2.0 — https://github.com/danieldg/ordered-stream - pango 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core - pango-sys 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core - parking 2.2.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/parking parking_lot 0.12.5 — MIT OR Apache-2.0 — https://github.com/Amanieu/parking_lot parking_lot_core 0.9.12 — MIT OR Apache-2.0 — https://github.com/Amanieu/parking_lot - paste 1.0.15 — MIT OR Apache-2.0 — https://github.com/dtolnay/paste - pastey 0.2.3 — MIT OR Apache-2.0 — https://github.com/as1100k/pastey pem 3.0.6 — MIT — https://github.com/jcreekmore/pem-rs.git - pem-rfc7468 0.7.0 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/pem-rfc7468 - percent-encoding 2.3.2 — MIT OR Apache-2.0 — https://github.com/servo/rust-url/ pin-project-lite 0.2.17 — Apache-2.0 OR MIT — https://github.com/taiki-e/pin-project-lite - piper 0.2.5 — MIT OR Apache-2.0 — https://github.com/smol-rs/piper - pipewire 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs - pipewire-sys 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs - pkcs1 0.7.5 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/pkcs1 - pkcs8 0.10.2 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/pkcs8 pkg-config 0.3.33 — MIT OR Apache-2.0 — https://github.com/rust-lang/pkg-config-rs - png 0.18.1 — MIT OR Apache-2.0 — https://github.com/image-rs/image-png - polling 3.11.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/polling poly1305 0.8.0 — Apache-2.0 OR MIT — https://github.com/RustCrypto/universal-hashes polyval 0.6.2 — Apache-2.0 OR MIT — https://github.com/RustCrypto/universal-hashes - potential_utf 0.1.5 — Unicode-3.0 — https://github.com/unicode-org/icu4x powerfmt 0.2.0 — MIT OR Apache-2.0 — https://github.com/jhpratt/powerfmt ppv-lite86 0.2.21 — MIT OR Apache-2.0 — https://github.com/cryptocorrosion/cryptocorrosion - prettyplease 0.2.37 — MIT OR Apache-2.0 — https://github.com/dtolnay/prettyplease proc-macro-crate 3.5.0 — MIT OR Apache-2.0 — https://github.com/bkchr/proc-macro-crate proc-macro2 1.0.106 — MIT OR Apache-2.0 — https://github.com/dtolnay/proc-macro2 proptest 1.11.0 — MIT OR Apache-2.0 — https://github.com/proptest-rs/proptest - pxfm 0.1.30 — BSD-3-Clause OR Apache-2.0 — https://github.com/awxkee/pxfm quick-error 1.2.3 — MIT/Apache-2.0 — http://github.com/tailhook/quick-error - quick-xml 0.39.4 — MIT — https://github.com/tafia/quick-xml quinn 0.11.11 — MIT OR Apache-2.0 — https://github.com/quinn-rs/quinn quinn-proto 0.11.15 — MIT OR Apache-2.0 — https://github.com/quinn-rs/quinn quinn-udp 0.5.14 — MIT OR Apache-2.0 — https://github.com/quinn-rs/quinn quote 1.0.46 — MIT OR Apache-2.0 — https://github.com/dtolnay/quote r-efi 5.3.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi r-efi 6.0.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi - rand 0.8.6 — MIT OR Apache-2.0 — https://github.com/rust-random/rand rand 0.9.4 — MIT OR Apache-2.0 — https://github.com/rust-random/rand - rand_chacha 0.3.1 — MIT OR Apache-2.0 — https://github.com/rust-random/rand rand_chacha 0.9.0 — MIT OR Apache-2.0 — https://github.com/rust-random/rand rand_core 0.6.4 — MIT OR Apache-2.0 — https://github.com/rust-random/rand rand_core 0.9.5 — MIT OR Apache-2.0 — https://github.com/rust-random/rand rand_xorshift 0.4.0 — MIT OR Apache-2.0 — https://github.com/rust-random/rngs raw-window-handle 0.6.2 — MIT OR Apache-2.0 OR Zlib — https://github.com/rust-windowing/raw-window-handle + rayon 1.12.0 — MIT OR Apache-2.0 — https://github.com/rayon-rs/rayon + rayon-core 1.13.0 — MIT OR Apache-2.0 — https://github.com/rayon-rs/rayon rcgen 0.13.2 — MIT OR Apache-2.0 — https://github.com/rustls/rcgen readme-rustdocifier 0.1.1 — MIT — https://github.com/malaire/readme-rustdocifier redox_syscall 0.5.18 — MIT — https://gitlab.redox-os.org/redox-os/syscall @@ -335,19 +180,9 @@ MANIFEST (crate version — SPDX license — source) regex 1.12.4 — MIT OR Apache-2.0 — https://github.com/rust-lang/regex regex-automata 0.4.14 — MIT OR Apache-2.0 — https://github.com/rust-lang/regex regex-syntax 0.8.11 — MIT OR Apache-2.0 — https://github.com/rust-lang/regex - reis 0.6.1 — MIT — https://github.com/ids1024/reis - relm4 0.11.0 — Apache-2.0 OR MIT — https://github.com/Relm4/Relm4 - relm4-css 0.11.0 — Apache-2.0 OR MIT — https://github.com/Relm4/Relm4 - relm4-macros 0.11.0 — Apache-2.0 OR MIT — https://github.com/Relm4/Relm4 ring 0.17.14 — Apache-2.0 AND ISC — https://github.com/briansmith/ring - roxmltree 0.21.1 — MIT OR Apache-2.0 — https://github.com/RazrFalcon/roxmltree - rpkg-config 0.1.2 — Zlib OR MIT OR Apache-2.0 — https://github.com/maia-s/rpkg-config-rs - rsa 0.9.10 — MIT OR Apache-2.0 — https://github.com/RustCrypto/RSA - rsqlite-vfs 0.1.1 — MIT - rusqlite 0.40.1 — MIT — https://github.com/rusqlite/rusqlite rustc-hash 2.1.2 — Apache-2.0 OR MIT — https://github.com/rust-lang/rustc-hash rustc_version 0.4.1 — MIT OR Apache-2.0 — https://github.com/djc/rustc-version-rs - rusticata-macros 4.1.0 — MIT/Apache-2.0 — https://github.com/rusticata/rusticata-macros.git rustix 1.1.4 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/rustix rustls 0.23.41 — Apache-2.0 OR ISC OR MIT — https://github.com/rustls/rustls rustls-native-certs 0.8.4 — Apache-2.0 OR ISC OR MIT — https://github.com/rustls/rustls-native-certs @@ -357,21 +192,9 @@ MANIFEST (crate version — SPDX license — source) rustls-webpki 0.103.13 — ISC — https://github.com/rustls/webpki rustversion 1.0.22 — MIT OR Apache-2.0 — https://github.com/dtolnay/rustversion rusty-fork 0.3.1 — MIT/Apache-2.0 — https://github.com/altsysrq/rusty-fork - rusty_enet 0.4.0 — MIT — https://github.com/jabuwu/rusty_enet - ryu 1.0.23 — Apache-2.0 OR BSL-1.0 — https://github.com/dtolnay/ryu - safe_arch 0.7.4 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/safe_arch same-file 1.0.6 — Unlicense/MIT — https://github.com/BurntSushi/same-file schannel 0.1.29 — MIT — https://github.com/steffengy/schannel-rs scopeguard 1.2.0 — MIT OR Apache-2.0 — https://github.com/bluss/scopeguard - sdl3 0.18.4 — MIT — https://github.com/vhspace/sdl3-rs - sdl3-image-src 3.4.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-image-sys 0.6.4+SDL-image-3.4.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-mixer-src 3.2.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-mixer-sys 0.6.3+SDL-mixer-3.2.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-src 3.4.10 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-sys 0.6.6+SDL-3.4.10 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-ttf-src 3.2.2 — Zlib — https://github.com/maia-s/sdl3-sys-rs - sdl3-ttf-sys 0.6.1+SDL-ttf-3.2.2 — Zlib — https://codeberg.org/maia/sdl3-sys-rs security-framework 3.7.0 — MIT OR Apache-2.0 — https://github.com/kornelski/rust-security-framework security-framework-sys 2.17.0 — MIT OR Apache-2.0 — https://github.com/kornelski/rust-security-framework semver 1.0.28 — MIT OR Apache-2.0 — https://github.com/dtolnay/semver @@ -379,149 +202,64 @@ MANIFEST (crate version — SPDX license — source) serde_core 1.0.228 — MIT OR Apache-2.0 — https://github.com/serde-rs/serde serde_derive 1.0.228 — MIT OR Apache-2.0 — https://github.com/serde-rs/serde serde_json 1.0.150 — MIT OR Apache-2.0 — https://github.com/serde-rs/json - serde_path_to_error 0.1.20 — MIT OR Apache-2.0 — https://github.com/dtolnay/path-to-error - serde_repr 0.1.20 — MIT OR Apache-2.0 — https://github.com/dtolnay/serde-repr - serde_spanned 0.6.9 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml serde_spanned 1.1.1 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml - serde_urlencoded 0.7.1 — MIT/Apache-2.0 — https://github.com/nox/serde_urlencoded sha2 0.10.9 — MIT OR Apache-2.0 — https://github.com/RustCrypto/hashes - sharded-slab 0.1.7 — MIT — https://github.com/hawkw/sharded-slab - shlex 1.3.0 — MIT OR Apache-2.0 — https://github.com/comex/rust-shlex shlex 2.0.1 — MIT OR Apache-2.0 — https://github.com/comex/rust-shlex signal-hook-registry 1.4.8 — MIT OR Apache-2.0 — https://github.com/vorner/signal-hook - signature 2.2.0 — Apache-2.0 OR MIT — https://github.com/RustCrypto/traits/tree/master/signature - simd-adler32 0.3.9 — MIT — https://github.com/mcountryman/simd-adler32 siphasher 1.0.3 — MIT/Apache-2.0 — https://github.com/jedisct1/rust-siphash - skia-bindings 0.87.0 — MIT — https://github.com/rust-skia/rust-skia - skia-safe 0.87.0 — MIT — https://github.com/rust-skia/rust-skia slab 0.4.12 — MIT — https://github.com/tokio-rs/slab smallvec 1.15.2 — MIT OR Apache-2.0 — https://github.com/servo/rust-smallvec socket-pktinfo 0.4.0 — MIT — https://github.com/pixsper/socket-pktinfo socket2 0.6.4 — MIT OR Apache-2.0 — https://github.com/rust-lang/socket2 spake2 0.4.0 — MIT OR Apache-2.0 — https://github.com/RustCrypto/PAKEs/tree/master/spake2 spin 0.9.8 — MIT — https://github.com/mvdnes/spin-rs.git - spki 0.7.3 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/spki - sqlite-wasm-rs 0.5.5 — MIT — https://github.com/Spxg/sqlite-wasm-rs - stable_deref_trait 1.2.1 — MIT OR Apache-2.0 — https://github.com/storyyeller/stable_deref_trait strsim 0.11.1 — MIT — https://github.com/rapidfuzz/strsim-rs subtle 2.6.1 — BSD-3-Clause — https://github.com/dalek-cryptography/subtle syn 2.0.118 — MIT OR Apache-2.0 — https://github.com/dtolnay/syn - sync_wrapper 1.0.2 — Apache-2.0 — https://github.com/Actyx/sync_wrapper - synstructure 0.13.2 — MIT — https://github.com/mystor/synstructure - system-deps 7.0.8 — MIT OR Apache-2.0 — https://github.com/gdesmott/system-deps - tar 0.4.46 — MIT OR Apache-2.0 — https://github.com/composefs/tar-rs - target-lexicon 0.13.5 — Apache-2.0 WITH LLVM-exception — https://github.com/bytecodealliance/target-lexicon tempfile 3.27.0 — MIT OR Apache-2.0 — https://github.com/Stebalien/tempfile - test_reactor 0.0.0 — UNKNOWN thiserror 1.0.69 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror thiserror 2.0.18 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror thiserror-impl 1.0.69 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror thiserror-impl 2.0.18 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror - thread_local 1.1.9 — MIT OR Apache-2.0 — https://github.com/Amanieu/thread_local-rs time 0.3.51 — MIT OR Apache-2.0 — https://github.com/time-rs/time time-core 0.1.9 — MIT OR Apache-2.0 — https://github.com/time-rs/time time-macros 0.2.30 — MIT OR Apache-2.0 — https://github.com/time-rs/time - tinystr 0.8.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x tinytemplate 1.2.1 — Apache-2.0 OR MIT — https://github.com/bheisler/TinyTemplate tinyvec 1.11.0 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/tinyvec tinyvec_macros 0.1.1 — MIT OR Apache-2.0 OR Zlib — https://github.com/Soveu/tinyvec_macros tokio 1.52.3 — MIT — https://github.com/tokio-rs/tokio tokio-macros 2.7.0 — MIT — https://github.com/tokio-rs/tokio - tokio-rustls 0.26.4 — MIT OR Apache-2.0 — https://github.com/rustls/tokio-rustls - tokio-util 0.7.18 — MIT — https://github.com/tokio-rs/tokio - toml 0.8.23 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml toml 0.9.12+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml - toml 1.1.2+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml - toml_datetime 0.6.11 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml toml_datetime 0.7.5+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml toml_datetime 1.1.1+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml - toml_edit 0.22.27 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml toml_edit 0.25.12+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml toml_parser 1.1.2+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml - toml_write 0.1.2 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml toml_writer 1.1.1+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml - tower 0.5.3 — MIT — https://github.com/tower-rs/tower - tower-layer 0.3.3 — MIT — https://github.com/tower-rs/tower - tower-service 0.3.3 — MIT — https://github.com/tower-rs/tower tracing 0.1.44 — MIT — https://github.com/tokio-rs/tracing tracing-attributes 0.1.31 — MIT — https://github.com/tokio-rs/tracing tracing-core 0.1.36 — MIT — https://github.com/tokio-rs/tracing - tracing-log 0.2.0 — MIT — https://github.com/tokio-rs/tracing - tracing-subscriber 0.3.23 — MIT — https://github.com/tokio-rs/tracing typenum 1.20.1 — MIT OR Apache-2.0 — https://github.com/paholg/typenum - uds_windows 1.2.1 — MIT — https://github.com/haraldh/rust_uds_windows + uac-host 0.1.0 — MIT OR Apache-2.0 — https://github.com/unom-io/usbfs-iso unarray 0.1.4 — MIT OR Apache-2.0 — https://github.com/cameron1024/unarray unicode-ident 1.0.24 — (MIT OR Apache-2.0) AND Unicode-3.0 — https://github.com/dtolnay/unicode-ident - unicode-segmentation 1.13.3 — MIT OR Apache-2.0 — https://github.com/unicode-rs/unicode-segmentation - unicode-width 0.2.2 — MIT OR Apache-2.0 — https://github.com/unicode-rs/unicode-width universal-hash 0.5.1 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits untrusted 0.9.0 — ISC — https://github.com/briansmith/untrusted - ureq 2.12.1 — MIT OR Apache-2.0 — https://github.com/algesten/ureq - url 2.5.8 — MIT OR Apache-2.0 — https://github.com/servo/rust-url - utf8_iter 1.0.4 — Apache-2.0 OR MIT — https://github.com/hsivonen/utf8_iter + usbfs-iso 0.1.0 — MIT OR Apache-2.0 — https://github.com/unom-io/usbfs-iso utf8parse 0.2.2 — Apache-2.0 OR MIT — https://github.com/alacritty/vte - utoipa 5.5.0 — MIT OR Apache-2.0 — https://github.com/juhaku/utoipa - utoipa-axum 0.2.0 — MIT OR Apache-2.0 — https://github.com/juhaku/utoipa - utoipa-gen 5.5.0 — MIT OR Apache-2.0 — https://github.com/juhaku/utoipa - utoipa-scalar 0.3.0 — MIT OR Apache-2.0 — https://github.com/juhaku/utoipa - uuid 1.23.4 — Apache-2.0 OR MIT — https://github.com/uuid-rs/uuid valuable 0.1.1 — MIT — https://github.com/tokio-rs/valuable - vcpkg 0.2.15 — MIT/Apache-2.0 — https://github.com/mcgoo/vcpkg-rs - version-compare 0.2.1 — MIT — https://gitlab.com/timvisee/version-compare version_check 0.9.5 — MIT/Apache-2.0 — https://github.com/SergioBenitez/version_check wait-timeout 0.2.1 — MIT/Apache-2.0 — https://github.com/alexcrichton/wait-timeout walkdir 2.5.0 — Unlicense/MIT — https://github.com/BurntSushi/walkdir - wasapi 0.23.0 — MIT — https://github.com/HEnquist/wasapi-rs wasi 0.11.1+wasi-snapshot-preview1 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/wasi wasip2 1.0.4+wasi-0.2.12 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/wasi-rs wasm-bindgen 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen wasm-bindgen-macro 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/macro wasm-bindgen-macro-support 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/macro-support wasm-bindgen-shared 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/shared - wayland-backend 0.3.15 — MIT — https://github.com/smithay/wayland-rs - wayland-client 0.31.14 — MIT — https://github.com/smithay/wayland-rs - wayland-protocols 0.32.13 — MIT — https://github.com/smithay/wayland-rs - wayland-protocols-misc 0.3.12 — MIT — https://github.com/smithay/wayland-rs - wayland-protocols-wlr 0.3.12 — MIT — https://github.com/smithay/wayland-rs - wayland-scanner 0.31.10 — MIT — https://github.com/smithay/wayland-rs - wayland-sys 0.31.11 — MIT — https://github.com/smithay/wayland-rs web-time 1.1.0 — MIT OR Apache-2.0 — https://github.com/daxpedda/web-time webpki-root-certs 1.0.8 — CDLA-Permissive-2.0 — https://github.com/rustls/webpki-roots - webpki-roots 0.26.11 — CDLA-Permissive-2.0 — https://github.com/rustls/webpki-roots - webpki-roots 1.0.8 — CDLA-Permissive-2.0 — https://github.com/rustls/webpki-roots - weezl 0.1.12 — MIT OR Apache-2.0 — https://github.com/image-rs/weezl - wide 0.7.33 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/wide - widestring 1.2.1 — MIT OR Apache-2.0 — https://github.com/VoidStarKat/widestring-rs - winapi 0.3.9 — MIT/Apache-2.0 — https://github.com/retep998/winapi-rs - winapi-i686-pc-windows-gnu 0.4.0 — MIT/Apache-2.0 — https://github.com/retep998/winapi-rs winapi-util 0.1.11 — Unlicense OR MIT — https://github.com/BurntSushi/winapi-util - winapi-x86_64-pc-windows-gnu 0.4.0 — MIT/Apache-2.0 — https://github.com/retep998/winapi-rs - windows 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-canvas 0.0.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-collections 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-collections 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-composition 0.0.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-core 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-core 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-future 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-future 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-implement 0.60.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-implement 0.60.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-interface 0.59.3 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-interface 0.59.3 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows-link 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-link 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-numerics 0.3.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-numerics 0.3.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-reactor 0.0.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-reactor-setup 0.0.0 — MIT OR Apache-2.0 - windows-reference 0.1.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-result 0.4.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-result 0.4.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-service 0.7.0 — MIT OR Apache-2.0 — https://github.com/mullvad/windows-service-rs - windows-strings 0.5.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-strings 0.5.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows-sys 0.45.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows-sys 0.52.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows-sys 0.59.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs @@ -530,10 +268,6 @@ MANIFEST (crate version — SPDX license — source) windows-targets 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows-targets 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows-targets 0.53.5 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-threading 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-threading 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-time 0.1.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-window 0.0.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows_aarch64_gnullvm 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows_aarch64_gnullvm 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows_aarch64_gnullvm 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs @@ -559,62 +293,26 @@ MANIFEST (crate version — SPDX license — source) windows_x86_64_msvc 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs winnow 0.7.15 — MIT — https://github.com/winnow-rs/winnow winnow 1.0.3 — MIT — https://github.com/winnow-rs/winnow - winreg 0.56.0 — MIT — https://github.com/gentoo90/winreg-rs - winresource 0.1.31 — MIT — https://github.com/BenjaminRi/winresource wit-bindgen 0.57.1 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/wit-bindgen - writeable 0.6.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x - x11rb 0.13.2 — MIT OR Apache-2.0 — https://github.com/psychon/x11rb - x11rb-protocol 0.13.2 — MIT OR Apache-2.0 — https://github.com/psychon/x11rb - x509-parser 0.16.0 — MIT OR Apache-2.0 — https://github.com/rusticata/x509-parser.git - xattr 1.6.1 — MIT OR Apache-2.0 — https://github.com/Stebalien/xattr - xkbcommon 0.8.0 — MIT — https://github.com/rust-x-bindings/xkbcommon-rs - xkeysym 0.2.1 — MIT OR Apache-2.0 OR Zlib — https://github.com/notgull/xkeysym yasna 0.5.2 — MIT OR Apache-2.0 — https://github.com/qnighy/yasna.rs - yoke 0.8.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x - yoke-derive 0.8.2 — Unicode-3.0 — https://github.com/unicode-org/icu4x - zbus 5.16.0 — MIT — https://github.com/z-galaxy/zbus/ - zbus_macros 5.16.0 — MIT — https://github.com/z-galaxy/zbus/ - zbus_names 4.3.2 — MIT — https://github.com/z-galaxy/zbus/ zerocopy 0.8.52 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy zerocopy-derive 0.8.52 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy - zerofrom 0.1.8 — Unicode-3.0 — https://github.com/unicode-org/icu4x - zerofrom-derive 0.1.7 — Unicode-3.0 — https://github.com/unicode-org/icu4x zeroize 1.9.0 — Apache-2.0 OR MIT — https://github.com/RustCrypto/utils - zerotrie 0.2.4 — Unicode-3.0 — https://github.com/unicode-org/icu4x - zerovec 0.11.6 — Unicode-3.0 — https://github.com/unicode-org/icu4x - zerovec-derive 0.11.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x zmij 1.0.21 — MIT — https://github.com/dtolnay/zmij - zune-core 0.5.1 — MIT OR Apache-2.0 OR Zlib — https://github.com/etemesi254/zune-image - zune-jpeg 0.5.15 — MIT OR Apache-2.0 OR Zlib — https://github.com/etemesi254/zune-image/tree/dev/crates/zune-jpeg - zvariant 5.12.0 — MIT — https://github.com/z-galaxy/zbus/ - zvariant_derive 5.12.0 — MIT — https://github.com/z-galaxy/zbus/ - zvariant_utils 3.4.0 — MIT — https://github.com/z-galaxy/zbus/ ---------------------------------------------------------------------------- Crates whose package did not embed a license file (SPDX + source only) ---------------------------------------------------------------------------- anes 0.1.6 — MIT OR Apache-2.0 — https://github.com/zrzka/anes-rs - asn1-rs-impl 0.2.0 — MIT/Apache-2.0 — https://github.com/rusticata/asn1-rs.git - cookie-factory 0.3.3 — MIT — https://github.com/rust-bakery/cookie-factory - ffmpeg-sys-next 8.1.0 — WTFPL — https://github.com/zmwangx/rust-ffmpeg-sys jni-sys-macros 0.4.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys ndk 0.9.0 — MIT OR Apache-2.0 — https://github.com/rust-mobile/ndk ndk-sys 0.6.0+11769913 — MIT OR Apache-2.0 — https://github.com/rust-mobile/ndk - openh264 0.9.3 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs - openh264-sys2 0.9.6 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs r-efi 5.3.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi r-efi 6.0.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi - rsqlite-vfs 0.1.1 — MIT rustls-platform-verifier-android 0.1.1 — MIT OR Apache-2.0 — https://github.com/rustls/rustls-platform-verifier - sdl3-image-src 3.4.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-mixer-src 3.2.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-ttf-src 3.2.2 — Zlib — https://github.com/maia-s/sdl3-sys-rs - skia-bindings 0.87.0 — MIT — https://github.com/rust-skia/rust-skia - skia-safe 0.87.0 — MIT — https://github.com/rust-skia/rust-skia - test_reactor 0.0.0 — UNKNOWN + uac-host 0.1.0 — MIT OR Apache-2.0 — https://github.com/unom-io/usbfs-iso + usbfs-iso 0.1.0 — MIT OR Apache-2.0 — https://github.com/unom-io/usbfs-iso valuable 0.1.1 — MIT — https://github.com/tokio-rs/valuable - winapi-i686-pc-windows-gnu 0.4.0 — MIT/Apache-2.0 — https://github.com/retep998/winapi-rs - winapi-x86_64-pc-windows-gnu 0.4.0 — MIT/Apache-2.0 — https://github.com/retep998/winapi-rs yasna 0.5.2 — MIT OR Apache-2.0 — https://github.com/qnighy/yasna.rs ============================================================================ @@ -622,258 +320,7 @@ FULL LICENSE TEXTS (deduplicated) ============================================================================ ---------------------------------------------------------------------------- -The following license (LICENSE-0BSD) applies to: adler2 2.0.1 ----------------------------------------------------------------------------- -Copyright (C) Jonas Schievink - -Permission to use, copy, modify, and/or distribute this software for -any purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN -AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT -OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: adler2 2.0.1, proc-macro-crate 3.5.0 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - https://www.apache.org/licenses/LICENSE-2.0 - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: adler2 2.0.1, anyhow 1.0.103, async-channel 2.5.0, async-executor 1.14.0, async-io 2.6.0, async-lock 3.4.2, async-process 2.5.0, async-recursion 1.1.1, async-signal 0.2.14, async-task 4.7.1, async-trait 0.1.89, atomic-waker 1.1.2, blocking 1.6.2, concurrent-queue 2.5.0, curve25519-dalek-derive 0.1.1, displaydoc 0.2.6, endi 1.1.1, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, flume 0.12.0, fs-err 3.3.0, futures-lite 2.6.1, hermit-abi 0.5.2, is-terminal 0.4.17, itoa 1.0.18, khronos-egl 6.0.0, linux-raw-sys 0.12.1, minimal-lexical 0.2.1, num_enum 0.7.6, num_enum_derive 0.7.6, once_cell 1.21.4, ordered-stream 0.2.0, parking 2.2.1, paste 1.0.15, pastey 0.2.3, pin-project-lite 0.2.17, piper 0.2.5, polling 3.11.0, prettyplease 0.2.37, proc-macro-crate 3.5.0, proc-macro2 1.0.106, quote 1.0.46, reis 0.6.1, relm4 0.11.0, relm4-css 0.11.0, relm4-macros 0.11.0, rsa 0.9.10, rustc-hash 2.1.2, rustix 1.1.4, rustversion 1.0.22, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, serde_path_to_error 0.1.20, serde_repr 0.1.20, syn 2.0.118, system-deps 7.0.8, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1, zmij 1.0.21, zvariant_utils 3.4.0 ----------------------------------------------------------------------------- -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 following license (LICENSE-APACHE) applies to: aead 0.5.2, aes 0.8.4, aes-gcm 0.10.3, base64ct 1.8.3, block-buffer 0.10.4, block-padding 0.3.3, cbc 0.1.2, chacha20 0.9.1, chacha20poly1305 0.10.1, cipher 0.4.4, const-oid 0.9.6, cpufeatures 0.2.17, crypto-common 0.1.7, ctr 0.9.2, der 0.7.10, digest 0.10.7, ghash 0.5.1, hkdf 0.12.4, hmac 0.12.1, inout 0.1.4, opaque-debug 0.3.1, pem-rfc7468 0.7.0, pkcs1 0.7.5, pkcs8 0.10.2, poly1305 0.8.0, polyval 0.6.2, sha2 0.10.9, signature 2.2.0, spake2 0.4.0, spki 0.7.3, universal-hash 0.5.1 +The following license (LICENSE-APACHE) applies to: aead 0.5.2, aes 0.8.4, aes-gcm 0.10.3, block-buffer 0.10.4, block-padding 0.3.3, chacha20 0.9.1, chacha20poly1305 0.10.1, cipher 0.4.4, const-oid 0.9.6, cpufeatures 0.2.17, crypto-common 0.1.7, ctr 0.9.2, digest 0.10.7, ghash 0.5.1, hkdf 0.12.4, hmac 0.12.1, inout 0.1.4, opaque-debug 0.3.1, poly1305 0.8.0, polyval 0.6.2, sha2 0.10.9, spake2 0.4.0, universal-hash 0.5.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -1178,7 +625,7 @@ You may use this code under the terms of either license. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: aho-corasick 1.1.4, byteorder-lite 0.1.0, memchr 2.8.2, walkdir 2.5.0 +The following license (LICENSE-MIT) applies to: aho-corasick 1.1.4, memchr 2.8.2, walkdir 2.5.0 ---------------------------------------------------------------------------- The MIT License (MIT) @@ -1204,7 +651,7 @@ THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (UNLICENSE) applies to: aho-corasick 1.1.4, byteorder-lite 0.1.0, ksni 0.3.5, memchr 2.8.2, same-file 1.0.6, walkdir 2.5.0, winapi-util 0.1.11 +The following license (UNLICENSE) applies to: aho-corasick 1.1.4, memchr 2.8.2, same-file 1.0.6, walkdir 2.5.0, winapi-util 0.1.11 ---------------------------------------------------------------------------- This is free and unencumbered software released into the public domain. @@ -1693,7 +1140,7 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: annotate-snippets 0.11.5, anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, crc32fast 1.5.0, env_filter 0.1.4, fallible-iterator 0.3.0, fallible-streaming-iterator 0.1.9, hex 0.4.3, is_terminal_polyfill 1.70.2, jni-sys 0.3.1, jni-sys 0.4.1, once_cell_polyfill 1.70.2, quick-error 1.2.3, serde_spanned 0.6.9, serde_spanned 1.1.1, toml 0.8.23, toml 0.9.12+spec-1.1.0, toml 1.1.2+spec-1.1.0, toml_datetime 0.6.11, toml_datetime 0.7.5+spec-1.1.0, toml_datetime 1.1.1+spec-1.1.0, toml_edit 0.22.27, toml_edit 0.25.12+spec-1.1.0, toml_parser 1.1.2+spec-1.1.0, toml_write 0.1.2, toml_writer 1.1.1+spec-1.1.0, winapi 0.3.9 +The following license (LICENSE-APACHE) applies to: anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, env_filter 0.1.4, is_terminal_polyfill 1.70.2, jni-sys 0.3.1, jni-sys 0.4.1, once_cell_polyfill 1.70.2, quick-error 1.2.3, serde_spanned 1.1.1, toml 0.9.12+spec-1.1.0, toml_datetime 0.7.5+spec-1.1.0, toml_datetime 1.1.1+spec-1.1.0, toml_edit 0.25.12+spec-1.1.0, toml_parser 1.1.2+spec-1.1.0, toml_writer 1.1.1+spec-1.1.0 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -1899,7 +1346,7 @@ Apache License ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: annotate-snippets 0.11.5, anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, env_filter 0.1.4, is_terminal_polyfill 1.70.2, once_cell_polyfill 1.70.2, serde_spanned 0.6.9, serde_spanned 1.1.1, toml 0.8.23, toml 0.9.12+spec-1.1.0, toml 1.1.2+spec-1.1.0, toml_datetime 0.6.11, toml_datetime 0.7.5+spec-1.1.0, toml_datetime 1.1.1+spec-1.1.0, toml_edit 0.22.27, toml_edit 0.25.12+spec-1.1.0, toml_parser 1.1.2+spec-1.1.0, toml_write 0.1.2, toml_writer 1.1.1+spec-1.1.0 +The following license (LICENSE-MIT) applies to: anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, env_filter 0.1.4, is_terminal_polyfill 1.70.2, once_cell_polyfill 1.70.2, serde_spanned 1.1.1, toml 0.9.12+spec-1.1.0, toml_datetime 0.7.5+spec-1.1.0, toml_datetime 1.1.1+spec-1.1.0, toml_edit 0.25.12+spec-1.1.0, toml_parser 1.1.2+spec-1.1.0, toml_writer 1.1.1+spec-1.1.0 ---------------------------------------------------------------------------- Copyright (c) Individual contributors @@ -1923,316 +1370,27 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: anyhow 1.0.103, async-trait 0.1.89, fastbloom 0.14.1, itoa 1.0.18, libc 0.2.186, num_enum 0.7.6, num_enum_derive 0.7.6, paste 1.0.15, pastey 0.2.3, prettyplease 0.2.37, proc-macro2 1.0.106, quote 1.0.46, relm4 0.11.0, relm4-css 0.11.0, relm4-macros 0.11.0, rustc-hash 2.1.2, rustversion 1.0.22, ryu 1.0.23, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, serde_path_to_error 0.1.20, serde_repr 0.1.20, serde_urlencoded 0.7.1, syn 2.0.118, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, utf8parse 0.2.2 +The following license (LICENSE.md) applies to: audiopus_sys 0.2.2 ---------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +ISC License -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Copyright (c) 2019, Lakelezz -1. Definitions. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: ash 0.38.0+1.3.281 ----------------------------------------------------------------------------- -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of this License; and -You must cause any modified files to carry prominent notices stating that You changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - -You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -Copyright 2016 Maik Klein - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: ash 0.38.0+1.3.281 ----------------------------------------------------------------------------- -Copyright (c) 2016 ASH - -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 following license (LICENSE) applies to: ashpd 0.13.12 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2020 Bilal Elmoussaoui - -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 following license (LICENSE-APACHE) applies to: asn1-rs 0.6.2, asn1-rs-derive 0.5.1, async-channel 2.5.0, async-executor 1.14.0, async-io 2.6.0, async-lock 3.4.2, async-process 2.5.0, async-recursion 1.1.1, async-signal 0.2.14, async-task 4.7.1, atomic-waker 1.1.2, autocfg 1.5.1, base64 0.22.1, bitflags 2.13.0, blocking 1.6.2, bumpalo 3.20.3, cast 0.3.0, cc 1.2.65, cexpr 0.6.0, cfg-if 1.0.4, cmake 0.1.58, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, der-parser 9.0.0, displaydoc 0.2.6, either 1.16.0, equivalent 1.0.2, errno 0.3.14, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, filetime 0.2.29, find-msvc-tools 0.1.9, fixedbitset 0.5.7, flate2 1.1.9, fnv 1.0.7, form_urlencoded 1.2.2, fs-err 3.3.0, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glob 0.3.3, hashbrown 0.16.1, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, httparse 1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.0, itertools 0.10.5, itertools 0.13.0, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, khronos-egl 6.0.0, lazy_static 1.5.0, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, mime 0.3.17, nasm-rs 0.3.2, num-bigint 0.4.6, num-bigint-dig 0.8.6, num-derive 0.4.2, num-integer 0.1.46, num-iter 0.1.45, num-traits 0.2.19, num_cpus 1.17.0, oid-registry 0.7.1, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, ordered-stream 0.2.0, parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5, pkg-config 0.3.33, png 0.18.1, polling 3.11.0, proptest 1.11.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, roxmltree 0.21.1, rsa 0.9.10, rustc_version 0.4.1, rusticata-macros 4.1.0, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, stable_deref_trait 1.2.1, system-deps 7.0.8, tar 0.4.46, tempfile 3.27.0, thread_local 1.1.9, tinytemplate 1.2.1, unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, uuid 1.23.4, vcpkg 0.2.15, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, weezl 0.1.12, wit-bindgen 0.57.1, x509-parser 0.16.0, xattr 1.6.1 +The following license (LICENSE-APACHE) applies to: autocfg 1.5.1, base64 0.22.1, bitflags 2.13.0, bumpalo 3.20.3, cast 0.3.0, cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, either 1.16.0, equivalent 1.0.2, errno 0.3.14, fastrand 2.4.1, find-msvc-tools 0.1.9, fixedbitset 0.5.7, fnv 1.0.7, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, indexmap 2.14.0, itertools 0.10.5, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, parking_lot 0.12.5, parking_lot_core 0.9.12, pkg-config 0.3.33, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, rustc_version 0.4.1, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, tempfile 3.27.0, tinytemplate 1.2.1, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, wit-bindgen 0.57.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -2437,327 +1595,6 @@ See the License for the specific language governing permissions and limitations under the License. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: asn1-rs 0.6.2, asn1-rs-derive 0.5.1, der-parser 9.0.0, oid-registry 0.7.1, rusticata-macros 4.1.0, x509-parser 0.16.0 ----------------------------------------------------------------------------- -Copyright (c) 2017 Pierre Chifflier - -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 following license (LICENSE-APACHE) applies to: async-broadcast 0.7.2 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - Copyright 2020 Yoshua Wuyts - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: async-broadcast 0.7.2 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2020 Yoshua Wuyts - -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 following license (LICENSE-THIRD-PARTY) applies to: atomic-waker 1.1.2, futures-lite 2.6.1 ----------------------------------------------------------------------------- -=============================================================================== - -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -=============================================================================== - -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors - -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 following license (LICENSE.md) applies to: audiopus_sys 0.2.2 ----------------------------------------------------------------------------- -ISC License - -Copyright (c) 2019, Lakelezz - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - ---------------------------------------------------------------------------- The following license (LICENSE-MIT) applies to: autocfg 1.5.1 ---------------------------------------------------------------------------- @@ -2788,92 +1625,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: axum 0.8.9 ----------------------------------------------------------------------------- -Copyright (c) 2019 axum 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 following license (LICENSE) applies to: axum-core 0.5.6 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2019–2025 axum 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 following license (LICENSE) applies to: axum-server 0.8.0 ----------------------------------------------------------------------------- -Copyright 2021 Axum Server 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 following license (LICENSE-MIT) applies to: base64 0.22.1 ---------------------------------------------------------------------------- @@ -2900,37 +1651,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: base64ct 1.8.3 ----------------------------------------------------------------------------- -Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com) -Copyright (c) 2021-2025 The RustCrypto Project Developers - -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 following license (bazzite.txt) applies to: Bazzite logo (vendored, assets/os-icons) ---------------------------------------------------------------------------- @@ -2951,41 +1671,7 @@ purposes only; their use does not imply endorsement. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: bindgen 0.72.1 ----------------------------------------------------------------------------- -BSD 3-Clause License - -Copyright (c) 2013, Jyun-Yan You -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: bit-set 0.8.0, bit-vec 0.8.0, cfg-expr 0.20.8, downcast-rs 1.2.1, hashlink 0.12.0, minimal-lexical 0.2.1 +The following license (LICENSE-APACHE) applies to: bit-set 0.8.0, bit-vec 0.8.0 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -3221,7 +1907,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: bitflags 2.13.0, glob 0.3.3, log 0.4.33, num-bigint 0.4.6, num-bigint-dig 0.8.6, num-derive 0.4.2, num-integer 0.1.46, num-iter 0.1.45, num-traits 0.2.19, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11 +The following license (LICENSE-MIT) applies to: bitflags 2.13.0, log 0.4.33, num-traits 0.2.19, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11 ---------------------------------------------------------------------------- Copyright (c) 2014 The Rust Project Developers @@ -3310,102 +1996,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: bytemuck 1.25.0, bytemuck_derive 1.10.2, safe_arch 0.7.4 ----------------------------------------------------------------------------- -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - - "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: bytemuck 1.25.0, bytemuck_derive 1.10.2 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2019 Daniel "Lokathor" Gee. - -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 (including the next paragraph) 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 following license (LICENSE-ZLIB) applies to: bytemuck 1.25.0, bytemuck_derive 1.10.2, tinyvec 1.11.0 ----------------------------------------------------------------------------- -Copyright (c) 2019 Daniel "Lokathor" Gee. - -This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. - - ---------------------------------------------------------------------------- The following license (LICENSE) applies to: bytes 1.12.0 ---------------------------------------------------------------------------- @@ -3436,47 +2026,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (COPYRIGHT) applies to: cairo-rs 0.22.0, gdk-pixbuf 0.22.0, gdk4 0.11.2, gio 0.22.6, glib 0.22.7, glib-build-tools 0.22.8, glib-macros 0.22.6, graphene-rs 0.22.0, gsk4 0.11.1, gtk4 0.11.3, gtk4-macros 0.11.0, pango 0.22.6 ----------------------------------------------------------------------------- -The gtk-rs Project is licensed under the MIT license, see the LICENSE file or -. - -Copyrights in the gtk-rs Project project are retained by their contributors. -No copyright assignment is required to contribute to the gtk-rs Project -project. - -For full authorship information, see the version control history. - -This project provides interoperability with various GNOME libraries but -doesn't distribute any parts of them. Distributing compiled libraries and -executables that link to those libraries may be subject to terms of the GNU -LGPL or other licenses. For more information check the license of each GNOME -library. - - ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: cairo-rs 0.22.0, cairo-sys-rs 0.22.0, gdk-pixbuf 0.22.0, gdk-pixbuf-sys 0.22.0, gdk4 0.11.2, gdk4-sys 0.11.2, gio 0.22.6, gio-sys 0.22.0, glib 0.22.7, glib-build-tools 0.22.8, glib-macros 0.22.6, glib-sys 0.22.6, gobject-sys 0.22.6, graphene-rs 0.22.0, graphene-sys 0.22.0, gsk4 0.11.1, gsk4-sys 0.11.1, gtk4 0.11.3, gtk4-macros 0.11.0, gtk4-sys 0.11.3, libadwaita 0.9.1, libadwaita-sys 0.9.1, pango 0.22.6, pango-sys 0.22.0 ----------------------------------------------------------------------------- -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 following license (LICENSE-MIT) applies to: cast 0.3.0 ---------------------------------------------------------------------------- @@ -3507,37 +2056,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: cbc 0.1.2, ctr 0.9.2 ----------------------------------------------------------------------------- -Copyright (c) 2018-2022 RustCrypto Developers -Copyright (c) 2018 Artyom Pavlov - -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 following license (LICENSE) applies to: cbindgen 0.29.4 ---------------------------------------------------------------------------- @@ -3917,7 +2435,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, filetime 0.2.29, find-msvc-tools 0.1.9, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 +The following license (LICENSE-MIT) applies to: cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, find-msvc-tools 0.1.9, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 ---------------------------------------------------------------------------- Copyright (c) 2014 Alex Crichton @@ -4373,66 +2891,6 @@ their own copyright notices and license terms: copyright itself, held by the contributor. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: cexpr 0.6.0 ----------------------------------------------------------------------------- -(C) Copyright 2016 Jethro G. Beekman - -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 following license (LICENSE-MIT) applies to: cfg-expr 0.20.8 ----------------------------------------------------------------------------- -Copyright (c) 2019 Embark Studios - -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 following license (LICENSE) applies to: cfg_aliases 0.2.1 ---------------------------------------------------------------------------- @@ -4509,7 +2967,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: ciborium 0.2.2, ciborium-io 0.2.2, ciborium-ll 0.2.2, clang-sys 1.8.1, flume 0.12.0, fragile 2.1.0, lru-slab 0.1.2, quinn 0.11.11, quinn-proto 0.11.15, quinn-udp 0.5.14, rpkg-config 0.1.2, rustls-platform-verifier 0.6.2, tinyvec 1.11.0, unarray 0.1.4, ureq 2.12.1, utf8_iter 1.0.4, utoipa 5.5.0, utoipa-axum 0.2.0, utoipa-gen 5.5.0, utoipa-scalar 0.3.0, x11rb 0.13.2, x11rb-protocol 0.13.2, zeroize 1.9.0, zune-core 0.5.1, zune-jpeg 0.5.15 +The following license (LICENSE) applies to: ciborium 0.2.2, ciborium-io 0.2.2, ciborium-ll 0.2.2, flume 0.12.0, lru-slab 0.1.2, quinn 0.11.11, quinn-proto 0.11.15, quinn-udp 0.5.14, rustls-platform-verifier 0.6.2, tinyvec 1.11.0, unarray 0.1.4, zeroize 1.9.0 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -4744,32 +3202,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: color_quant 1.1.0 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2016 PistonDevelopers - -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 following license (LICENSE) applies to: combine 4.6.7 ---------------------------------------------------------------------------- @@ -4826,32 +3258,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: convert_case 0.8.0 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2025 rutrum - -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 following license (LICENSE-MIT) applies to: core-foundation 0.10.1, core-foundation-sys 0.8.7 ---------------------------------------------------------------------------- @@ -4912,32 +3318,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: crc32fast 1.5.0 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2018 Sam Rijs, Alex Crichton and 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 following license (LICENSE-MIT) applies to: criterion 0.5.1, criterion-plot 0.5.0 ---------------------------------------------------------------------------- @@ -4969,7 +3349,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: crossbeam-utils 0.8.21 +The following license (LICENSE-MIT) applies to: crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21 ---------------------------------------------------------------------------- The MIT License (MIT) @@ -5056,6 +3436,37 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: ctr 0.9.2 +---------------------------------------------------------------------------- +Copyright (c) 2018-2022 RustCrypto Developers +Copyright (c) 2018 Artyom Pavlov + +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 following license (LICENSE) applies to: curve25519-dalek 4.1.3 ---------------------------------------------------------------------------- @@ -5127,37 +3538,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: data-encoding 2.11.0 +The following license (LICENSE-MIT) applies to: curve25519-dalek-derive 0.1.1, fastrand 2.4.1, flume 0.12.0, hermit-abi 0.5.2, is-terminal 0.4.17, itoa 1.0.18, linux-raw-sys 0.12.1, num_enum 0.7.6, num_enum_derive 0.7.6, once_cell 1.21.4, pin-project-lite 0.2.17, proc-macro-crate 3.5.0, proc-macro2 1.0.106, quote 1.0.46, rustc-hash 2.1.2, rustix 1.1.4, rustversion 1.0.22, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, syn 2.0.118, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1, zmij 1.0.21 ---------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2015-2020 Julien Cretin -Copyright (c) 2017-2020 Google Inc. - -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 following license (LICENSE-MIT) applies to: der 0.7.10, pkcs8 0.10.2 ----------------------------------------------------------------------------- -Copyright (c) 2020-2023 The RustCrypto Project Developers - 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 @@ -5444,37 +3826,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: downcast-rs 1.2.1 ----------------------------------------------------------------------------- -Copyright (c) 2020 Ashish Myles and 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 following license (LICENSE-MIT) applies to: either 1.16.0, itertools 0.10.5, itertools 0.13.0 +The following license (LICENSE-MIT) applies to: either 1.16.0, itertools 0.10.5 ---------------------------------------------------------------------------- Copyright (c) 2015 @@ -5503,212 +3855,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: enumflags2 0.7.12 ----------------------------------------------------------------------------- -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of this License; and -You must cause any modified files to carry prominent notices stating that You changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - -You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - - -Copyright 2017-2023 Maik Klein, Maja Kądziołka - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: enumflags2 0.7.12 ----------------------------------------------------------------------------- -Copyright (c) 2017-2023 Maik Klein, Maja Kądziołka - -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 following license (LICENSE-APACHE) applies to: enumflags2_derive 0.7.12 ----------------------------------------------------------------------------- -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of this License; and -You must cause any modified files to carry prominent notices stating that You changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - -You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - - -Copyright [2017] [Maik Klein] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: enumflags2_derive 0.7.12 ----------------------------------------------------------------------------- -Copyright (c) 2017 Maik Klein - -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 following license (LICENSE-MIT) applies to: equivalent 1.0.2 ---------------------------------------------------------------------------- @@ -5770,51 +3916,184 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: fallible-iterator 0.3.0 +The following license (LICENSE-APACHE) applies to: fastbloom 0.14.1, itoa 1.0.18, libc 0.2.186, num_enum 0.7.6, num_enum_derive 0.7.6, proc-macro2 1.0.106, quote 1.0.46, rustc-hash 2.1.2, rustversion 1.0.22, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, syn 2.0.118, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, utf8parse 0.2.2 ---------------------------------------------------------------------------- -Copyright (c) 2015 The rust-openssl-verify Developers +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -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: +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +1. Definitions. -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. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: fallible-streaming-iterator 0.1.9 ----------------------------------------------------------------------------- -Copyright (c) 2016 The fallible-streaming-iterator Developers + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -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: + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -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. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS ---------------------------------------------------------------------------- @@ -5847,233 +4126,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: fdeflate 0.3.7, field-offset 0.3.6, half 2.7.1, image 0.25.10, miniz_oxide 0.8.9, num-conv 0.2.2, pin-project-lite 0.2.17, raw-window-handle 0.6.2, sync_wrapper 1.0.2, time 0.3.51, time-core 0.1.9, time-macros 0.2.30, widestring 1.2.1 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: fdeflate 0.3.7, image 0.25.10 ----------------------------------------------------------------------------- -MIT License - -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 following license (LICENSE) applies to: ffmpeg-next 8.1.0 ----------------------------------------------------------------------------- -DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 - - Everyone is permitted to copy and distribute verbatim or modified - copies of this license document, and changing it is allowed as long - as the name is changed. - - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. You just DO WHAT THE FUCK YOU WANT TO. - - ---------------------------------------------------------------------------- The following license (COPYRIGHT) applies to: fiat-crypto 0.2.9 ---------------------------------------------------------------------------- @@ -6160,32 +4212,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: field-offset 0.3.6 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2016-2021 Diggory Blake, and other 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 following license (LICENSE-MIT) applies to: fixedbitset 0.5.7 ---------------------------------------------------------------------------- @@ -6216,36 +4242,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: flate2 1.1.9 ----------------------------------------------------------------------------- -Copyright (c) 2014-2026 Alex Crichton - -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 following license (LICENSE-MIT) applies to: fnv 1.0.7 ---------------------------------------------------------------------------- @@ -6323,37 +4319,7 @@ identification purposes only; their use does not imply endorsement. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: form_urlencoded 1.2.2 ----------------------------------------------------------------------------- -Copyright (c) 2013-2016 The rust-url developers - -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 following license (LICENSE-APACHE) applies to: futures 0.3.32, futures-channel 0.3.32, futures-core 0.3.32, futures-executor 0.3.32, futures-io 0.3.32, futures-macro 0.3.32, futures-sink 0.3.32, futures-task 0.3.32, futures-util 0.3.32 +The following license (LICENSE-APACHE) applies to: futures-channel 0.3.32, futures-core 0.3.32, futures-io 0.3.32, futures-macro 0.3.32, futures-sink 0.3.32, futures-task 0.3.32, futures-util 0.3.32 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -6560,7 +4526,7 @@ limitations under the License. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: futures 0.3.32, futures-channel 0.3.32, futures-core 0.3.32, futures-executor 0.3.32, futures-io 0.3.32, futures-macro 0.3.32, futures-sink 0.3.32, futures-task 0.3.32, futures-util 0.3.32 +The following license (LICENSE-MIT) applies to: futures-channel 0.3.32, futures-core 0.3.32, futures-io 0.3.32, futures-macro 0.3.32, futures-sink 0.3.32, futures-task 0.3.32, futures-util 0.3.32 ---------------------------------------------------------------------------- Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors @@ -6617,7 +4583,7 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: getrandom 0.2.17, getrandom 0.3.4, getrandom 0.4.3, rand_chacha 0.3.1 +The following license (LICENSE-APACHE) applies to: getrandom 0.2.17, getrandom 0.3.4, getrandom 0.4.3 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -6945,32 +4911,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: gif 0.14.2 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2015 nwin - -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 following license (LICENSE) applies to: Granite subset (vendored, crates/pyrowave-sys) ---------------------------------------------------------------------------- @@ -6997,37 +4937,188 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: h2 0.4.15 +The following license (LICENSE-APACHE) applies to: half 2.7.1, num-conv 0.2.2, pin-project-lite 0.2.17, raw-window-handle 0.6.2, time 0.3.51, time-core 0.1.9, time-macros 0.2.30 ---------------------------------------------------------------------------- -Copyright (c) 2017 h2 authors +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -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: + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. + 1. Definitions. -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. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: half 2.7.1, widestring 1.2.1 +The following license (LICENSE-MIT) applies to: half 2.7.1 ---------------------------------------------------------------------------- MIT License @@ -7051,7 +5142,7 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: hashbrown 0.16.1, hashbrown 0.17.1 +The following license (LICENSE-MIT) applies to: hashbrown 0.17.1 ---------------------------------------------------------------------------- Copyright (c) 2016 Amanieu d'Antras @@ -7081,38 +5172,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: hashlink 0.12.0 ----------------------------------------------------------------------------- -This work is derived in part from the `linked-hash-map` crate, Copyright (c) -2015 The Rust Project Developers - -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 following license (LICENSE-MIT) applies to: heck 0.5.0, unicode-segmentation 1.13.3, unicode-width 0.2.2 +The following license (LICENSE-MIT) applies to: heck 0.5.0 ---------------------------------------------------------------------------- Copyright (c) 2015 The Rust Project Developers @@ -7141,31 +5201,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: hex 0.4.3 ----------------------------------------------------------------------------- -Copyright (c) 2013-2014 The Rust Project Developers. -Copyright (c) 2015-2020 The rust-hex Developers - -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 following license (LICENSE-MIT) applies to: hkdf 0.12.4 ---------------------------------------------------------------------------- @@ -7197,715 +5232,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: http 1.4.2 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright 2017 http-rs authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: http 1.4.2 ----------------------------------------------------------------------------- -Copyright (c) 2017 http-rs authors - -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 following license (LICENSE) applies to: http-body 1.0.1 ----------------------------------------------------------------------------- -Copyright (c) 2019-2024 Sean McArthur & Hyper 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 following license (LICENSE) applies to: http-body-util 0.1.3 ----------------------------------------------------------------------------- -Copyright (c) 2019-2025 Sean McArthur & Hyper 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 following license (LICENSE-MIT) applies to: httparse 1.10.1, num_cpus 1.17.0 ----------------------------------------------------------------------------- -Copyright (c) 2015-2025 Sean McArthur - -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 following license (LICENSE-APACHE) applies to: httpdate 1.0.3 ----------------------------------------------------------------------------- -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, -and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by -the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all -other entities that control, are controlled by, or are under common -control with that entity. For the purposes of this definition, -"control" means (i) the power, direct or indirect, to cause the -direction or management of such entity, whether by contract or -otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity -exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, -including but not limited to software source code, documentation -source, and configuration files. - -"Object" form shall mean any form resulting from mechanical -transformation or translation of a Source form, including but -not limited to compiled object code, generated documentation, -and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or -Object form, made available under the License, as indicated by a -copyright notice that is included in or attached to the work -(an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object -form, that is based on (or derived from) the Work and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. For the purposes -of this License, Derivative Works shall not include works that remain -separable from, or merely link (or bind by name) to the interfaces of, -the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including -the original version of the Work and any modifications or additions -to that Work or Derivative Works thereof, that is intentionally -submitted to Licensor for inclusion in the Work by the copyright owner -or by an individual or Legal Entity authorized to submit on behalf of -the copyright owner. For the purposes of this definition, "submitted" -means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, -and issue tracking systems that are managed by, or on behalf of, the -Licensor for the purpose of discussing and improving the Work, but -excluding communication that is conspicuously marked or otherwise -designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity -on behalf of whom a Contribution has been received by Licensor and -subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the -Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except as stated in this section) patent license to make, have made, -use, offer to sell, sell, import, and otherwise transfer the Work, -where such license applies only to those patent claims licensable -by such Contributor that are necessarily infringed by their -Contribution(s) alone or by combination of their Contribution(s) -with the Work to which such Contribution(s) was submitted. If You -institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work -or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses -granted to You under this License for that Work shall terminate -as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the -Work or Derivative Works thereof in any medium, with or without -modifications, and in Source or Object form, provided that You -meet the following conditions: - -(a) You must give any other recipients of the Work or -Derivative Works a copy of this License; and - -(b) You must cause any modified files to carry prominent notices -stating that You changed the files; and - -(c) You must retain, in the Source form of any Derivative Works -that You distribute, all copyright, patent, trademark, and -attribution notices from the Source form of the Work, -excluding those notices that do not pertain to any part of -the Derivative Works; and - -(d) If the Work includes a "NOTICE" text file as part of its -distribution, then any Derivative Works that You distribute must -include a readable copy of the attribution notices contained -within such NOTICE file, excluding those notices that do not -pertain to any part of the Derivative Works, in at least one -of the following places: within a NOTICE text file distributed -as part of the Derivative Works; within the Source form or -documentation, if provided along with the Derivative Works; or, -within a display generated by the Derivative Works, if and -wherever such third-party notices normally appear. The contents -of the NOTICE file are for informational purposes only and -do not modify the License. You may add Your own attribution -notices within Derivative Works that You distribute, alongside -or as an addendum to the NOTICE text from the Work, provided -that such additional attribution notices cannot be construed -as modifying the License. - -You may add Your own copyright statement to Your modifications and -may provide additional or different license terms and conditions -for use, reproduction, or distribution of Your modifications, or -for any such Derivative Works as a whole, provided Your use, -reproduction, and distribution of the Work otherwise complies with -the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, -any Contribution intentionally submitted for inclusion in the Work -by You to the Licensor shall be under the terms and conditions of -this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify -the terms of any separate license agreement you may have executed -with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade -names, trademarks, service marks, or product names of the Licensor, -except as required for reasonable and customary use in describing the -origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or -agreed to in writing, Licensor provides the Work (and each -Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied, including, without limitation, any warranties or conditions -of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A -PARTICULAR PURPOSE. You are solely responsible for determining the -appropriateness of using or redistributing the Work and assume any -risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, -whether in tort (including negligence), contract, or otherwise, -unless required by applicable law (such as deliberate and grossly -negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, -incidental, or consequential damages of any character arising as a -result of this License or out of the use or inability to use the -Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all -other commercial damages or losses), even if such Contributor -has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing -the Work or Derivative Works thereof, You may choose to offer, -and charge a fee for, acceptance of support, warranty, indemnity, -or other liability obligations and/or rights consistent with this -License. However, in accepting such obligations, You may act only -on Your own behalf and on Your sole responsibility, not on behalf -of any other Contributor, and only if You agree to indemnify, -defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason -of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following -boilerplate notice, with the fields enclosed by brackets "[]" -replaced with your own identifying information. (Don't include -the brackets!) The text should be enclosed in the appropriate -comment syntax for the file format. We also recommend that a -file or class name and description of purpose be included on the -same "printed page" as the copyright notice for easier -identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: httpdate 1.0.3 ----------------------------------------------------------------------------- -Copyright (c) 2016 Pyfisch - -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 following license (LICENSE) applies to: hyper 1.10.1 ----------------------------------------------------------------------------- -Copyright (c) 2014-2026 Sean McArthur - -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 following license (LICENSE) applies to: hyper-util 0.1.20 ----------------------------------------------------------------------------- -Copyright (c) 2023-2025 Sean McArthur - -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 following license (LICENSE) applies to: icu_collections 2.2.0, icu_locale_core 2.2.0, icu_normalizer 2.2.0, icu_normalizer_data 2.2.0, icu_properties 2.2.0, icu_properties_data 2.2.0, icu_provider 2.2.0, litemap 0.8.2, potential_utf 0.1.5, tinystr 0.8.3, writeable 0.6.3, yoke 0.8.3, yoke-derive 0.8.2, zerofrom 0.1.8, zerofrom-derive 0.1.7, zerotrie 0.2.4, zerovec 0.11.6, zerovec-derive 0.11.3 ----------------------------------------------------------------------------- -UNICODE LICENSE V3 - -COPYRIGHT AND PERMISSION NOTICE - -Copyright © 2020-2024 Unicode, Inc. - -NOTICE TO USER: Carefully read the following legal agreement. BY -DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR -SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE -TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT -DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of data files and any associated documentation (the "Data Files") or -software and any associated documentation (the "Software") to deal in the -Data Files or Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, and/or sell -copies of the Data Files or Software, and to permit persons to whom the -Data Files or Software are furnished to do so, provided that either (a) -this copyright and permission notice appear with all copies of the Data -Files or Software, or (b) this copyright and permission notice appear in -associated Documentation. - -THE DATA FILES AND SOFTWARE ARE 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 OF -THIRD PARTY RIGHTS. - -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE -BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA -FILES OR SOFTWARE. - -Except as contained in this notice, the name of a copyright holder shall -not be used in advertising or otherwise to promote the sale, use or other -dealings in these Data Files or Software without prior written -authorization of the copyright holder. - -SPDX-License-Identifier: Unicode-3.0 - -— - -Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. -ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: idna 1.1.0, percent-encoding 2.3.2, url 2.5.8 ----------------------------------------------------------------------------- -Copyright (c) 2013-2025 The rust-url developers - -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 following license (LICENSE-MIT) applies to: idna_adapter 1.2.2 ----------------------------------------------------------------------------- -Copyright (c) The rust-url developers - -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 following license (LICENSE-BSD) applies to: if-addrs 0.13.4, if-addrs 0.15.0 ---------------------------------------------------------------------------- @@ -8075,36 +5401,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: lazy_static 1.5.0 ----------------------------------------------------------------------------- -Copyright (c) 2010 The Rust Project Developers - -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 following license (LICENSE-MIT) applies to: libc 0.2.186 ---------------------------------------------------------------------------- @@ -8135,23 +5431,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: libloading 0.8.9 ----------------------------------------------------------------------------- -Copyright © 2015, Simonas Kazlauskas - -Permission to use, copy, modify, and/or distribute this software for any purpose with or without -fee is hereby granted, provided that the above copyright notice and this permission notice appear -in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS -SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE -AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, -NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -THIS SOFTWARE. - - ---------------------------------------------------------------------------- The following license (LICENSE.txt) applies to: libm 0.2.16 ---------------------------------------------------------------------------- @@ -8415,55 +5694,6 @@ have been licensed under extremely permissive terms. Copyright notices are retained in src/* files where relevant. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: libspa 0.9.2, libspa-sys 0.9.2, pipewire 0.9.2, pipewire-sys 0.9.2 ----------------------------------------------------------------------------- -Copyright The pipewire-rs 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 (including the next -paragraph) 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 following license (LICENSE) applies to: libsqlite3-sys 0.38.1, rusqlite 0.40.1 ----------------------------------------------------------------------------- -Copyright (c) 2014 The rusqlite developers - -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 following license (COPYRIGHT) applies to: linux-raw-sys 0.12.1 ---------------------------------------------------------------------------- @@ -8499,7 +5729,7 @@ at your option. ---------------------------------------------------------------------------- -The following license (LICENSE-Apache-2.0_WITH_LLVM-exception) applies to: linux-raw-sys 0.12.1, rustix 1.1.4, target-lexicon 0.13.5, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1 +The following license (LICENSE-Apache-2.0_WITH_LLVM-exception) applies to: linux-raw-sys 0.12.1, rustix 1.1.4, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -8722,7 +5952,7 @@ Software. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: lock_api 0.4.14, nasm-rs 0.3.2, parking_lot 0.12.5, parking_lot_core 0.9.12, rustc_version 0.4.1, thread_local 1.1.9 +The following license (LICENSE-MIT) applies to: lock_api 0.4.14, parking_lot 0.12.5, parking_lot_core 0.9.12, rustc_version 0.4.1 ---------------------------------------------------------------------------- Copyright (c) 2016 The Rust Project Developers @@ -8787,308 +6017,6 @@ the following restrictions: 3. This notice may not be removed or altered from any source distribution. ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE2.0) applies to: mac_address 1.1.8 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2018 Wesley Norris - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: mac_address 1.1.8 ----------------------------------------------------------------------------- -Copyright © 2018 Wesley Norris - -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 following license (LICENSE) applies to: matchers 0.2.0 ----------------------------------------------------------------------------- -Copyright (c) 2019 Eliza Weisman - -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 following license (LICENSE) applies to: matchit 0.8.4 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2022 Ibraheem Ahmed - -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 following license (LICENSE.httprouter) applies to: matchit 0.8.4 ----------------------------------------------------------------------------- -BSD 3-Clause License - -Copyright (c) 2013, Julien Schmidt -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ---------------------------------------------------------------------------- The following license (LICENSE-APACHE) applies to: mdns-sd 0.20.1 ---------------------------------------------------------------------------- @@ -9321,411 +6249,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: memmap2 0.9.11 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [2015] [Dan Burkert] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: memmap2 0.9.11 ----------------------------------------------------------------------------- -Copyright (c) 2020 Yevhenii Reizner -Copyright (c) 2015 Dan Burkert - -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 following license (LICENSE) applies to: memoffset 0.9.1 ----------------------------------------------------------------------------- -Copyright (c) 2017 Gilad Naaman - -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 following license (LICENSE-MIT) applies to: mime 0.3.17 ----------------------------------------------------------------------------- -Copyright (c) 2014 Sean McArthur - -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 following license (LICENSE.md) applies to: minimal-lexical 0.2.1 ----------------------------------------------------------------------------- -Minimal-lexical is dual licensed under the Apache 2.0 license as well as the MIT -license. See the LICENCE-MIT and the LICENCE-APACHE files for the licenses. - ---- - -`src/bellerophon.rs` is loosely based off the Golang implementation, -found [here](https://github.com/golang/go/blob/b10849fbb97a2244c086991b4623ae9f32c212d0/src/strconv/extfloat.go). -That code (used if the `compact` feature is enabled) is subject to a -[3-clause BSD license](https://github.com/golang/go/blob/b10849fbb97a2244c086991b4623ae9f32c212d0/LICENSE): - -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: miniz_oxide 0.8.9 ----------------------------------------------------------------------------- -MIT License - -Copyright 2013-2014 RAD Game Tools and Valve Software -Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC -Copyright (c) 2017 Frommi -Copyright (c) 2017-2024 oyvindln - - -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 following license (LICENSE-MIT.md) applies to: miniz_oxide 0.8.9 ----------------------------------------------------------------------------- -MIT License - -Copyright 2013-2014 RAD Game Tools and Valve Software -Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC -Copyright (c) 2017 Frommi -Copyright (c) 2017-2024 oyvindln - -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 following license (LICENSE-ZLIB.md) applies to: miniz_oxide 0.8.9 ----------------------------------------------------------------------------- -Copyright 2013-2014 RAD Game Tools and Valve Software -Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC -Copyright (c) 2020 Frommi -Copyright (c) 2017-2024 oyvindln - -This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. - - ---------------------------------------------------------------------------- The following license (LICENSE) applies to: mio 1.2.1 ---------------------------------------------------------------------------- @@ -9750,321 +6273,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE.md) applies to: moxcms 0.8.1, pxfm 0.1.30 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2024 Radzivon Bartoshyk - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE.md) applies to: moxcms 0.8.1, pxfm 0.1.30 ----------------------------------------------------------------------------- -Copyright (c) Radzivon Bartoshyk. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: nix 0.29.0, nix 0.30.1 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2015 Carl Lerche + nix-rust Authors - -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 following license (LICENSE) applies to: nom 7.1.3, nom 8.0.0 ----------------------------------------------------------------------------- -Copyright (c) 2014-2019 Geoffroy Couprie - -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 following license (LICENSE) applies to: nu-ansi-term 0.50.3 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2014 Benjamin Sago -Copyright (c) 2021-2022 The Nushell Project Developers - -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 following license (LICENSE-MIT) applies to: num-conv 0.2.2 ---------------------------------------------------------------------------- @@ -10201,20 +6409,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-THIRD-PARTY) applies to: parking 2.2.1 ----------------------------------------------------------------------------- -=============================================================================== - -Copyright 2014-2020 The Rust Project Developers - -Licensed under the Apache License, Version 2.0 or the MIT license -, at your -option. All files in the project carrying such notice may not be -copied, modified, or distributed except according to those terms. - - ---------------------------------------------------------------------------- The following license (LICENSE.md) applies to: pem 3.0.6 ---------------------------------------------------------------------------- @@ -10241,96 +6435,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: pem-rfc7468 0.7.0 ----------------------------------------------------------------------------- -Copyright (c) 2021 The RustCrypto Project Developers - -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 following license (LICENSE-MIT) applies to: pkcs1 0.7.5, spki 0.7.3 ----------------------------------------------------------------------------- -Copyright (c) 2021-2023 The RustCrypto Project Developers - -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 following license (LICENSE-MIT) applies to: png 0.18.1 ----------------------------------------------------------------------------- -Copyright (c) 2015 nwin - -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 following license (LICENSE-MIT) applies to: poly1305 0.8.0 ---------------------------------------------------------------------------- @@ -10857,6 +6961,212 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: proc-macro-crate 3.5.0 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/LICENSE-2.0 + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + ---------------------------------------------------------------------------- The following license (LICENSE-MIT) applies to: proptest 1.11.0, rusty-fork 0.3.1 ---------------------------------------------------------------------------- @@ -10936,34 +7246,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT.md) applies to: quick-xml 0.39.4 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2016 Johann Tuffe - -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 following license (LICENSE-MIT) applies to: quinn 0.11.11, quinn-proto 0.11.15, quinn-udp 0.5.14 ---------------------------------------------------------------------------- @@ -10977,7 +7259,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ---------------------------------------------------------------------------- -The following license (COPYRIGHT) applies to: rand 0.8.6, rand 0.9.4, rand_chacha 0.3.1, rand_chacha 0.9.0, rand_core 0.6.4, rand_core 0.9.5, rand_xorshift 0.4.0 +The following license (COPYRIGHT) applies to: rand 0.9.4, rand_chacha 0.9.0, rand_core 0.6.4, rand_core 0.9.5, rand_xorshift 0.4.0 ---------------------------------------------------------------------------- Copyrights in the Rand project are retained by their contributors. No copyright assignment is required to contribute to the Rand project. @@ -10994,7 +7276,7 @@ published under these same licenses. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: rand 0.8.6, rand 0.9.4, rand_chacha 0.9.0, rand_xorshift 0.4.0 +The following license (LICENSE-APACHE) applies to: rand 0.9.4, rand_chacha 0.9.0, rand_xorshift 0.4.0 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -11175,7 +7457,7 @@ END OF TERMS AND CONDITIONS ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: rand 0.8.6, rand 0.9.4, rand_chacha 0.3.1, rand_chacha 0.9.0, rand_core 0.6.4, rand_core 0.9.5, rand_xorshift 0.4.0 +The following license (LICENSE-MIT) applies to: rand 0.9.4, rand_chacha 0.9.0, rand_core 0.6.4, rand_core 0.9.5, rand_xorshift 0.4.0 ---------------------------------------------------------------------------- Copyright 2018 Developers of the Rand project Copyright (c) 2014 The Rust Project Developers @@ -11439,6 +7721,36 @@ Permission is granted to anyone to use this software for any purpose, including 3. This notice may not be removed or altered from any source distribution. +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: rayon 1.12.0, rayon-core 1.13.0 +---------------------------------------------------------------------------- +Copyright (c) 2010 The Rust Project Developers + +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 following license (LICENSE) applies to: rcgen 0.13.2 ---------------------------------------------------------------------------- @@ -12102,82 +8414,6 @@ OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: roxmltree 0.21.1 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2018 Yevhenii Reizner - -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 following license (LICENSE-MIT.md) applies to: rpkg-config 0.1.2 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2024 Maia S. R. - -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 following license (LICENSE-ZLIB.md) applies to: rpkg-config 0.1.2, sdl3-src 3.4.10 ----------------------------------------------------------------------------- -zlib License - -(C) 2024 Maia S. R. - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. - - ---------------------------------------------------------------------------- The following license (COPYRIGHT) applies to: rustix 1.1.4 ---------------------------------------------------------------------------- @@ -12562,76 +8798,6 @@ The files under third-party/chromium are licensed as described in third-party/chromium/LICENSE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: rusty_enet 0.4.0 ----------------------------------------------------------------------------- -Copyright (c) 2002-2020 Lee Salzman - -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 following license (LICENSE-BOOST) applies to: ryu 1.0.23 ----------------------------------------------------------------------------- -Boost Software License - Version 1.0 - August 17th, 2003 - -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: - -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -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, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT.md) applies to: safe_arch 0.7.4 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2023 Daniel "Lokathor" Gee. - -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 (including the next paragraph) 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 following license (LICENSE-ZLIB.md) applies to: safe_arch 0.7.4, wide 0.7.33 ----------------------------------------------------------------------------- -Copyright (c) 2020 Daniel "Lokathor" Gee. - -This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. - - ---------------------------------------------------------------------------- The following license (LICENSE-MIT) applies to: same-file 1.0.6, winapi-util 0.1.11 ---------------------------------------------------------------------------- @@ -12700,79 +8866,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: sdl3 0.18.4 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2013 Mozilla Foundation - -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 following license (LICENSE.md) applies to: sdl3-image-sys 0.6.4+SDL-image-3.4.4, sdl3-mixer-sys 0.6.3+SDL-mixer-3.2.4, sdl3-ttf-sys 0.6.1+SDL-ttf-3.2.2 ----------------------------------------------------------------------------- -zlib License - -(C) 2025 Maia S Ravn - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. - - ----------------------------------------------------------------------------- -The following license (LICENSE.md) applies to: sdl3-sys 0.6.6+SDL-3.4.10 ----------------------------------------------------------------------------- -zlib License - -(C) 2024-2025 Maia S Ravn - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. - - ---------------------------------------------------------------------------- The following license (LICENSE-MIT) applies to: security-framework 3.7.0, security-framework-sys 2.17.0 ---------------------------------------------------------------------------- @@ -12798,36 +8891,6 @@ 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 following license (LICENSE-MIT) applies to: serde_urlencoded 0.7.1 ----------------------------------------------------------------------------- -Copyright (c) 2016 Anthony Ramine - -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 following license (LICENSE-MIT) applies to: sha2 0.10.9 ---------------------------------------------------------------------------- @@ -12861,31 +8924,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: sharded-slab 0.1.7 ----------------------------------------------------------------------------- -Copyright (c) 2019 Eliza Weisman - -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 following license (LICENSE-APACHE) applies to: shlex 1.3.0, shlex 2.0.1 +The following license (LICENSE-APACHE) applies to: shlex 2.0.1 ---------------------------------------------------------------------------- Copyright 2015 Nicholas Allegra (comex). @@ -12903,7 +8942,7 @@ limitations under the License. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: shlex 1.3.0, shlex 2.0.1 +The following license (LICENSE-MIT) applies to: shlex 2.0.1 ---------------------------------------------------------------------------- The MIT License (MIT) @@ -12958,62 +8997,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: signature 2.2.0 ----------------------------------------------------------------------------- -Copyright (c) 2018-2023 RustCrypto Developers - -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 following license (LICENSE.md) applies to: simd-adler32 0.3.9 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) [2021] [Marvin Countryman] - -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 following license (simple-icons.txt) applies to: Simple Icons (vendored, assets/os-icons) ---------------------------------------------------------------------------- @@ -13180,62 +9163,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: sqlite-wasm-rs 0.5.5 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2024 Spxg - -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 following license (LICENSE-MIT) applies to: stable_deref_trait 1.2.1 ----------------------------------------------------------------------------- -Copyright (c) 2017 Robert Grosse - -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 following license (LICENSE) applies to: strsim 0.11.1 ---------------------------------------------------------------------------- @@ -13299,49 +9226,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: synstructure 0.13.2 ----------------------------------------------------------------------------- -Copyright 2016 Nika Layzell - -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 following license (LICENSE-MIT) applies to: tar 0.4.46 ----------------------------------------------------------------------------- -Copyright (c) The tar-rs Project 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 following license (LICENSE-MIT) applies to: tempfile 3.27.0, xattr 1.6.1 +The following license (LICENSE-MIT) applies to: tempfile 3.27.0 ---------------------------------------------------------------------------- Copyright (c) 2015 Steven Allen @@ -13434,6 +9319,22 @@ The above copyright notice and this permission notice shall be included in all c 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 following license (LICENSE-ZLIB.md) applies to: tinyvec 1.11.0 +---------------------------------------------------------------------------- +Copyright (c) 2019 Daniel "Lokathor" Gee. + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + + ---------------------------------------------------------------------------- The following license (LICENSE-APACHE.md) applies to: tinyvec_macros 0.1.1 ---------------------------------------------------------------------------- @@ -13691,7 +9592,7 @@ freely, subject to the following restrictions: ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: tokio 1.52.3, tokio-util 0.7.18 +The following license (LICENSE) applies to: tokio 1.52.3 ---------------------------------------------------------------------------- MIT License @@ -13744,273 +9645,7 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: tokio-rustls 0.26.4 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright 2017 quininer kel - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: tokio-rustls 0.26.4 ----------------------------------------------------------------------------- -Copyright (c) 2017 quininer kel - -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 following license (LICENSE) applies to: tower 0.5.3, tower-layer 0.3.3, tower-service 0.3.3 ----------------------------------------------------------------------------- -Copyright (c) 2019 Tower 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 following license (LICENSE) applies to: tracing 0.1.44, tracing-attributes 0.1.31, tracing-core 0.1.36, tracing-log 0.2.0, tracing-subscriber 0.3.23 +The following license (LICENSE) applies to: tracing 0.1.44, tracing-attributes 0.1.31, tracing-core 0.1.36 ---------------------------------------------------------------------------- Copyright (c) 2019 Tokio Contributors @@ -14277,110 +9912,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: uds_windows 1.2.1 ----------------------------------------------------------------------------- -MIT License - - Copyright (c) Microsoft Corporation. All rights reserved. - - 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 following license (THIRDPARTYNOTICES) applies to: uds_windows 1.2.1 ----------------------------------------------------------------------------- -Third Party Notices - -*** - -rust -Copyright 2016 The Rust Project Developers -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. - -*** - -mio -Copyright (c) 2014 Carl Lerche and other MIO 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. - -*** - -miow -Copyright (c) 2014 Alex Crichton -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 following license (LICENSE-MIT) applies to: unarray 0.1.4 ---------------------------------------------------------------------------- @@ -14451,18 +9982,6 @@ dealings in these Data Files or Software without prior written authorization of the copyright holder. ----------------------------------------------------------------------------- -The following license (COPYRIGHT) applies to: unicode-segmentation 1.13.3, unicode-width 0.2.2 ----------------------------------------------------------------------------- -Licensed under the Apache License, Version 2.0 - or the MIT -license , -at your option. All files in the project carrying such -notice may not be copied, modified, or distributed except -according to those terms. - - ---------------------------------------------------------------------------- The following license (LICENSE-MIT) applies to: universal-hash 0.5.1 ---------------------------------------------------------------------------- @@ -14511,109 +10030,6 @@ The following license (LICENSE.txt) applies to: untrusted 0.9.0 // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: ureq 2.12.1 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2019 Martin Algesten - -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 following license (COPYRIGHT) applies to: utf8_iter 1.0.4 ----------------------------------------------------------------------------- -Copyright Mozilla Foundation - -Licensed under the Apache License (Version 2.0), or the MIT license, -(the "Licenses") at your option. You may not use this file except in -compliance with one of the Licenses. You may obtain copies of the -Licenses at: - - https://www.apache.org/licenses/LICENSE-2.0 - https://opensource.org/licenses/MIT - -Unless required by applicable law or agreed to in writing, software -distributed under the Licenses is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the Licenses for the specific language governing permissions and -limitations under the Licenses. - --- - -Test code is dedicated to the Public Domain when so designated (see -the individual files for PD/CC0-dedicated sections). - --- - -The implementation for Utf8CharIndices was adapted from the -CharIndices implementation of the Rust standard library at revision -ab32548539ec38a939c1b58599249f3b54130026 -(https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/library/core/src/str/iter.rs). - -Excerpt from https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/COPYRIGHT , -which refers to -https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/LICENSE-APACHE -and -https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/LICENSE-MIT -: - -For full authorship information, see the version control history or -https://thanks.rust-lang.org - -Except as otherwise noted (below and/or in individual files), Rust is -licensed under the Apache License, Version 2.0 or - or the MIT license - or , at your option. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: utf8_iter 1.0.4 ----------------------------------------------------------------------------- -Copyright Mozilla Foundation - -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 following license (LICENSE-MIT) applies to: utf8parse 0.2.2 ---------------------------------------------------------------------------- @@ -14644,118 +10060,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: utoipa 5.5.0, utoipa-axum 0.2.0, utoipa-gen 5.5.0, utoipa-scalar 0.3.0 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright © 2021 - - -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 following license (LICENSE-MIT) applies to: uuid 1.23.4 ----------------------------------------------------------------------------- -Copyright (c) 2014 The Rust Project Developers -Copyright (c) 2018 Ashley Mannix, Christopher Armstrong, Dylan DPC, Hunar Roop Kahlon - -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 following license (LICENSE-MIT) applies to: vcpkg 0.2.15 ----------------------------------------------------------------------------- -Copyright (c) 2017 Jim McGrath - -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 following license (LICENSE) applies to: version-compare 0.2.1 ----------------------------------------------------------------------------- -Copyright (c) 2017 Tim Visée - -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 following license (LICENSE-MIT) applies to: version_check 0.9.5 ---------------------------------------------------------------------------- @@ -14827,54 +10131,6 @@ Full license text of these licenses is available at: * MIT: https://opensource.org/licenses/MIT ----------------------------------------------------------------------------- -The following license (LICENSE.txt) applies to: wasapi 0.23.0 ----------------------------------------------------------------------------- -Copyright (c) 2020 Henrik Enquist - -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 following license (LICENSE.txt) applies to: wayland-backend 0.3.15, wayland-client 0.31.14, wayland-protocols 0.32.13, wayland-protocols-misc 0.3.12, wayland-protocols-wlr 0.3.12, wayland-scanner 0.31.10, wayland-sys 0.31.11 ----------------------------------------------------------------------------- -Copyright (c) 2015 Elinor Berger - -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 following license (LICENSE-APACHE) applies to: web-time 1.1.0 ---------------------------------------------------------------------------- @@ -15108,7 +10364,7 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: webpki-root-certs 1.0.8, webpki-roots 0.26.11, webpki-roots 1.0.8 +The following license (LICENSE) applies to: webpki-root-certs 1.0.8 ---------------------------------------------------------------------------- # Community Data License Agreement - Permissive - Version 2.0 @@ -15174,57 +10430,7 @@ insights. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: weezl 0.1.12 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) HeroicKatora 2020 - -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 following license (LICENSE-MIT) applies to: winapi 0.3.9 ----------------------------------------------------------------------------- -Copyright (c) 2015-2018 The winapi-rs Developers - -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 following license (license-apache-2.0) applies to: windows 0.62.2, windows-canvas 0.0.0, windows-collections 0.3.2, windows-composition 0.0.0, windows-core 0.62.2, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-numerics 0.3.1, windows-reactor 0.0.0, windows-reactor-setup 0.0.0, windows-reference 0.1.0, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.45.0, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.60.2, windows-sys 0.61.2, windows-targets 0.42.2, windows-targets 0.52.6, windows-targets 0.53.5, windows-threading 0.2.1, windows-time 0.1.0, windows-window 0.0.0, windows_aarch64_gnullvm 0.42.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_gnullvm 0.53.1, windows_aarch64_msvc 0.42.2, windows_aarch64_msvc 0.52.6, windows_aarch64_msvc 0.53.1, windows_i686_gnu 0.42.2, windows_i686_gnu 0.52.6, windows_i686_gnu 0.53.1, windows_i686_gnullvm 0.52.6, windows_i686_gnullvm 0.53.1, windows_i686_msvc 0.42.2, windows_i686_msvc 0.52.6, windows_i686_msvc 0.53.1, windows_x86_64_gnu 0.42.2, windows_x86_64_gnu 0.52.6, windows_x86_64_gnu 0.53.1, windows_x86_64_gnullvm 0.42.2, windows_x86_64_gnullvm 0.52.6, windows_x86_64_gnullvm 0.53.1, windows_x86_64_msvc 0.42.2, windows_x86_64_msvc 0.52.6, windows_x86_64_msvc 0.53.1 +The following license (license-apache-2.0) applies to: windows-link 0.2.1, windows-sys 0.45.0, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.60.2, windows-sys 0.61.2, windows-targets 0.42.2, windows-targets 0.52.6, windows-targets 0.53.5, windows_aarch64_gnullvm 0.42.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_gnullvm 0.53.1, windows_aarch64_msvc 0.42.2, windows_aarch64_msvc 0.52.6, windows_aarch64_msvc 0.53.1, windows_i686_gnu 0.42.2, windows_i686_gnu 0.52.6, windows_i686_gnu 0.53.1, windows_i686_gnullvm 0.52.6, windows_i686_gnullvm 0.53.1, windows_i686_msvc 0.42.2, windows_i686_msvc 0.52.6, windows_i686_msvc 0.53.1, windows_x86_64_gnu 0.42.2, windows_x86_64_gnu 0.52.6, windows_x86_64_gnu 0.53.1, windows_x86_64_gnullvm 0.42.2, windows_x86_64_gnullvm 0.52.6, windows_x86_64_gnullvm 0.53.1, windows_x86_64_msvc 0.42.2, windows_x86_64_msvc 0.52.6, windows_x86_64_msvc 0.53.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -15430,7 +10636,7 @@ Apache License ---------------------------------------------------------------------------- -The following license (license-mit) applies to: windows 0.62.2, windows-canvas 0.0.0, windows-collections 0.3.2, windows-composition 0.0.0, windows-core 0.62.2, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-numerics 0.3.1, windows-reactor 0.0.0, windows-reactor-setup 0.0.0, windows-reference 0.1.0, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.45.0, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.60.2, windows-sys 0.61.2, windows-targets 0.42.2, windows-targets 0.52.6, windows-targets 0.53.5, windows-threading 0.2.1, windows-time 0.1.0, windows-window 0.0.0, windows_aarch64_gnullvm 0.42.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_gnullvm 0.53.1, windows_aarch64_msvc 0.42.2, windows_aarch64_msvc 0.52.6, windows_aarch64_msvc 0.53.1, windows_i686_gnu 0.42.2, windows_i686_gnu 0.52.6, windows_i686_gnu 0.53.1, windows_i686_gnullvm 0.52.6, windows_i686_gnullvm 0.53.1, windows_i686_msvc 0.42.2, windows_i686_msvc 0.52.6, windows_i686_msvc 0.53.1, windows_x86_64_gnu 0.42.2, windows_x86_64_gnu 0.52.6, windows_x86_64_gnu 0.53.1, windows_x86_64_gnullvm 0.42.2, windows_x86_64_gnullvm 0.52.6, windows_x86_64_gnullvm 0.53.1, windows_x86_64_msvc 0.42.2, windows_x86_64_msvc 0.52.6, windows_x86_64_msvc 0.53.1 +The following license (license-mit) applies to: windows-link 0.2.1, windows-sys 0.45.0, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.60.2, windows-sys 0.61.2, windows-targets 0.42.2, windows-targets 0.52.6, windows-targets 0.53.5, windows_aarch64_gnullvm 0.42.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_gnullvm 0.53.1, windows_aarch64_msvc 0.42.2, windows_aarch64_msvc 0.52.6, windows_aarch64_msvc 0.53.1, windows_i686_gnu 0.42.2, windows_i686_gnu 0.52.6, windows_i686_gnu 0.53.1, windows_i686_gnullvm 0.52.6, windows_i686_gnullvm 0.53.1, windows_i686_msvc 0.42.2, windows_i686_msvc 0.52.6, windows_i686_msvc 0.53.1, windows_x86_64_gnu 0.42.2, windows_x86_64_gnu 0.52.6, windows_x86_64_gnu 0.53.1, windows_x86_64_gnullvm 0.42.2, windows_x86_64_gnullvm 0.52.6, windows_x86_64_gnullvm 0.53.1, windows_x86_64_msvc 0.42.2, windows_x86_64_msvc 0.52.6, windows_x86_64_msvc 0.53.1 ---------------------------------------------------------------------------- MIT License @@ -15455,242 +10661,6 @@ MIT License SOFTWARE ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: windows-service 0.7.0 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright 2018 Amagicom AB - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: windows-service 0.7.0 ----------------------------------------------------------------------------- -Copyright (c) 2017 Amagicom AB - -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 following license (LICENSE-MIT) applies to: winnow 0.7.15, winnow 1.0.3 ---------------------------------------------------------------------------- @@ -15714,396 +10684,6 @@ 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 following license (LICENSE) applies to: winreg 0.56.0 ----------------------------------------------------------------------------- -Copyright (c) 2015 Igor Shaula - -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 following license (LICENSE) applies to: winresource 0.1.31 ----------------------------------------------------------------------------- -Copyright 2016 Max Resch - -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 following license (LICENSE-MIT) applies to: x11rb 0.13.2, x11rb-protocol 0.13.2 ----------------------------------------------------------------------------- -Copyright 2019 x11rb Contributers - -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 following license (LICENSE) applies to: xkbcommon 0.8.0 ----------------------------------------------------------------------------- -Copyright (c) 2016 Remi Thebault - -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 following license (LICENSE-APACHE) applies to: xkeysym 0.2.1 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2022-2023 John Nunley - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: xkeysym 0.2.1 ----------------------------------------------------------------------------- -Copyright (c) 2022-2023 John Nunley - -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 following license (LICENSE-ZLIB) applies to: xkeysym 0.2.1 ----------------------------------------------------------------------------- -Copyright (c) 2022-2023 John Nunley - -This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. - - ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: zbus 5.16.0, zbus_macros 5.16.0, zbus_names 4.3.2, zvariant 5.12.0, zvariant_derive 5.12.0 ----------------------------------------------------------------------------- -Copyright (c) 2024 Zeeshan Ali Khan & zbus 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 following license (LICENSE-APACHE) applies to: zerocopy 0.8.52, zerocopy-derive 0.8.52 ---------------------------------------------------------------------------- @@ -16398,53 +10978,3 @@ 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 following license (LICENSE-MIT) applies to: zune-core 0.5.1, zune-jpeg 0.5.15 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) zune-image developers - -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 following license (LICENSE-ZLIB) applies to: zune-core 0.5.1, zune-jpeg 0.5.15 ----------------------------------------------------------------------------- -zlib License - -(C) zune-image developers - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. - diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadSettingsScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadSettingsScreen.kt index 7da32711..a9611574 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadSettingsScreen.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadSettingsScreen.kt @@ -57,6 +57,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeSource +import io.unom.punktfunk.kit.DeviceGyro import io.unom.punktfunk.kit.deviceBodyVibrator import io.unom.punktfunk.kit.security.KnownHost import io.unom.punktfunk.kit.security.KnownHostStore @@ -126,6 +127,8 @@ fun GamepadSettingsScreen( val context = LocalContext.current // Gates the "Rumble on this phone" row — a TV box has no body vibrator to mirror onto. val hasBodyVibrator = remember { deviceBodyVibrator(context) != null } + // Gates "Gyro from this phone" the same way — a TV box has no gyroscope to mirror from. + val hasGyroscope = remember { DeviceGyro.available(context) } // Gates the AV1 codec row the same way the touch settings do (see `codecOptionsFor`). val av1Capable = remember { io.unom.punktfunk.kit.VideoDecoders.pickDecoder("video/av01") != null } @@ -159,7 +162,7 @@ fun GamepadSettingsScreen( // path there is this screen's own Controller-optimized UI toggle, which swaps in the standard // interface remote-navigably. The strings branch on it. val tv = remember { isTvDevice(context) } - val allRows = buildSettingsRows(s, hasBodyVibrator, av1Capable, ::update) + + val allRows = buildSettingsRows(s, hasBodyVibrator, hasGyroscope, av1Capable, ::update) + buildProfileRows(profiles, savedHosts, tv) { pinProfile = it } // Which section is showing, and where each one's focus was when it was last left — a detour // into another tab shouldn't lose your place. @@ -445,12 +448,13 @@ private fun SettingRowView(row: GpRow, focused: Boolean, adjustDir: Int, onClick } /** Build the console settings rows from the current [Settings], writing through [update]. - * [hasBodyVibrator] gates the "Rumble on this phone" row (absent on TVs); [av1Capable] gates the - * AV1 codec entry (see `codecOptionsFor`). Every row declares its [GpTab]; the screen shows one - * tab at a time. */ + * [hasBodyVibrator] gates the "Rumble on this phone" row and [hasGyroscope] the "Gyro from this + * phone" row (both absent on TVs); [av1Capable] gates the AV1 codec entry (see + * `codecOptionsFor`). Every row declares its [GpTab]; the screen shows one tab at a time. */ internal fun buildSettingsRows( s: Settings, hasBodyVibrator: Boolean, + hasGyroscope: Boolean, av1Capable: Boolean, update: (Settings) -> Unit, ): List { @@ -598,6 +602,18 @@ internal fun buildSettingsRows( } else { null }, + // The rumble mirror's sibling, data flowing the other way — needs a gyroscope to + // mirror FROM, which a TV box lacks. + if (hasGyroscope) { + toggle( + "phoneGyro", GpTab.CONTROLLER, null, "Gyro from this phone", + "When the controller has no gyro of its own, send this phone's motion " + + "sensors as controller 1's — for clip-on pads without one.", + s.gyroOnPhone, + ) { update(s.copy(gyroOnPhone = it)) } + } else { + null + }, ) + listOf( // NOT gated on the vibrator (the bug A2 fixed in the touch settings): an SC2 capture has // nothing to do with this device's motor, and a TV box is where it matters most. diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/Settings.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/Settings.kt index ea6328d4..9b1048a1 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/Settings.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/Settings.kt @@ -158,6 +158,16 @@ data class Settings( * toggle is hidden on devices without a vibrator (TVs), where this would be a silent no-op. */ val rumbleOnPhone: Boolean = false, + /** + * Opt-in: use this phone's own gyroscope as controller 1's motion when the forwarded pad has + * none of its own — for clip-on gamepads without an IMU, where the phone body moves with the + * player's hands. The rumble mirror's sibling, data flowing the other way. Off by default; + * read once per session by StreamScreen (it starts a [io.unom.punktfunk.kit.DeviceGyro] only + * when set), and the mirror stands down by itself whenever wire pad 0 is fed by a capture + * link (USB DualSense / SC2 — pads with a real gyro). The toggle is hidden on devices + * without a gyroscope (TVs), where this would be a silent no-op. + */ + val gyroOnPhone: Boolean = false, /** * Capture a Steam Controller 2 (wired / Puck dongle over USB, or an already-paired BLE pad) @@ -300,6 +310,7 @@ class SettingsStore(context: Context) { smoothBuffer = prefs.getInt(K_SMOOTH_BUFFER, 0), autoWakeEnabled = prefs.getBoolean(K_AUTO_WAKE, true), rumbleOnPhone = prefs.getBoolean(K_RUMBLE_ON_PHONE, false), + gyroOnPhone = prefs.getBoolean(K_GYRO_ON_PHONE, false), sc2Capture = prefs.getBoolean(K_SC2_CAPTURE, true), dsCapture = prefs.getBoolean(K_DS_CAPTURE, true), padHaptics = prefs.getBoolean(K_PAD_HAPTICS, true), @@ -340,6 +351,7 @@ class SettingsStore(context: Context) { .putInt(K_SMOOTH_BUFFER, s.smoothBuffer) .putBoolean(K_AUTO_WAKE, s.autoWakeEnabled) .putBoolean(K_RUMBLE_ON_PHONE, s.rumbleOnPhone) + .putBoolean(K_GYRO_ON_PHONE, s.gyroOnPhone) .putBoolean(K_SC2_CAPTURE, s.sc2Capture) .putBoolean(K_DS_CAPTURE, s.dsCapture) .putBoolean(K_PAD_HAPTICS, s.padHaptics) @@ -390,6 +402,7 @@ class SettingsStore(context: Context) { const val K_SMOOTH_BUFFER = "smooth_buffer" const val K_AUTO_WAKE = "auto_wake_enabled" const val K_RUMBLE_ON_PHONE = "rumble_on_phone" + const val K_GYRO_ON_PHONE = "gyro_on_phone" const val K_SC2_CAPTURE = "sc2_capture" const val K_DS_CAPTURE = "ds_capture" const val K_PAD_HAPTICS = "pad_haptics" diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt index b150cca8..86bb00b9 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt @@ -77,6 +77,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat +import io.unom.punktfunk.kit.DeviceGyro import io.unom.punktfunk.kit.VideoDecoders import io.unom.punktfunk.kit.deviceBodyVibrator import io.unom.punktfunk.kit.security.KnownHostStore @@ -888,6 +889,18 @@ private fun ControllerSettings(s: Settings, update: (Settings) -> Unit, onOpenCo onCheckedChange = { on -> update(s.copy(rumbleOnPhone = on)) }, ) } + // The rumble mirror's sibling, data flowing the other way: needs a gyroscope to + // mirror FROM — a TV box has none, so the row would be a silent no-op there. + val hasGyroscope = remember { DeviceGyro.available(context) } + if (hasGyroscope) { + ToggleRow( + title = "Gyro from this phone", + subtitle = "When the controller has no gyro, send this phone's motion " + + "sensors as controller 1's", + checked = s.gyroOnPhone, + onCheckedChange = { on -> update(s.copy(gyroOnPhone = on)) }, + ) + } // NOT gated on the vibrator: SC2 passthrough is a USB/BLE capture that has nothing to do // with rumbling this device's body, and the gate hid the toggle on exactly the machines // that most want it — TV boxes, where a Steam Controller 2 is the whole input story. diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/StreamScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/StreamScreen.kt index 600804b7..517662b5 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/StreamScreen.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/StreamScreen.kt @@ -67,6 +67,7 @@ import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner +import io.unom.punktfunk.kit.DeviceGyro import io.unom.punktfunk.kit.DsCapture import io.unom.punktfunk.kit.GamepadFeedback import io.unom.punktfunk.kit.GamepadRouter @@ -455,6 +456,16 @@ fun StreamScreen(session: ActiveSession, onSessionEnded: (SessionEndReason) -> U router, deviceVibrator = if (initialSettings.rumbleOnPhone) deviceBodyVibrator(context) else null, ).also { it.start() } + // "Gyro from this phone" (opt-in): this device's IMU speaks for controller 1's motion + // while wire pad 0 is a controller without a gyro of its own — the rumble mirror's + // sibling, data flowing the other way. The mirror gates itself per sample (it stands + // down whenever a capture link — USB DualSense / SC2, pads with a real IMU — holds + // pad 0), so it composes with the captures below without coordination here. + val phoneGyro = if (initialSettings.gyroOnPhone && initialSettings.gamepadForwarding) { + DeviceGyro(context, handle, router).also { it.start() } + } else { + null + } // Free a disconnected controller's rumble/lights bindings promptly (else the open lights // session leaks until the session ends). The router owns hot-plug; the feedback owns the binds. router.onSlotClosed = feedback::onDeviceRemoved @@ -587,6 +598,7 @@ fun StreamScreen(session: ActiveSession, onSessionEnded: (SessionEndReason) -> U feedback.onHidRaw = null feedback.sink = null feedback.stop() // stop + join the poll threads BEFORE the router is released / handle freed + phoneGyro?.stop() // join the sensor thread + park pad 0's rotation at zero, same ordering rule sc2UsbReceiver?.let { runCatching { context.unregisterReceiver(it) } } sc2?.stop() // release the USB/BLE link + free the wire slot (host tears the pad down) dsUsbReceiver?.let { runCatching { context.unregisterReceiver(it) } } diff --git a/clients/android/app/src/test/kotlin/io/unom/punktfunk/GamepadPaletteTest.kt b/clients/android/app/src/test/kotlin/io/unom/punktfunk/GamepadPaletteTest.kt index df662de4..bff22ff0 100644 --- a/clients/android/app/src/test/kotlin/io/unom/punktfunk/GamepadPaletteTest.kt +++ b/clients/android/app/src/test/kotlin/io/unom/punktfunk/GamepadPaletteTest.kt @@ -123,7 +123,9 @@ class GamepadPaletteTest { */ @Test fun everySettingsRowHasATab() { - val rows = buildSettingsRows(Settings(), hasBodyVibrator = true, av1Capable = true) {} + val rows = buildSettingsRows( + Settings(), hasBodyVibrator = true, hasGyroscope = true, av1Capable = true, + ) {} assertTrue(rows.isNotEmpty()) assertEquals(rows.size, rows.map { it.id }.toSet().size) // Profiles is built separately (from the catalog), so no settings row claims it. @@ -137,7 +139,9 @@ class GamepadPaletteTest { @Test fun backgroundRowStepsTheSharedKey() { var s = Settings() - fun rows() = buildSettingsRows(s, hasBodyVibrator = false, av1Capable = false) { s = it } + fun rows() = buildSettingsRows( + s, hasBodyVibrator = false, hasGyroscope = false, av1Capable = false, + ) { s = it } fun palette() = rows().first { it.id == "palette" } assertEquals("violet", s.uiPalette) diff --git a/clients/android/app/src/test/kotlin/io/unom/punktfunk/GamepadSettingsRowsTest.kt b/clients/android/app/src/test/kotlin/io/unom/punktfunk/GamepadSettingsRowsTest.kt index b9012c0f..2f47864d 100644 --- a/clients/android/app/src/test/kotlin/io/unom/punktfunk/GamepadSettingsRowsTest.kt +++ b/clients/android/app/src/test/kotlin/io/unom/punktfunk/GamepadSettingsRowsTest.kt @@ -24,6 +24,7 @@ class GamepadSettingsRowsTest { ): List = buildSettingsRows( Settings(gamepadForwarding = forwarding), hasBodyVibrator = true, + hasGyroscope = true, av1Capable = true, ) { sink += it } diff --git a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/DeviceGyro.kt b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/DeviceGyro.kt new file mode 100644 index 00000000..6f19cb14 --- /dev/null +++ b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/DeviceGyro.kt @@ -0,0 +1,179 @@ +package io.unom.punktfunk.kit + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.view.Display +import android.view.Surface +import android.view.WindowManager +import kotlin.math.roundToInt + +/** + * The opt-in phone-gyro mirror ("Gyro from this phone", off by default): while wire pad 0 is a + * controller with no motion source of its own, THIS device's IMU speaks for it on the rich-input + * motion plane — for clip-on and third-party pads that ship without a gyro, where the phone body + * is rigidly attached to (or simply is) the thing in the player's hands. [GamepadFeedback]'s + * rumble-on-phone mirror with the data flowing the other way. + * + * On Android the only motion sources are the capture links (USB DualSense / SC2 — pads with a + * real IMU, claimed as [GamepadRouter.ExternalPad]s), so the stand-down rule is exactly + * [GamepadRouter.padHasOwnMotion]: when a capture link holds pad 0, the mirror sends nothing — + * two motion writers on one wire pad would fight. It also sends nothing while pad 0 has no slot + * at all (motion never creates a host pad; a controller must have arrived first). + * + * Two properties this class enforces itself: + * - samples ride a dedicated [HandlerThread] with batching disabled (`maxReportLatencyUs = 0`) — + * sensor batching would trade the exact latency gyro aim exists to avoid; + * - a stand-down edge (capture link claims pad 0, or [stop]) sends ONE zero-gyro sample, so the + * host's virtual pad never keeps integrating an angular velocity this device stopped + * producing (the gyro-sweep "stale angular velocity re-sent forever" failure mode). + * + * Units are the wire contract (mirrors `pf-client-core`'s constants): gyro rad/s → 20 LSB/°·s, + * accel m/s² → g → 10000 LSB/g. Android's accelerometer reads specific force (+1 g on the up + * axis at rest), which is the DualSense report's own convention — no sign flip. The one thing + * the phone adds is a frame remap: sensors report in the device's natural-portrait frame, while + * the wire wants the controller frame the player sees (x right, y up, z out of the screen), so + * each sample is rotated by the current display rotation — a phone clipped landscape must yaw + * when the player yaws, not roll. The matrix is derived and pinned by `DeviceGyroTest`; + * correctable in one place if on-glass says otherwise. + */ +class DeviceGyro( + context: Context, + private val handle: Long, + private val router: GamepadRouter, +) : SensorEventListener { + + private val sensorManager: SensorManager? = + context.getSystemService(SensorManager::class.java) + + /** For the live rotation; null on contexts without a display association (then portrait). */ + private val display: Display? = runCatching { + if (Build.VERSION.SDK_INT >= 30) { + context.display + } else { + @Suppress("DEPRECATION") + context.getSystemService(WindowManager::class.java)?.defaultDisplay + } + }.getOrNull() + + private val thread = HandlerThread("pf-phone-gyro") + + /** Latest converted accel, paired with each gyro send (the wire fuses both per sample). */ + private val lastAccel = intArrayOf(0, ACCEL_LSB_PER_G, 0) + + /** Whether the last gyro event actually went to pad 0 — the stand-down zero-send edge. */ + private var wasWriting = false + + /** Register the listeners; a device without a gyroscope makes this a no-op. */ + fun start() { + val sm = sensorManager ?: return + val gyro = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE) ?: return + thread.start() + val h = Handler(thread.looper) + // ~200 Hz requested (the framework clamps to what the hardware offers), zero report + // latency: batching is poison for gyro aim. + sm.registerListener(this, gyro, SAMPLING_PERIOD_US, 0, h) + sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.let { + sm.registerListener(this, it, SAMPLING_PERIOD_US, 0, h) + } + } + + /** + * Unregister and join the sensor thread, then park the host pad's rotation at zero if this + * mirror was the live writer. Call BEFORE the router is released / the handle freed — + * teardown-ordered like the feedback threads. + */ + fun stop() { + sensorManager?.unregisterListener(this) + thread.quitSafely() + runCatching { thread.join() } + if (wasWriting) { + wasWriting = false + sendZero() + } + } + + override fun onSensorChanged(event: SensorEvent) { + val rotation = display?.rotation ?: Surface.ROTATION_0 + when (event.sensor.type) { + Sensor.TYPE_ACCELEROMETER -> { + val v = remap(rotation, event.values[0], event.values[1], event.values[2]) + for (i in 0..2) { + lastAccel[i] = (v[i] / GRAVITY * ACCEL_LSB_PER_G) + .roundToInt().coerceIn(-32768, 32767) + } + } + Sensor.TYPE_GYROSCOPE -> { + // The write gate, per sample: pad 0 must exist (motion never creates a pad) + // and must not be a capture link's (its own IMU is streaming). + val write = router.padPresent(0) && !router.padHasOwnMotion(0) + if (!write) { + // Stand-down edge: never leave the last angular velocity latched host-side. + if (wasWriting) { + wasWriting = false + sendZero() + } + return + } + wasWriting = true + val v = remap(rotation, event.values[0], event.values[1], event.values[2]) + NativeBridge.nativeSendPadMotion( + handle, 0, + (v[0] * GYRO_LSB_PER_RAD_S).roundToInt().coerceIn(-32768, 32767), + (v[1] * GYRO_LSB_PER_RAD_S).roundToInt().coerceIn(-32768, 32767), + (v[2] * GYRO_LSB_PER_RAD_S).roundToInt().coerceIn(-32768, 32767), + lastAccel[0], lastAccel[1], lastAccel[2], + ) + } + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} + + /** Zero rotation, last-known accel — "at rest", not free-fall. */ + private fun sendZero() { + NativeBridge.nativeSendPadMotion( + handle, 0, 0, 0, 0, lastAccel[0], lastAccel[1], lastAccel[2], + ) + } + + companion object { + /** Whether this device can source motion at all — gates the settings rows (a TV box + * without an IMU would make the toggle a silent no-op, the rumble mirror's rule). */ + fun available(context: Context): Boolean = + context.getSystemService(SensorManager::class.java) + ?.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null + + /** ~200 Hz — between the sensor's usual FASTEST (~250-500 Hz) and GAME (~50 Hz). */ + private const val SAMPLING_PERIOD_US = 5000 + + /** The wire contract (pf-client-core `GYRO_LSB_PER_RAD_S`): 20 LSB/°·s from rad/s. */ + const val GYRO_LSB_PER_RAD_S = 20f * 180f / Math.PI.toFloat() + + /** The wire contract (pf-client-core `ACCEL_LSB_PER_G`). */ + const val ACCEL_LSB_PER_G = 10_000 + + /** pf-client-core's `G`. */ + const val GRAVITY = 9.80665f + + /** + * Rotate one device-frame vector (rotation rate or acceleration — both transform the + * same way under an in-plane rotation) into the controller frame for [rotation] + * ([Surface].ROTATION_*). Sensors report in the natural-portrait frame (+x right edge, + * +y top, +z out of the screen); the controller frame keeps +z (the screen always faces + * the player) and rotates x/y to mean "player's right" and "player's up". ROTATION_90 = + * the device physically turned counter-clockwise, top to the player's LEFT. + */ + fun remap(rotation: Int, x: Float, y: Float, z: Float): FloatArray = when (rotation) { + Surface.ROTATION_90 -> floatArrayOf(-y, x, z) // top left: right = bottom, up = +x + Surface.ROTATION_270 -> floatArrayOf(y, -x, z) // top right: right = top, up = −x + Surface.ROTATION_180 -> floatArrayOf(-x, -y, z) + else -> floatArrayOf(x, y, z) + } + } +} diff --git a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/GamepadRouter.kt b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/GamepadRouter.kt index 7fe1f02d..8eaa9cd2 100644 --- a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/GamepadRouter.kt +++ b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/GamepadRouter.kt @@ -320,6 +320,19 @@ class GamepadRouter( return null } + /** Whether ANY live slot currently holds wire pad [pad]. Read from the phone-gyro thread. */ + fun padPresent(pad: Int): Boolean = slots.values.any { it.index == pad } + + /** + * Whether wire pad [pad] is held by a capture-link slot ([ExternalPad] — USB DualSense / + * SC2), whose motion arrives from the pad's OWN IMU. The phone-gyro mirror stands down for + * those: two motion writers on one wire pad would fight. Synthetic ids are negative + * ([EXTERNAL_ID_BASE]); real [InputDevice] ids are positive. Read from the phone-gyro thread + * (the slot table is concurrent). + */ + fun padHasOwnMotion(pad: Int): Boolean = + slots.any { (id, slot) -> slot.index == pad && id < 0 } + /** * A capture-link pad occupying a wire slot without an Android [InputDevice] — the as-is Steam * Controller 2 passthrough (USB/BLE claimed directly, invisible to the input stack). Shares diff --git a/clients/android/kit/src/test/kotlin/io/unom/punktfunk/kit/DeviceGyroTest.kt b/clients/android/kit/src/test/kotlin/io/unom/punktfunk/kit/DeviceGyroTest.kt new file mode 100644 index 00000000..b7468a82 --- /dev/null +++ b/clients/android/kit/src/test/kotlin/io/unom/punktfunk/kit/DeviceGyroTest.kt @@ -0,0 +1,53 @@ +package io.unom.punktfunk.kit + +import android.view.Surface +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Pins the phone-gyro mirror's device→controller frame remap and its wire-unit constants + * ([DeviceGyro]). Pure JVM: [Surface]'s ROTATION_* are compile-time constants and remap is + * plain math. The matrix is derived (like the wire scale constants) — if on-glass says an axis + * is wrong, fix [DeviceGyro.remap] AND these expectations together. + * Run: `./gradlew :kit:testDebugUnitTest`. + */ +class DeviceGyroTest { + /** A distinct value per axis so a swapped or flipped component can't cancel out. */ + private fun remap(rotation: Int) = DeviceGyro.remap(rotation, 1f, 2f, 3f).toList() + + @Test + fun naturalPortraitIsIdentity() = assertEquals(listOf(1f, 2f, 3f), remap(Surface.ROTATION_0)) + + @Test + fun upsideDownFlipsInPlane() = assertEquals(listOf(-1f, -2f, 3f), remap(Surface.ROTATION_180)) + + /** ROTATION_90 = device turned counter-clockwise, top to the player's LEFT: + * player-right = device-bottom (−y), player-up = device-right (+x); z never changes. */ + @Test + fun rotation90TopLeft() = assertEquals(listOf(-2f, 1f, 3f), remap(Surface.ROTATION_90)) + + /** ROTATION_270 = top to the player's RIGHT: player-right = +y, player-up = −x. */ + @Test + fun rotation270TopRight() = assertEquals(listOf(2f, -1f, 3f), remap(Surface.ROTATION_270)) + + /** Every remap stays a proper (right-handed) rotation: x̂ × ŷ = ẑ after mapping. */ + @Test + fun handednessPreserved() { + for (r in listOf( + Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, Surface.ROTATION_270, + )) { + val x = DeviceGyro.remap(r, 1f, 0f, 0f) + val y = DeviceGyro.remap(r, 0f, 1f, 0f) + assertEquals("left-handed remap at rotation $r", 1f, x[0] * y[1] - x[1] * y[0], 0f) + } + } + + /** The wire contract, shared with pf-client-core / the Swift client: 20 LSB/°·s means + * 1 rad/s ⇒ ~1145.9 raw; 1 g ⇒ 10000 raw. */ + @Test + fun wireUnitConstants() { + assertEquals(20f * 180f / Math.PI.toFloat(), DeviceGyro.GYRO_LSB_PER_RAD_S, 0f) + assertEquals(1145.9156f, DeviceGyro.GYRO_LSB_PER_RAD_S, 0.001f) + assertEquals(10_000, DeviceGyro.ACCEL_LSB_PER_G) + } +} diff --git a/clients/apple/Sources/PunktfunkClient/ContentView.swift b/clients/apple/Sources/PunktfunkClient/ContentView.swift index 2b16327b..83357046 100644 --- a/clients/apple/Sources/PunktfunkClient/ContentView.swift +++ b/clients/apple/Sources/PunktfunkClient/ContentView.swift @@ -384,7 +384,13 @@ struct ContentView: View { .frame(minWidth: 940, minHeight: 620) } #else - .fullScreenCover(item: $libraryTarget) { host in + // iOS: the cover is the TOUCH UI's presentation only. In gamepad mode the library is one + // of GamepadHomeView's in-place layers (the console shell — no bottom-up cover), so the + // proxy hides the target from the cover while that mode owns it; every writer (Y on a + // tile, `returnToLibrary`) keeps writing the same `libraryTarget` either way, and a + // controller arriving or leaving mid-browse hands the open library to whichever + // presentation the new mode owns. + .fullScreenCover(item: touchLibraryTarget) { host in NavigationStack { LibraryView(store: store, host: host, onLaunch: { launchTitle(host, $0) }) } @@ -401,6 +407,14 @@ struct ContentView: View { Binding(get: { deepLinkNotice != nil }, set: { if !$0 { deepLinkNotice = nil } }) } + /// The iOS library cover's item: `libraryTarget`, hidden while the gamepad shell presents + /// the library in place (see the cover's comment). + private var touchLibraryTarget: Binding { + Binding( + get: { gamepadUIActive ? nil : libraryTarget }, + set: { libraryTarget = $0 }) + } + private var approvalChoicePresented: Binding { Binding(get: { approvalChoice != nil }, set: { if !$0 { approvalChoice = nil } }) } @@ -558,7 +572,8 @@ struct ContentView: View { GamepadHomeView( store: store, model: model, discovery: discovery, libraryTarget: $libraryTarget, waker: waker, - connect: { connect($0, profile: $1) }, connectDiscovered: connectDiscovered) + connect: { connect($0, profile: $1) }, connectDiscovered: connectDiscovered, + launchTitle: launchTitle) } else { HomeView( store: store, model: model, discovery: discovery, @@ -574,7 +589,8 @@ struct ContentView: View { GamepadHomeView( store: store, model: model, discovery: discovery, libraryTarget: $libraryTarget, waker: waker, - connect: { connect($0, profile: $1) }, connectDiscovered: connectDiscovered) + connect: { connect($0, profile: $1) }, connectDiscovered: connectDiscovered, + launchTitle: launchTitle) // On tvOS pairing/library normally present from HomeView's navigationDestinations // — which aren't mounted while the gamepad launcher is up. Give the launcher its // own presenters (exactly one of the two homes is mounted at a time, so these can diff --git a/clients/apple/Sources/PunktfunkClient/Home/ConnectOverlay.swift b/clients/apple/Sources/PunktfunkClient/Home/ConnectOverlay.swift index c3fe8b3e..e06561d7 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/ConnectOverlay.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/ConnectOverlay.swift @@ -83,6 +83,11 @@ struct ConnectOverlay: View { } } + /// The overlay's text/glyph colour: the palette's ink in the console takeover — over a pale + /// aurora, literal white was the one console surface that stayed white-on-white — and white + /// in the touch modal, whose branch is deliberately forced dark over a black scrim. + private var overlayFG: Color { gamepadUI ? ink.fg : .white } + @ViewBuilder private func content(_ phase: Phase) -> some View { // The takeover carries larger type than the compact modal. let titleSize: CGFloat = gamepadUI ? 24 : 19 @@ -90,21 +95,24 @@ struct ConnectOverlay: View { VStack(spacing: gamepadUI ? 16 : 14) { switch phase { case .connecting(let name): - ProgressView().controlSize(.large).tint(.white) + ProgressView().controlSize(.large).tint(overlayFG) Text("Connecting to \(name)") - .font(.geist(titleSize, .bold, relativeTo: .title3)).foregroundStyle(.white) + .font(.geist(titleSize, .bold, relativeTo: .title3)).foregroundStyle(overlayFG) .multilineTextAlignment(.center) Text("Establishing a secure connection…") - .font(.geist(bodySize, relativeTo: .caption)).foregroundStyle(.white.opacity(0.6)) + .font(.geist(bodySize, relativeTo: .caption)) + .foregroundStyle(overlayFG.opacity(0.6)) Button("Cancel") { onCancelConnect() }.buttonStyle(.bordered).padding(.top, 6) case .waking(let w) where w.timedOut: Image(systemName: "moon.zzz.fill") - .font(.system(size: gamepadUI ? 40 : 34)).foregroundStyle(.white.opacity(0.9)) + .font(.system(size: gamepadUI ? 40 : 34)) + .foregroundStyle(overlayFG.opacity(0.9)) Text("\(w.hostName) didn't wake") - .font(.geist(titleSize, .bold, relativeTo: .title3)).foregroundStyle(.white) + .font(.geist(titleSize, .bold, relativeTo: .title3)).foregroundStyle(overlayFG) .multilineTextAlignment(.center) Text("It may still be booting, or it's powered off / off this network.") - .font(.geist(bodySize, relativeTo: .caption)).foregroundStyle(.white.opacity(0.6)) + .font(.geist(bodySize, relativeTo: .caption)) + .foregroundStyle(overlayFG.opacity(0.6)) .multilineTextAlignment(.center) HStack(spacing: 12) { Button("Cancel") { waker.cancel() }.buttonStyle(.bordered) @@ -112,12 +120,13 @@ struct ConnectOverlay: View { } .padding(.top, 6) case .waking(let w): - ProgressView().controlSize(.large).tint(.white) + ProgressView().controlSize(.large).tint(overlayFG) Text("Waking \(w.hostName)…") - .font(.geist(titleSize, .bold, relativeTo: .title3)).foregroundStyle(.white) + .font(.geist(titleSize, .bold, relativeTo: .title3)).foregroundStyle(overlayFG) .multilineTextAlignment(.center) Text("Waiting for it to come online · \(w.seconds)s") - .font(.geistFixed(bodySize)).foregroundStyle(.white.opacity(0.6)).monospacedDigit() + .font(.geistFixed(bodySize)).foregroundStyle(overlayFG.opacity(0.6)) + .monospacedDigit() // A wake-only wait (no dial after) offers "Stop Waiting"; a wake-&-connect is "Cancel". Button(w.connectsAfter ? "Cancel" : "Stop Waiting") { waker.cancel() } .buttonStyle(.bordered).padding(.top, 6) diff --git a/clients/apple/Sources/PunktfunkClient/Home/GamepadAddHostView.swift b/clients/apple/Sources/PunktfunkClient/Home/GamepadAddHostView.swift index f5af6b36..b6b09ab0 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/GamepadAddHostView.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/GamepadAddHostView.swift @@ -14,7 +14,15 @@ import SwiftUI struct GamepadAddHostView: View { @Environment(\.gamepadInk) private var ink @Environment(\.dismiss) private var dismiss + @Environment(\.gamepadHostedInShell) private var hostedInShell let onAdd: (StoredHost) -> Void + /// How the in-place shell (iOS) closes this screen; nil (the macOS sheet, the tvOS cover) + /// falls back to the environment dismiss. Declared AFTER `onAdd` so the existing trailing- + /// closure call sites keep binding to it, not to this. + var close: (() -> Void)? + /// Whether this screen owns the controller — false while the shell is mid-transition or the + /// connect takeover is up (see GamepadSettingsView's twin). + var controllerActive = true #if os(iOS) /// `.compact` in a landscape phone window — tighter chrome so the keyboard tray still fits. @@ -36,8 +44,8 @@ struct GamepadAddHostView: View { items: rows, focusID: $focusID, onActivate: { activate(id: $0.id) }, - onBack: { dismiss() }, - isActive: editing == nil + onBack: { performClose() }, + isActive: controllerActive && editing == nil ) { row, focused in rowView(row, focused: focused) .frame(maxWidth: GamepadFormMetrics.rowMaxWidth) @@ -45,7 +53,8 @@ struct GamepadAddHostView: View { } .frame(maxWidth: .infinity) .safeAreaInset(edge: .top, spacing: 0) { - VStack(spacing: 4) { + VStack(alignment: .leading, spacing: gamepadHeaderSpacing(compact: compact)) { + // Leading, like every gamepad heading — and no close chrome (B is the exit). Text("Add Host") .font(.geist(gamepadTitleSize(compact: compact), .bold, relativeTo: .title)) .foregroundStyle(ink.fg) @@ -54,14 +63,14 @@ struct GamepadAddHostView: View { + "for everything else.") .font(.geist(GamepadFormMetrics.detailFont, relativeTo: .caption)) .foregroundStyle(ink.fg(0.55)) - .multilineTextAlignment(.center) - .frame(maxWidth: GamepadFormMetrics.rowMaxWidth * 0.72) + .multilineTextAlignment(.leading) + .frame(maxWidth: GamepadFormMetrics.rowMaxWidth * 0.72, alignment: .leading) } } + .padding(.horizontal, 24) .padding(.top, gamepadTitleTopPadding(compact: compact)) - .padding(.bottom, compact ? 4 : 8) - .frame(maxWidth: .infinity) - .overlay(alignment: .topTrailing) { closeButton.padding(.top, 20).padding(.trailing, 20) } + .padding(.bottom, gamepadTitleBottomPadding(compact: compact)) + .frame(maxWidth: .infinity, alignment: .leading) .background { GamepadTrayScrim(edge: .top) } } .safeAreaInset(edge: .bottom, spacing: 0) { @@ -73,7 +82,10 @@ struct GamepadAddHostView: View { .background { GamepadTrayScrim(edge: .bottom) } } // No aurora — the same clean Liquid-Glass-over-dark base as the gamepad settings screen. - .background { GamepadFormBackground() } + // Hosted in the shell, the field is the shell's (see GamepadSettingsView's twin). + .background { + if !hostedInShell { GamepadFormBackground() } + } // Publish the palette's ink to this screen (text, glass, accent, scrims) — a // pale palette flips all of them, and no leaf should have to read the setting. .gamepadPaletteInk() @@ -81,6 +93,18 @@ struct GamepadAddHostView: View { .onChange(of: port) { _, value in if value.count > 5 { port = String(value.prefix(5)) } } + #if !os(tvOS) + // The visible close ✕ is gone (a gamepad UI exits with B) — this keeps a hardware + // keyboard's Esc and the macOS sheet's cancel working without chrome. + .background { + Button("Cancel") { performClose() } + .keyboardShortcut(.cancelAction) + .buttonStyle(.plain) + .frame(width: 0, height: 0) + .opacity(0) + .accessibilityHidden(true) + } + #endif #if os(tvOS) // tvOS types with the SYSTEM fullscreen keyboard (TVTextEntry) instead of the custom // tray — the remote and the pad both drive it natively. Same `editing` state as the @@ -141,22 +165,10 @@ struct GamepadAddHostView: View { #endif } - /// Touch/click fallback for closing — the controller path is B, a hardware keyboard's Esc - /// rides the cancel action. - private var closeButton: some View { - Button { dismiss() } label: { - Image(systemName: "xmark") - .font(.system(size: GamepadFormMetrics.closeFont, weight: .semibold)) - .foregroundStyle(ink.fg) - .frame(width: GamepadFormMetrics.closeSide, height: GamepadFormMetrics.closeSide) - .glassBackground(Circle(), interactive: true) - .contentShape(Circle()) - } - .buttonStyle(.plain) - #if !os(tvOS) - .keyboardShortcut(.cancelAction) // unavailable on tvOS (Menu is the cancel there) - #endif - .accessibilityLabel("Cancel") + /// Close this screen through whichever mechanism presents it: the shell's layer pop on iOS, + /// the environment dismiss under a macOS sheet / tvOS cover. + private func performClose() { + if let close { close() } else { dismiss() } } // MARK: - Rows @@ -237,7 +249,7 @@ struct GamepadAddHostView: View { name: name.trimmingCharacters(in: .whitespaces), address: address.trimmingCharacters(in: .whitespaces), port: UInt16(port) ?? 9777)) - dismiss() + performClose() default: openKeyboard(id) } diff --git a/clients/apple/Sources/PunktfunkClient/Home/GamepadCarousel.swift b/clients/apple/Sources/PunktfunkClient/Home/GamepadCarousel.swift index 64369f6d..ab4e7d13 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/GamepadCarousel.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/GamepadCarousel.swift @@ -55,7 +55,14 @@ struct GamepadCarousel: View where Item.ID: Hash /// otherwise poll the SAME controller at once — driving both. The parent sets this false while /// something is presented on top so only the front-most carousel consumes the gamepad. var isActive: Bool = true - @ViewBuilder let card: (Item) -> Card + /// Whether the cards are worth showing off yet — the entrance holds until this is true. The + /// library passes "the first covers have their artwork" (see LibraryCoverflowView); anything + /// whose cards are ready the moment they mount leaves it alone. + var contentReady: Bool = true + /// Builds one card. The `CardEntrance` handed along is the card's share of the strip's + /// arrival, and the caller MUST apply it (`.modifier(entrance)`) *underneath* its own + /// `.scrollTransition` — see `CardEntrance` for why that placement is load-bearing. + @ViewBuilder let card: (Item, CardEntrance) -> Card @State private var input = GamepadMenuInput(manager: .shared) @State private var haptics = MenuHaptics(manager: .shared) @@ -83,6 +90,26 @@ struct GamepadCarousel: View where Item.ID: Hash /// confirm and end-stop events (moves trigger on `cursor`). @State private var activateTick = 0 @State private var boundaryTick = 0 + /// The strip's entrance, as ONE timeline: 0 = every card still away, 1 = every card landed + /// (see `CardEntrance`, which slices its own window out of this). Animated exactly once per + /// mount — a strip that re-played its entrance every time a screen popped off the top of it + /// would be noise, and the shell's push/pop carries that motion already. So it plays when a + /// screen is entered: the launcher when the gamepad UI comes up, the coverflow each time the + /// library opens (its layer mounts fresh). + /// + /// One animated Double rather than a Bool behind per-card `.animation(_:value:)` modifiers, + /// because those modifiers wrap the caller's card — INCLUDING its `.scrollTransition` — and a + /// delayed spring flipping while the scroll view was still settling captured the transition's + /// own per-frame phase updates, stranding the centred card in a half-receded state until the + /// next scroll re-drove it. Nothing here wraps the card in an animation at all. + @State private var entranceProgress: Double = 0 + /// Which card the entrance fans out from — the cursor as it stood when the strip was armed, + /// so a restored selection assembles around where the eye already is instead of sweeping in + /// from the left. + @State private var entranceAnchor = 0 + /// The entrance has been scheduled; it plays exactly once per mount. + @State private var entranceArmed = false + @Environment(\.accessibilityReduceMotion) private var reduceMotion /// Read-back from a touch drag is honoured only once the gamepad has been quiet this long /// (longer than a move animation, so overlapping held-stick moves never let it through). @@ -94,24 +121,27 @@ struct GamepadCarousel: View where Item.ID: Hash ScrollViewReader { proxy in ScrollView(.horizontal) { HStack(spacing: spacing) { - ForEach(items) { item in + // Enumerated for the entrance stagger only — identity stays `item.id`, + // which is what `.scrollTargetLayout()` and `scrollPosition` key on. + ForEach(Array(items.enumerated()), id: \.element.id) { idx, item in #if os(tvOS) // A focusable Button per card: the focus engine does the navigating // (remote swipes and pad dpad alike), select activates. The bare style // below keeps the tile's own look — the `.scrollTransition` center pop // is the focus treatment, since focus and center track each other. Button { activate(item) } label: { - card(item) + card(item, entrance(idx)) .frame(width: itemWidth) } .buttonStyle(ConsoleBareButtonStyle()) .focused($focusedID, equals: item.id) .id(item.id) #else - card(item) + card(item, entrance(idx)) .frame(width: itemWidth) .contentShape(Rectangle()) .onTapGesture { tap(item) } + .id(item.id) // explicit scroll-target identity for scrollPosition #endif } } @@ -165,7 +195,10 @@ struct GamepadCarousel: View where Item.ID: Hash reconcile() wire() if isActive { input.start() } + armEntrance() } + // The cards became worth showing (the library's covers got their art) — play now. + .onChange(of: contentReady) { _, _ in armEntrance() } .onDisappear { input.stop() haptics.stop() @@ -200,9 +233,55 @@ struct GamepadCarousel: View where Item.ID: Hash .onChange(of: items.map(\.id)) { _, _ in reconcile() wire() + // A strip that mounted empty (its content arrived after) still gets its entrance. + armEntrance() } } + // MARK: - Entrance + + /// Run the entrance, once, as soon as the strip is mounted AND its cards are worth showing. + /// + /// Deferred one runloop turn ON PURPOSE: a state change made inside `onAppear` lands in the + /// same transaction as the view's insertion, where SwiftUI runs with animations disabled — so + /// the cards would simply BE there. Note the failure mode is benign either way: progress + /// reaching 1 without animating leaves every card at exact identity, never stranded. + private func armEntrance() { + guard !entranceArmed, contentReady, !items.isEmpty else { return } + entranceArmed = true + // After `reconcile`, so the fan-out anchors on the seeded/restored cursor. + entranceAnchor = cursor + // Not just the next runloop turn (a change made inside `onAppear` lands in the + // insertion's transaction, where animations are disabled) but a couple of frames: the + // GeometryReader's first pass can report no width at all, so the strip has to lay out + // for real and the scroll view has to centre itself on the cursor before this starts. + // Cards are invisible until then (progress 0 ⇒ opacity 0), so the wait never shows. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + // Linear on purpose: the master timeline is a clock, and each card eases its OWN + // slice of it (see `CardEntrance`) — a spring here would warp every card's curve. + withAnimation( + reduceMotion ? .easeOut(duration: 0.28) : .linear(duration: CardEntrance.total) + ) { + entranceProgress = 1 + } + } + } + + /// The card's share of the strip's entrance: it swings in on the drum, the anchored card + /// landing first and its neighbours fanning outward to either side. + private func entrance(_ idx: Int) -> CardEntrance { + // Capped so a several-hundred-title library never queues a card behind a visibly long + // wait — everything past the cap lands together, well off-screen anyway. + let delay = min(CardEntrance.maxDelay, Double(abs(idx - entranceAnchor)) * 0.07) + return CardEntrance( + progress: entranceProgress, + start: delay / CardEntrance.total, + // Never zero: the anchor is the card the eye is ON, so it must swing like the rest — + // giving it "no rotation" left the one card you actually watch merely sliding up. + side: idx < entranceAnchor ? -1 : 1, + reduceMotion: reduceMotion) + } + // MARK: - Input wiring private func wire() { @@ -346,4 +425,87 @@ struct GamepadCarousel: View where Item.ID: Hash withAnimation(.spring(response: 0.34, dampingFraction: 0.7).delay(0.1)) { bumpOffset = 0 } } } + +/// How a card arrives when its strip does: turned away on the drum, small, low and invisible — +/// then it swings flat, grows and rises into place on a spring soft enough to overshoot. Cards to +/// the left of the anchor hinge on their trailing edge and cards to its right on their leading +/// one, so the strip FANS OPEN from the cursor rather than sweeping past it; the anchor card +/// itself only grows, since it is already facing you. Each card carries its own delay (see +/// `entrance(_:)`) — that stagger is what makes the strip read as one gesture instead of a +/// simultaneous flash, and it is the same hinge/perspective language the coverflow's own recede +/// speaks, so the arrival and the scrolling feel like one object. +/// +/// ⚠️ APPLY THIS UNDERNEATH THE CARD'S OWN `.scrollTransition`, never around it. A scroll +/// transition derives its phase from the geometry of the view it wraps, so an entrance layered +/// on the OUTSIDE moves the very thing the transition is measuring: every card read as far from +/// centre for the whole travel, its phase pinned at fully-receded, and the centred card only +/// collapsed into its focused look as the entrance ended — arriving as a jump. Underneath, the +/// transition measures a card that never moves and simply composes its own scale/rotation on top. +/// +/// Transforms only — nothing here touches layout, so the scroll view's snapping and the tvOS +/// focus engine are untouched either. Reduce Motion drops every bit of travel for a plain, +/// unstaggered cross-fade. +struct CardEntrance: ViewModifier, Animatable { + /// How long ONE card takes to travel, and the most any card waits before it starts. + static let perCard: Double = 0.6 + static let maxDelay: Double = 0.42 + /// The master timeline the carousel animates 0 → 1. + static var total: Double { perCard + maxDelay } + + /// The interpolated master progress. `Animatable` is the whole point: SwiftUI hands this + /// modifier a fresh value every frame and re-runs `body`, so the card's transforms are a pure + /// FUNCTION of the clock. No `.animation` modifier wraps the card, so nothing here can catch + /// the caller's `.scrollTransition` mid-scroll and strand it. + var progress: Double + /// Where this card's window opens on that timeline, 0…1. + let start: Double + /// Which way the card swings in: -1 hinged on its trailing edge (it sits left of the anchor), + /// +1 hinged on its leading edge (right of it). Never 0 — every card turns, including the + /// centred one. + let side: Double + let reduceMotion: Bool + + var animatableData: Double { + get { progress } + set { progress = newValue } + } + + func body(content: Content) -> some View { + // This card's own 0…1, sliced out of the master clock. + let span = Self.perCard / Self.total + let raw = min(max((progress - start) / span, 0), 1) + // The travel eases out with a whisker of overshoot, so a card settles rather than stops. + let travel = Self.easeOutBack(raw) + // The fade is FAR quicker than the travel — it finishes in the first third of the window. + // Sharing one curve meant the card spent its whole swing at near-zero opacity and only + // the last few degrees ever showed, which is why this read as a small slide. + let fade = Self.easeOut(min(raw / 0.34, 1)) + // Deep turn, well down, well shrunk — the card is genuinely edge-on and travelling. The + // sign matches the coverflow's own recede (right of centre turns negative about its + // leading edge), so the arrival deepens the turn the card wears at rest and unwinds into + // it instead of swinging the opposite way. + let away = reduceMotion ? 0 : 1 - travel + return content + .opacity(reduceMotion ? raw : fade) + .scaleEffect(1 - 0.26 * away) + .rotation3DEffect( + .degrees(side * -64 * away), + axis: (x: 0, y: 1, z: 0), + anchor: .center, + perspective: 0.65) + .offset(y: 34 * away) + } + + /// `1 - (1-t)³`, with a small overshoot past 1 before it settles. + private static func easeOutBack(_ t: Double) -> Double { + let c1 = 1.2, c3 = c1 + 1 + let u = t - 1 + return 1 + c3 * u * u * u + c1 * u * u + } + + private static func easeOut(_ t: Double) -> Double { + let u = 1 - t + return 1 - u * u * u + } +} #endif diff --git a/clients/apple/Sources/PunktfunkClient/Home/GamepadChrome.swift b/clients/apple/Sources/PunktfunkClient/Home/GamepadChrome.swift index f0fb8fc4..b35097c0 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/GamepadChrome.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/GamepadChrome.swift @@ -23,22 +23,47 @@ func buttonGlyph( /// Top padding for a gamepad screen's pinned title. macOS gets extra clearance — the launcher /// title sits right under the window titlebar and the settings/add-host sheets have no titlebar -/// at all, so the iOS value hugs the top edge there. +/// at all. The other values follow the console shell's rhythm (title top = 18 design units, +/// k-floored to 10 for a landscape phone): the title needs air to the screen edge or the whole +/// header reads pressed against the bezel, which the tab strip's extra band made obvious. func gamepadTitleTopPadding(compact: Bool) -> CGFloat { #if os(macOS) 26 + #elseif os(tvOS) + 24 #else - compact ? 4 : 10 + compact ? 18 : 28 + #endif +} + +/// Padding under a gamepad screen's pinned header block (title, and the tab strip where there is +/// one) before the content: the console leaves ~14 units of air under its tab pills, and without +/// it the first row sits shoulder-to-shoulder with the header. +func gamepadTitleBottomPadding(compact: Bool) -> CGFloat { + #if os(tvOS) + 16 + #else + compact ? 8 : 12 + #endif +} + +/// Spacing between a header's stacked elements (title over tab strip / subtitle). +func gamepadHeaderSpacing(compact: Bool) -> CGFloat { + #if os(tvOS) + 13 + #else + compact ? 6 : 10 #endif } /// Point size for a gamepad screen's pinned title: TV-large on tvOS (read from the couch), the -/// in-hand compact-aware sizes elsewhere. +/// in-hand compact-aware sizes elsewhere. Sized as a proper screen heading — the field verdict +/// on the smaller first cut was "way too small" once the title moved off-centre. func gamepadTitleSize(compact: Bool) -> CGFloat { #if os(tvOS) 44 #else - compact ? 20 : 30 + compact ? 24 : 34 #endif } @@ -58,8 +83,7 @@ enum GamepadFormMetrics { static let rowCorner: CGFloat = 18 static let rowMaxWidth: CGFloat = 920 static let detailFont: CGFloat = 19 - static let closeFont: CGFloat = 20 - static let closeSide: CGFloat = 48 + static let bandWidth: CGFloat = 380 #else static let headerFont: CGFloat = 12 static let labelFont: CGFloat = 16 @@ -72,8 +96,8 @@ enum GamepadFormMetrics { static let rowCorner: CGFloat = 14 static let rowMaxWidth: CGFloat = 620 static let detailFont: CGFloat = 13 - static let closeFont: CGFloat = 14 - static let closeSide: CGFloat = 34 + /// The option band's (GamepadOptionBand) fixed stage inside a choice row. + static let bandWidth: CGFloat = 240 #endif } @@ -147,8 +171,21 @@ struct GamepadHintBar: View { /// header). Honors Reduce Motion by freezing the field at a fixed phase. struct GamepadScreenBackground: View { @Environment(\.gamepadInk) private var ink - /// Quiet the field for a form screen (see the type comment). - var calm = false + /// How far toward the form screens' quiet the field sits: 0 = the launcher's full aurora, + /// 1 = calm, fractional mid-chase. Continuous (not a Bool) so the in-place shell can CHASE + /// it during a push/pop — the console does the same with its `bg_mix` — and every + /// calm-dependent factor below rides an `.opacity` modifier, which animates reliably where + /// re-built gradient stops do not. + var calmMix: Double + + /// The Bool spelling every non-shell call site uses (see the type comment for `calm`). + init(calm: Bool = false) { + calmMix = calm ? 1 : 0 + } + + init(calmMix: Double) { + self.calmMix = calmMix + } @Environment(\.accessibilityReduceMotion) private var reduceMotion @AppStorage(DefaultsKey.uiPalette) private var paletteID = "violet" @@ -184,21 +221,22 @@ struct GamepadScreenBackground: View { // ±8° over ~5 min — the whole field very slowly warms and cools. .hueRotation(.degrees(sin(t * 0.021) * 8)) // Calm = col·0.6 + ground·0.4: over the ground, `.opacity` IS the multiply… - .opacity(calm ? 0.6 : 1) - if calm { - // …and a plusLighter wash of the palette's own ground IS the add. Chosen so the - // ground lands exactly where it was and the bright pools come down to meet it. - Self.color(palette.ground) - .opacity(0.4) - .blendMode(.plusLighter) - } + .opacity(1 - 0.4 * calmMix) + // …and a plusLighter wash of the palette's own ground IS the add. Chosen so the + // ground lands exactly where it was and the bright pools come down to meet it. + // Mounted unconditionally — at opacity 0 a plusLighter layer contributes nothing, + // and an always-present layer is what lets the mix animate instead of popping. + Self.color(palette.ground) + .opacity(0.4 * calmMix) + .blendMode(.plusLighter) // Cinematic vignette: the edges settle toward the scrim so the cards sit in the // pooled light. Soft (extends past the frame) so the corners deepen rather than // crush. Halved under calm: a launcher's cards sit in the pooled centre, but a form // screen's rows run out toward the edges, where crushing them just eats the list. EllipticalGradient( - colors: [.clear, scrim.opacity((calm ? 0.21 : 0.42) * strength)], + colors: [.clear, scrim.opacity(0.42 * strength)], center: .center, startRadiusFraction: 0.25, endRadiusFraction: 1.15) + .opacity(1 - 0.5 * calmMix) // Legibility grounding for the pinned title (top) and hint pill (bottom). This one // works on the field itself (it's the backdrop's bottom layer — nothing behind it to // blur), so it stays a gradient, just a light one. @@ -342,20 +380,39 @@ struct GamepadTrayScrim: View { // to keep the pinned title legible, so it has to frost dark under white ink and // light under dark ink. .environment(\.colorScheme, ink.isLight ? .light : .dark) - // Fade the whole blur out toward the content so it dissolves rather than ending on a line. + // Sink the material's grey luminance lift toward the palette's shade (black on a + // dark field — field ask: the frost read GREY over the aurora). Inside the mask, so + // the tint dissolves with the blur. + .overlay(ink.shade(0.35)) + // Fade the whole blur out toward the content so it dissolves rather than ending on a + // line. The strong region sits deep (0.65) because the first stretch of the gradient + // now runs over the fixed 80 pt outer overhang below. .mask { LinearGradient( stops: [ .init(color: .black, location: 0), - .init(color: .black.opacity(0.9), location: 0.5), + .init(color: .black.opacity(0.92), location: 0.65), .init(color: .clear, location: 1), ], startPoint: fromEdge, endPoint: toContent) } // Grow past the tray so the fade-to-clear happens OUTSIDE its bounds — the tray's own - // text always sits on the strong part, rows blur out before they reach it. - .padding(edge == .top ? .bottom : .top, -32) - .ignoresSafeArea() + // text always sits on the strong part, rows blur out before they reach it. The bottom + // gets the longer runway: its tray sits over SCROLLING rows plus the detail line, and + // the field verdict on the short reach was rows colliding visibly with the legend. + .padding(edge == .top ? .bottom : .top, edge == .top ? -44 : -72) + // Full-bleed by LAYOUT, not by `.ignoresSafeArea()`: safe-area expansion resolves a + // beat after insertion (outside any geometry group and outside this view's own + // transaction), which is exactly the pop the field kept seeing — vertically first, + // then, once the vertical runway became padding, on the X axis alone (the landscape + // side insets). 80 pt clears every inset on every device; backgrounds never clip, + // so the overhang simply draws. + .padding(edge == .top ? .top : .bottom, -80) + .padding(.horizontal, -80) + // And the shape must NEVER animate: mounted inside a pushed shell layer, any late + // geometry would ride the push's transaction and visibly grow into place. The + // layer's own fade/slide still carries the scrim; only its SHAPE is pinned. + .transaction { $0.animation = nil } } } diff --git a/clients/apple/Sources/PunktfunkClient/Home/GamepadHomeView.swift b/clients/apple/Sources/PunktfunkClient/Home/GamepadHomeView.swift index 030ce2dd..94d3bab4 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/GamepadHomeView.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/GamepadHomeView.swift @@ -74,6 +74,9 @@ struct GamepadHomeView: View { @ObservedObject var waker: HostWaker let connect: (StoredHost, ProfileSelection) -> Void let connectDiscovered: (DiscoveredHost) -> Void + /// Launch a library title on a host — the in-place library layer's activate path (iOS; the + /// cover/sheet presentations wire ContentView's `launchTitle` into LibraryView themselves). + let launchTitle: (StoredHost, String) -> Void /// The profile catalog — pinned host+profile combos render as their own tiles here, which is /// how a controller picks a profile: one focus-and-press instead of a menu (design §5.4). @@ -93,29 +96,56 @@ struct GamepadHomeView: View { private let compact = false // no size classes on macOS; the window minimum keeps room #endif @ObservedObject private var gamepads = GamepadManager.shared + @Environment(\.accessibilityReduceMotion) private var reduceMotion @State private var selection: GamepadHomeTarget? @State private var showSettings = false @State private var showAddHost = false + /// The console's input drop: true for the transition's 0.26 s, during which NO layer polls + /// the controller — a double-tapped A can't push two screens, and the held button that + /// caused the change is long released before the next poller starts (whose own + /// `needsSnapshot` seed swallows it if not). + @State private var transitioning = false + /// Guards the gate's release against an interrupted transition: only the newest hold clears. + @State private var transitionEpoch = 0 var body: some View { - GeometryReader { geo in - hero(for: geo.size) + // The in-place shell (see GamepadShell.swift): the launcher is the base layer, the + // current sub-screen a transparent layer over it, both over ONE persistent backdrop + // that never unmounts — a push slides the screen up out of a fade while the launcher + // recedes underneath, the console's own choreography. On macOS/tvOS `topScreen` is + // constantly nil and this ZStack degenerates to the plain launcher, presented over by + // the sheets/covers below exactly as before. + ZStack { + homeLayer + .opacity(covered ? 0 : 1) + .scaleEffect(covered ? GamepadShellMotion.underScale : 1) + // The covers used to swallow touch; the recessed layer must too. + .allowsHitTesting(!covered) + #if os(iOS) + if let screen = topScreen { + screenLayer(screen) + // Settle the screen's internal layout before the insertion animates, so + // descendants never lerp from a half-resolved first frame. (Not sufficient + // for the tray blurs on its own — safe-area expansion resolves outside a + // geometry group; GamepadTrayScrim pins its own geometry too.) + .geometryGroup() + .zIndex(1) + .id(screen.id) + .transition(.gamepadScreen(slide: GamepadShellMotion.slide(compact: compact))) + } + #endif } - // Pinned inside the safe area, out of the carousel's vertical budget — never clipped. - .safeAreaInset(edge: .top, spacing: 0) { - titleBar - .padding(.top, gamepadTitleTopPadding(compact: compact)) - .padding(.bottom, compact ? 4 : 8) + // Value-keyed rather than `withAnimation` at the triggers: pushes originate outside + // this view too (`model.returnToLibrary` writes `libraryTarget`), and keying on the + // derived id catches every writer. Reduce Motion snaps. + .animation(reduceMotion ? nil : GamepadShellMotion.screen, value: topScreenID) + // ONE living field for every layer, still a `.background` (the layout rule in this + // file's header). Its calm is CHASED between the launcher's aurora and the form + // screens' quiet, never crossfaded per screen — the console's `bg_mix`. + .background { + GamepadScreenBackground(calmMix: calmTarget) + .animation(reduceMotion ? nil : GamepadShellMotion.calm, value: calmTarget) } - .safeAreaInset(edge: .bottom, alignment: .leading, spacing: 0) { - GamepadHintBar(hints: hints) - // Equal distance from the left and bottom edges — the pill's corner inset was the - // real asymmetry (leading 22 vs bottom 10), not its internal padding. - .padding(.leading, compact ? 12 : 18) - .padding(.bottom, compact ? 12 : 18) - .padding(.top, compact ? 4 : 8) - } - .background { GamepadScreenBackground() } // Publish the palette's ink to this screen (text, glass, accent, scrims) — a // pale palette flips all of them, and no leaf should have to read the setting. .gamepadPaletteInk() @@ -129,6 +159,17 @@ struct GamepadHomeView: View { try? await Task.sleep(for: .seconds(10)) } } + #if os(iOS) + .onChange(of: topScreenID) { _, _ in + transitionEpoch += 1 + let epoch = transitionEpoch + transitioning = true + let hold = reduceMotion ? 0.05 : GamepadShellMotion.duration + 0.02 + DispatchQueue.main.asyncAfter(deadline: .now() + hold) { + if epoch == transitionEpoch { transitioning = false } + } + } + #endif // The remote's Play/Pause mirrors the pad's X (Settings): the focus engine never surfaces // X, and historically tvOS maps a pad's X to this same press — the poll and this command // double-firing just sets the same Bool twice. @@ -136,8 +177,9 @@ struct GamepadHomeView: View { .onPlayPauseCommand { showSettings = true } #endif // The settings / add-host screens take over the controller (the carousel's `isActive` - // gate above). iOS presents them full screen — the immersive console feel; macOS has no - // fullScreenCover, so they become generously sized sheets over the dimmed launcher. + // gate above). macOS has no fullScreenCover — they are generously sized sheets over the + // dimmed launcher; tvOS keeps its focus-engine covers. iOS needs nothing here: the + // shell's layers above ARE the presentation. #if os(macOS) .sheet(isPresented: $showSettings) { GamepadSettingsView(store: store) @@ -148,7 +190,7 @@ struct GamepadHomeView: View { .frame(width: 660, height: 620) } .frame(minWidth: 640, minHeight: 420) - #else + #elseif os(tvOS) .fullScreenCover(isPresented: $showSettings) { GamepadSettingsView(store: store) } .fullScreenCover(isPresented: $showAddHost) { GamepadAddHostView { store.add($0) } @@ -156,6 +198,110 @@ struct GamepadHomeView: View { #endif } + // MARK: - The shell's layers (see GamepadShell.swift) + + /// The launcher itself — everything the pre-shell body was, minus the backdrop (hoisted to + /// the shell) and the presentation modifiers (below). + private var homeLayer: some View { + GeometryReader { geo in + hero(for: geo.size) + } + // Pinned inside the safe area, out of the carousel's vertical budget — never clipped. + .safeAreaInset(edge: .top, spacing: 0) { + titleBar + .padding(.top, gamepadTitleTopPadding(compact: compact)) + .padding(.bottom, gamepadTitleBottomPadding(compact: compact)) + } + .safeAreaInset(edge: .bottom, alignment: .leading, spacing: 0) { + GamepadHintBar(hints: hints) + // Equal distance from the left and bottom edges — the pill's corner inset was the + // real asymmetry (leading 22 vs bottom 10), not its internal padding. + .padding(.leading, compact ? 12 : 18) + .padding(.bottom, compact ? 12 : 18) + .padding(.top, compact ? 4 : 8) + } + } + + #if os(iOS) + /// The screen the shell shows over the launcher — derived from the same triggers every + /// platform sets, so `returnToLibrary`, the tiles, X and Y all keep writing what they wrote. + private var topScreen: GamepadScreen? { + if showSettings { return .settings } + if showAddHost { return .addHost } + if let host = libraryTarget { return .library(host) } + return nil + } + + @ViewBuilder private func screenLayer(_ screen: GamepadScreen) -> some View { + // The layer owns the controller only once the push settles and nothing rides over the + // shell (the connect/wake takeover is an overlay in ContentView, above these layers). + let active = !transitioning && waker.waking == nil && model.phase != .connecting + Group { + switch screen { + case .settings: + GamepadSettingsView( + store: store, + close: { if !transitioning { showSettings = false } }, + controllerActive: active) + case .addHost: + GamepadAddHostView( + onAdd: { store.add($0) }, + close: { if !transitioning { showAddHost = false } }, + controllerActive: active) + case .library(let host): + GamepadLibraryScreen( + store: store, host: host, + onLaunch: { launchTitle(host, $0) }, + close: { if !transitioning { libraryTarget = nil } }, + controllerActive: active) + } + } + .environment(\.gamepadHostedInShell, true) + } + #endif + + private var covered: Bool { + #if os(iOS) + topScreen != nil + #else + false + #endif + } + + private var topScreenID: String? { + #if os(iOS) + topScreen?.id + #else + nil + #endif + } + + /// The backdrop's calm target: 1 under a form screen, 0 under the launcher/library. The + /// macOS sheets / tvOS covers mount their own calmed field, so the launcher behind them + /// keeps its aurora — exactly what shipped. + private var calmTarget: Double { + #if os(iOS) + topScreen?.isForm == true ? 1 : 0 + #else + 0 + #endif + } + + /// Stop consuming the controller while another screen (or the connect/wake takeover) is on + /// top — otherwise the launcher navigates behind it (invisibly on iPhone, visibly on iPad), + /// and a second A during a dial would launch a concurrent connect. `.connecting` covers the + /// takeover's Connecting phase; `waker.waking` its Waking phase. On iOS the shell adds the + /// transition's input drop, during which NOBODY polls. + private var homeOwnsController: Bool { + #if os(iOS) + topScreen == nil && !transitioning + && waker.waking == nil && model.phase != .connecting + #else + libraryTarget == nil && !showSettings && !showAddHost + && waker.waking == nil && model.phase != .connecting + #endif + } + // MARK: - Hero (carousel + detail), sized to fit the space between the pinned title and hints @ViewBuilder private func hero(for size: CGSize) -> some View { @@ -181,32 +327,27 @@ struct GamepadHomeView: View { // MARK: - Chrome private var titleBar: some View { - // The chip used to be a trailing `.overlay`, which reserves no width: on a portrait phone - // it sat directly on top of the centred title ("Select a Host" ran straight into the pad - // name). Laying it out as a row with a hidden mirror on the leading side keeps the title - // optically centred AND clear of the chip at every width; the title shrinks a little - // before it would ever truncate. + // Leading title (a console heading, not a floating label — field ask), chip trailing. + // The old hidden-mirror trick existed only to keep a CENTRED title clear of the chip; + // a leading title needs none of it — the flexible frame keeps the two apart, and the + // title shrinks a little before it would ever truncate. HStack(spacing: 12) { - statusChip(hidden: true) Text("Select a Host") .font(.geist(gamepadTitleSize(compact: compact), .bold, relativeTo: .title)) .foregroundStyle(ink.fg) .lineLimit(1) .minimumScaleFactor(0.75) - .frame(maxWidth: .infinity) - statusChip(hidden: false) + .frame(maxWidth: .infinity, alignment: .leading) + statusChip } - .padding(.horizontal, 20) + .padding(.horizontal, 24) } /// Which pad is driving this UI (name + battery) — quiet, and only where there's room; a - /// compact-height phone gives the pixels to the carousel instead. `hidden` renders the same - /// chip purely as a width reserve. - @ViewBuilder private func statusChip(hidden: Bool) -> some View { + /// compact-height phone gives the pixels to the carousel instead. + @ViewBuilder private var statusChip: some View { if !compact, let active = gamepads.active { ControllerStatusChip(controller: active) - .opacity(hidden ? 0 : 1) - .accessibilityHidden(hidden) } } @@ -229,14 +370,9 @@ struct GamepadHomeView: View { onActivate: { $0.activate() }, onSecondary: { openLibraryForSelected() }, onTertiary: { showSettings = true }, - // Stop consuming the controller while another screen (or the connect/wake takeover) is on - // top — otherwise the launcher navigates behind it (invisibly on iPhone, visibly on iPad), - // and a second A during a dial would launch a concurrent connect. `.connecting` covers the - // takeover's Connecting phase; `waker.waking` covers its Waking phase. - isActive: libraryTarget == nil && !showSettings && !showAddHost - && waker.waking == nil && model.phase != .connecting - ) { tile in - hostCard(tile, size: CGSize(width: cardWidth, height: cardHeight)) + isActive: homeOwnsController + ) { tile, entrance in + hostCard(tile, size: CGSize(width: cardWidth, height: cardHeight), entrance: entrance) } .frame(height: cardHeight + 40) } @@ -245,8 +381,12 @@ struct GamepadHomeView: View { /// per-frame `phase` (real distance-from-centered), so the look always matches what's on screen /// mid-scroll. `.shadow`/`.overlay` aren't part of `VisualEffect`, so the focus pop is scale + /// brightness/saturation + a depth blur on the recessed neighbors. - private func hostCard(_ tile: HomeTile, size: CGSize) -> some View { + private func hostCard( + _ tile: HomeTile, size: CGSize, entrance: CardEntrance + ) -> some View { GamepadHostTile(tile: tile, size: size) + // Beneath the scroll transition, never around it — see CardEntrance. + .modifier(entrance) .scrollTransition { content, phase in let d = CGFloat(min(abs(phase.value), 1)) let scale = 1 - d * 0.12 @@ -402,10 +542,15 @@ private struct GamepadHostTile: View { .foregroundStyle(ink.fg(0.5)) } if tile.isOnline { + // Status colours stay palette-independent (a pip must not change meaning + // with the wallpaper) — only the glow softens on a pale field, where it + // reads as a smudge at full strength. Circle() - .fill(Color.green) + .fill(GamepadInk.onlineGreen) .frame(width: Self.pipSide, height: Self.pipSide) - .shadow(color: .green.opacity(0.7), radius: 5) + .shadow( + color: GamepadInk.onlineGreen.opacity(ink.isLight ? 0.45 : 0.7), + radius: 5) } } } @@ -441,7 +586,7 @@ private struct GamepadHostTile: View { startPoint: .top, endPoint: .bottom), style: StrokeStyle(lineWidth: 1, dash: tile.filled ? [] : [6, 5])) } - .shadow(color: .black.opacity(0.45), radius: 20, y: 14) + .shadow(color: ink.shadow(0.45), radius: 20, y: 14) } private var monogramBadge: some View { diff --git a/clients/apple/Sources/PunktfunkClient/Home/GamepadInk.swift b/clients/apple/Sources/PunktfunkClient/Home/GamepadInk.swift index fbb74b34..852e3a9c 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/GamepadInk.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/GamepadInk.swift @@ -37,6 +37,12 @@ struct GamepadInk: Equatable, Sendable { func accent(_ alpha: Double) -> Color { accent.opacity(alpha) } /// A wash under text: `alpha` is the dark-field strength, scaled for a pale one. func shade(_ alpha: Double) -> Color { shade.opacity(alpha * shadeScale) } + /// The glass base at `alpha` — what a surface's material is washed with so it carries the + /// palette's hue (the console fills its panels with exactly this colour). + func glass(_ alpha: Double) -> Color { glass.opacity(alpha) } + /// A drop shadow: always black — a white shadow is not a shadow — but softened on a pale + /// field, where full-strength black under every card reads as a smear rather than depth. + func shadow(_ alpha: Double) -> Color { .black.opacity(alpha * (isLight ? 0.4 : 1)) } static func of(_ p: GamepadPalette) -> GamepadInk { let accent = Color(red: p.accent.x, green: p.accent.y, blue: p.accent.z) @@ -60,6 +66,10 @@ struct GamepadInk: Equatable, Sendable { /// The shipped dark look — what a preview or a test composition gets. static let dark = GamepadInk.of(GamepadPalette.named("violet")) + + /// The online pip — deliberately NOT palette-derived: a status colour must not change + /// meaning with the wallpaper (the console's rule; this is its `ONLINE_GREEN` verbatim). + static let onlineGreen = Color(red: 0.20, green: 0.84, blue: 0.29) } private struct GamepadInkKey: EnvironmentKey { diff --git a/clients/apple/Sources/PunktfunkClient/Home/GamepadKeyboard.swift b/clients/apple/Sources/PunktfunkClient/Home/GamepadKeyboard.swift index 88965c6c..66d67ece 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/GamepadKeyboard.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/GamepadKeyboard.swift @@ -111,7 +111,9 @@ struct GamepadKeyboard: View { .font(.geist(15, .semibold, relativeTo: .callout)) } } - .foregroundStyle(focused ? Color.black : ink.fg) + // The focused keycap sits on `ink.accent`, so `onAccent` is what reads on it — a dark + // accent palette got black-on-dark with the old literal black. + .foregroundStyle(focused ? ink.onAccent : ink.fg) .frame(maxWidth: .infinity, minHeight: compact ? 34 : 42) .background { RoundedRectangle(cornerRadius: 9, style: .continuous) diff --git a/clients/apple/Sources/PunktfunkClient/Home/GamepadLibraryScreen.swift b/clients/apple/Sources/PunktfunkClient/Home/GamepadLibraryScreen.swift new file mode 100644 index 00000000..8402e890 --- /dev/null +++ b/clients/apple/Sources/PunktfunkClient/Home/GamepadLibraryScreen.swift @@ -0,0 +1,55 @@ +// The library as one of the gamepad shell's in-place layers (iOS): console chrome — a pinned +// title and a close ✕ styled like the settings screen's — around the shared LibraryView, whose +// gamepad branch renders the coverflow. The cover presentation used to get its title and Close +// from the wrapping NavigationStack's bar; a shell layer has no bar, so this restores both in +// the console's own grammar. Everything data-shaped (the fetch, the loading/error/empty states, +// the image session lifecycle) stays LibraryView's. + +import PunktfunkKit +import SwiftUI +#if os(iOS) + +struct GamepadLibraryScreen: View { + @Environment(\.gamepadInk) private var ink + @ObservedObject var store: HostStore + let host: StoredHost + let onLaunch: (String) -> Void + let close: () -> Void + var controllerActive = true + + /// `.compact` in a landscape phone window — tighter chrome, like every gamepad screen. + @Environment(\.verticalSizeClass) private var vSizeClass + + private var compact: Bool { vSizeClass == .compact } + + var body: some View { + LibraryView( + store: store, host: host, onLaunch: onLaunch, + onClose: close, controllerActive: controllerActive) + .safeAreaInset(edge: .top, spacing: 0) { + // Leading, like every gamepad heading — no close chrome, B is the exit (the + // coverflow's, or LibraryView's own back-catcher before the coverflow exists). + Text("\(host.displayName) — Library") + .font(.geist(gamepadTitleSize(compact: compact), .bold, relativeTo: .title)) + .foregroundStyle(ink.fg) + .lineLimit(1) + .minimumScaleFactor(0.75) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 24) + .padding(.top, gamepadTitleTopPadding(compact: compact)) + .padding(.bottom, gamepadTitleBottomPadding(compact: compact)) + .background { GamepadTrayScrim(edge: .top) } + } + // A hardware keyboard's Esc still closes, without chrome. + .background { + Button("Close") { close() } + .keyboardShortcut(.cancelAction) + .buttonStyle(.plain) + .frame(width: 0, height: 0) + .opacity(0) + .accessibilityHidden(true) + } + .gamepadPaletteInk() + } +} +#endif diff --git a/clients/apple/Sources/PunktfunkClient/Home/GamepadShell.swift b/clients/apple/Sources/PunktfunkClient/Home/GamepadShell.swift new file mode 100644 index 00000000..e9d025ec --- /dev/null +++ b/clients/apple/Sources/PunktfunkClient/Home/GamepadShell.swift @@ -0,0 +1,95 @@ +// The gamepad UI's screen-shell vocabulary (iOS): which screen sits over the launcher, and the +// console push/pop choreography that presents it. On iOS the launcher's sub-screens (settings, +// add-host, library) are NOT system covers — they are transparent layers composited in +// GamepadHomeView's ZStack over ONE persistent living backdrop, exactly the model +// `pf-console-ui`'s shell renders on the desktop clients: a push slides the incoming screen up +// out of a fade while the outgoing one recedes; a pop mirrors it; the field underneath never +// moves and never leaves. A system `fullScreenCover` — an opaque sheet sliding up from the +// bottom edge, mounting its own backdrop — was exactly the wrong grammar for a console. +// (macOS keeps its windowed sheets and tvOS its focus-engine covers; this file's motion +// constants are iOS-only in practice, but compile everywhere for the shared call sites.) + +import PunktfunkKit +import SwiftUI +#if os(iOS) || os(macOS) || os(tvOS) + +/// The screen the shell currently shows over the launcher. Derived, not stored: the presentation +/// triggers (`showSettings`, `showAddHost`, `libraryTarget`) stay authoritative on every +/// platform — this enum is just their iOS rendering. Depth is ≤ 1 by construction (the settings +/// pin picker is an in-screen layer, and every trigger is only reachable from the launcher), so +/// there is no stack to model. +enum GamepadScreen: Identifiable { + case settings + case addHost + case library(StoredHost) + + var id: String { + switch self { + case .settings: return "settings" + case .addHost: return "addHost" + case .library(let host): return "library-\(host.id.uuidString)" + } + } + + /// The backdrop's calm target while this screen is up: the form screens quiet the field + /// (`Bg::Form` in the console); the library keeps the launcher's full aurora. + var isForm: Bool { + switch self { + case .settings, .addHost: return true + case .library: return false + } + } +} + +/// The console shell's motion constants, mapped to SwiftUI. Source of truth: +/// `crates/pf-console-ui/src/shell/render.rs` (push/pop) and `shell.rs` (`TRANSITION_S`). +enum GamepadShellMotion { + /// One transition, both layers — the console's `TRANSITION_S`. + static let duration: TimeInterval = 0.26 + /// `1-(1-t)³` as a bezier: the standard ease-out-cubic control points. + static let screen = Animation.timingCurve(0.33, 1, 0.68, 1, duration: duration) + /// The backdrop's calm chase. The console runs an exponential approach (τ 0.12 s); the same + /// ease-out at 0.30 s lands within a few percent of it and settles together with the screen. + static let calm = Animation.timingCurve(0.33, 1, 0.68, 1, duration: 0.30) + /// The push/pop travel — the console's `36 * k`, k-floored for a landscape phone. + static func slide(compact: Bool) -> CGFloat { compact ? 27 : 36 } + /// The incoming screen grows from this; the revealed launcher grows back from `underScale`. + static let inScale: CGFloat = 0.985 + static let underScale: CGFloat = 0.96 +} + +extension AnyTransition { + /// The console push/pop for the top layer. Insertion: up out of a fade, growing from 0.985. + /// Removal: down into a fade at full size (the console's pop leaves scale alone). The + /// launcher's recede underneath is NOT a transition — it never unmounts — it is the + /// `covered` opacity/scale in GamepadHomeView, animated in the same transaction. + /// + /// Known deviation from the console: a pop there re-reveals the launcher from α 0.4; a + /// SwiftUI opacity animates from 0. Same duration, same landing — the revealed screen just + /// reads a beat later in the fade, not worth an explicitly-driven progress machine. + static func gamepadScreen(slide: CGFloat) -> AnyTransition { + .asymmetric( + insertion: .opacity + .combined(with: .offset(y: slide)) + .combined(with: .scale(scale: GamepadShellMotion.inScale)), + removal: .opacity.combined(with: .offset(y: slide))) + } +} + +private struct GamepadHostedInShellKey: EnvironmentKey { + static let defaultValue = false +} + +extension EnvironmentValues { + /// True for a screen mounted as one of the shell's layers: it must NOT mount its own + /// backdrop (the shell's single persistent field is behind everything already — a second + /// one would double the mesh cost and break the "field never moves" illusion). The same + /// screens presented as macOS sheets / tvOS covers read the default `false` and keep + /// mounting their own, exactly as before. + var gamepadHostedInShell: Bool { + get { self[GamepadHostedInShellKey.self] } + set { self[GamepadHostedInShellKey.self] = newValue } + } +} + +#endif diff --git a/clients/apple/Sources/PunktfunkClient/Home/LibraryCoverflowView.swift b/clients/apple/Sources/PunktfunkClient/Home/LibraryCoverflowView.swift index 5f73b0ef..509d3fc8 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/LibraryCoverflowView.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/LibraryCoverflowView.swift @@ -26,6 +26,11 @@ struct LibraryCoverflowView: View { /// Button B (back) — dismisses the library screen. No touch equivalent needed here (the toolbar /// Close button already covers that); this is what makes gamepad-only exit possible. var onDismiss: (() -> Void)? + /// Whether the carousel owns the controller — the in-place shell gates it (mid-transition, + /// and under the connect takeover after A launches a title, where this coverflow used to + /// keep polling underneath). Cover/sheet presentations keep the default. + var controllerActive = true + @Environment(\.gamepadHostedInShell) private var hostedInShell #if os(iOS) /// `.compact` in a landscape phone window — drives a tighter poster so everything still fits. @@ -36,6 +41,18 @@ struct LibraryCoverflowView: View { private let compact = false // no size classes on macOS #endif @State private var selection: String? + /// How many covers have settled (art loaded, or every candidate exhausted). + @State private var artSettled = 0 + /// The backstop below has fired: play the entrance regardless of what the art is doing. + @State private var artWaitOver = false + + /// Whether the strip may play its entrance yet. Cards swinging in as grey placeholders and + /// then filling with artwork afterwards is the whole effect wasted, so the entrance waits for + /// the first few covers — every poster is fetched in parallel, so those land together and + /// cover the visible strip. The wait is capped: a slow or artless library still animates. + private var contentReady: Bool { + artWaitOver || artSettled >= min(4, games.count) + } var body: some View { GeometryReader { geo in @@ -46,10 +63,19 @@ struct LibraryCoverflowView: View { .padding(.leading, 22) .padding(.vertical, compact ? 6 : 10) } - .background { GamepadScreenBackground() } + // Hosted in the shell, the field is the shell's own persistent aurora (the library is + // an aurora screen — the calm mix simply stays 0, so nothing even chases). + .background { + if !hostedInShell { GamepadScreenBackground() } + } // Publish the palette's ink to this screen (text, glass, accent, scrims) — a // pale palette flips all of them, and no leaf should have to read the setting. .gamepadPaletteInk() + // The entrance's backstop (see `contentReady`). + .task { + try? await Task.sleep(for: .milliseconds(700)) + artWaitOver = true + } } @ViewBuilder private func content(for size: CGSize) -> some View { @@ -81,9 +107,11 @@ struct LibraryCoverflowView: View { spacing: 34, onActivate: { onLaunch?($0.id) }, onBack: { onDismiss?() }, - shoulderJump: 5 - ) { game in - cover(game, width: coverWidth, height: coverHeight) + shoulderJump: 5, + isActive: controllerActive, + contentReady: contentReady + ) { game, entrance in + cover(game, width: coverWidth, height: coverHeight, entrance: entrance) } .frame(height: coverHeight + 44) } @@ -92,18 +120,26 @@ struct LibraryCoverflowView: View { /// per-frame `phase` (real distance-from-centered), so the tilt tracks what's actually on screen /// mid-scroll. `.shadow` isn't a `VisualEffect`, so it's baked constant into the card; the /// scale/rotation/opacity ramp already makes the centered cover prominent. - private func cover(_ game: GameEntry, width: CGFloat, height: CGFloat) -> some View { - PosterImage(candidates: game.art.posterCandidates, title: game.title, session: imageSession) + private func cover( + _ game: GameEntry, width: CGFloat, height: CGFloat, entrance: CardEntrance + ) -> some View { + PosterImage( + candidates: game.art.posterCandidates, title: game.title, session: imageSession, + onLoaded: { artSettled += 1 }) .frame(width: width, height: height) .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) .overlay(alignment: .topLeading) { - StoreBadge(label: game.storeLabel, isLauncher: game.isLauncher) + // `solid`: a frosted chip can't sample a backdrop through this card's own + // composited transform, so it would only show up on the centred card. + StoreBadge(label: game.storeLabel, isLauncher: game.isLauncher, solid: true) } .overlay { RoundedRectangle(cornerRadius: 16, style: .continuous) .strokeBorder(ink.fg(0.12), lineWidth: 1) } - .shadow(color: .black.opacity(0.5), radius: 16, y: 12) + .shadow(color: ink.shadow(0.5), radius: 16, y: 12) + // Beneath the scroll transition, never around it — see CardEntrance. + .modifier(entrance) .scrollTransition { content, phase in let v = phase.value let d = CGFloat(min(abs(v), 1)) diff --git a/clients/apple/Sources/PunktfunkClient/Home/LibraryView.swift b/clients/apple/Sources/PunktfunkClient/Home/LibraryView.swift index 03a2e918..13efdf4e 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/LibraryView.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/LibraryView.swift @@ -12,6 +12,13 @@ struct LibraryView: View { /// Tapping a title starts a session that asks the host to launch it (the library id is passed /// through). `nil` ⇒ browse-only (cards aren't tappable). var onLaunch: ((String) -> Void)? = nil + /// How the gamepad shell (GamepadLibraryScreen) closes this screen; nil — every sheet/cover + /// presentation — falls back to the environment dismiss. + var onClose: (() -> Void)? = nil + /// Whether the gamepad coverflow owns the controller — the shell gates it during a push/pop + /// and while the connect takeover is up. Presentations that cover the launcher keep the + /// default (their being up IS the launcher's gate). + var controllerActive = true @Environment(\.dismiss) private var dismiss @State private var games: [GameEntry] = [] @@ -58,6 +65,17 @@ struct LibraryView: View { imageSession?.finishTasksAndInvalidate() imageSession = nil } + #if os(iOS) || os(macOS) + // B closes the library even before the coverflow exists (loading / error / empty): + // the coverflow's carousel owns B once games render; until then this zero-size + // listener does — without it a controller-only user is trapped on an error screen + // (the gamepad screens carry no close chrome). + .background { + if gamepadUIActive && games.isEmpty { + LibraryBackCatcher(active: controllerActive) { (onClose ?? { dismiss() })() } + } + } + #endif } @ViewBuilder private var content: some View { @@ -72,7 +90,8 @@ struct LibraryView: View { if gamepadUIActive { LibraryCoverflowView( games: games, imageSession: imageSession, onLaunch: onLaunch, - onDismiss: { dismiss() }) + onDismiss: { (onClose ?? { dismiss() })() }, + controllerActive: controllerActive) } else { grid } @@ -202,6 +221,30 @@ struct LibraryView: View { } } +#if os(iOS) || os(macOS) +/// Zero-size controller listener for the library's pre-coverflow states — B backs out. The same +/// shape as ConnectOverlay's `ConnectControllerInput`; `GamepadMenuInput.needsSnapshot` swallows +/// the held press that opened the screen. Unmounts the moment the coverflow (and its own B) is up. +private struct LibraryBackCatcher: View { + let active: Bool + let onBack: () -> Void + @State private var input = GamepadMenuInput(manager: .shared) + + var body: some View { + Color.clear + .frame(width: 0, height: 0) + .onAppear { + input.onBack = onBack + if active { input.start() } + } + .onChange(of: active) { _, nowActive in + if nowActive { input.start() } else { input.stop() } + } + .onDisappear { input.stop() } + } +} +#endif + /// One poster tile. Steam vs custom is marked with a badge; the art walks the candidate URLs /// (portrait → header → hero) and finally a text placeholder. private struct GameCard: View { diff --git a/clients/apple/Sources/PunktfunkClient/Home/LibraryWidgets.swift b/clients/apple/Sources/PunktfunkClient/Home/LibraryWidgets.swift index 79d2219d..86d577ac 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/LibraryWidgets.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/LibraryWidgets.swift @@ -17,16 +17,29 @@ struct StoreBadge: View { /// A launcher entry (design D4) gets the brand fill, so "opens Steam" is legible at poster size /// without reading the title. var isLauncher: Bool = false + /// Fill the chip with a flat wash instead of a frosted material. + /// + /// The coverflow MUST pass true. Its cards ride a `.scrollTransition` that composites them + /// with `opacity < 1` and a 3D rotation, and a material cannot sample a backdrop through an + /// offscreen composite — so the frost stayed blank on every card and only appeared on the one + /// card sitting at exactly full opacity in the centre, reading as a flash on focus. A flat + /// wash has no backdrop to sample: it is simply always there. (Deliberately black, not + /// palette ink: the chip sits on cover art, whose colours the palette has no business + /// fighting.) + var solid: Bool = false + + private var fill: AnyShapeStyle { + if isLauncher { return AnyShapeStyle(Color.brand) } + return solid ? AnyShapeStyle(Color.black.opacity(0.58)) : AnyShapeStyle(.ultraThinMaterial) + } var body: some View { Text(label) .font(.geist(11, .semibold, relativeTo: .caption2)) - .foregroundStyle(isLauncher ? AnyShapeStyle(.white) : AnyShapeStyle(.primary)) + .foregroundStyle(isLauncher || solid ? AnyShapeStyle(.white) : AnyShapeStyle(.primary)) .padding(.horizontal, 6) .padding(.vertical, 3) - .background( - isLauncher ? AnyShapeStyle(Color.brand) : AnyShapeStyle(.ultraThinMaterial), - in: Capsule()) + .background(fill, in: Capsule()) .padding(6) } } @@ -58,6 +71,10 @@ struct PosterImage: View { let candidates: [URL] let title: String let session: URLSession? + /// Fires once this poster has settled — art loaded, or every candidate exhausted and the + /// placeholder is what it will be. The gamepad coverflow waits on a few of these before + /// playing its entrance, so the cards swing in carrying artwork rather than grey rectangles. + var onLoaded: (() -> Void)? @State private var index = 0 @State private var image: PlatformImage? @@ -67,19 +84,30 @@ struct PosterImage: View { Image(platformImage: image) .resizable() .scaledToFill() + .transition(.opacity) } else if index < candidates.count { ZStack { placeholder; ProgressView() } + .transition(.opacity) } else { placeholder + .transition(.opacity) } } + // Art crosses over its placeholder instead of replacing it between two frames. Cover + // fetches land one by one, so without this a freshly opened library is a run of cards + // visibly snapping from grey to artwork after the strip has already settled. + .animation(.easeOut(duration: 0.3), value: image != nil) .frame(maxWidth: .infinity, maxHeight: .infinity) .clipped() .task(id: index) { await loadCurrent() } } private func loadCurrent() async { - guard index < candidates.count else { return } + // Past the end: the placeholder IS the final look, so this poster has settled. + guard index < candidates.count else { + onLoaded?() + return + } guard let session, let data = try? await session.data(from: candidates[index]).0, let loaded = PlatformImage(data: data) else { @@ -87,6 +115,7 @@ struct PosterImage: View { return } image = loaded + onLoaded?() } private var placeholder: some View { diff --git a/clients/apple/Sources/PunktfunkClient/Screenshots/ScreenshotScenes.swift b/clients/apple/Sources/PunktfunkClient/Screenshots/ScreenshotScenes.swift index 7c4a4bba..4017f335 100644 --- a/clients/apple/Sources/PunktfunkClient/Screenshots/ScreenshotScenes.swift +++ b/clients/apple/Sources/PunktfunkClient/Screenshots/ScreenshotScenes.swift @@ -243,7 +243,7 @@ private struct ShotGamepadHome: View { GamepadHomeView( store: store, model: model, discovery: discovery, libraryTarget: .constant(nil), waker: waker, - connect: { _, _ in }, connectDiscovered: { _ in }) + connect: { _, _ in }, connectDiscovered: { _ in }, launchTitle: { _, _ in }) } } @@ -301,7 +301,7 @@ private struct ShotConnect: View { GamepadHomeView( store: store, model: model, discovery: discovery, libraryTarget: .constant(nil), waker: waker, - connect: { _, _ in }, connectDiscovered: { _ in }) + connect: { _, _ in }, connectDiscovered: { _ in }, launchTitle: { _, _ in }) } else { ShotHome() } diff --git a/clients/apple/Sources/PunktfunkClient/Session/StreamHUDView.swift b/clients/apple/Sources/PunktfunkClient/Session/StreamHUDView.swift index 41606725..9e06fdb3 100644 --- a/clients/apple/Sources/PunktfunkClient/Session/StreamHUDView.swift +++ b/clients/apple/Sources/PunktfunkClient/Session/StreamHUDView.swift @@ -224,9 +224,18 @@ struct StreamHUDView: View { /// The card's inner content padding. Roomier on tvOS — the stat text auto-scales for the /// couch (relative system styles), so the card's chrome must keep pace or it reads cramped. + /// + /// On iOS it also has to CLEAR THE CORNER. A rounded corner of radius `r` pulls the card's + /// edge inward by `r − √(r² − (r−y)²)` at a distance `y` below the top, so the first and last + /// lines of a padded stack sit inside the arc unless the padding keeps pace with the radius. + /// At `0.45 · r` that intrusion stays well inside the padding across the whole range this + /// card can wear (≈4.6 pt of arc against 12.6 pt of padding at the 28 pt cap), so no line + /// ever runs into the curve. private var cardPadding: CGFloat { #if os(tvOS) return 16 + #elseif os(iOS) + return max(10, cardCornerRadius * 0.45) #else return 10 #endif @@ -246,13 +255,20 @@ struct StreamHUDView: View { #endif } - /// The card's corner radius. On iOS it's concentric with the physical display corner — - /// `displayCornerRadius − edgeInset`, so the gap to the screen edge stays uniform right around the - /// corner instead of a small-radius card cutting into the very rounded glass. Clamped so a - /// flat-cornered device (or a hidden radius) still gets a sensibly rounded card. + /// The card's corner radius. On iOS it aims to be concentric with the physical display + /// corner — `displayCornerRadius − edgeInset`, so the gap to the screen edge stays uniform + /// right around the corner instead of a small-radius card cutting into the very rounded + /// glass — but that aim is BOUNDED by what a card this small can actually carry. + /// + /// Unbounded, a modern phone (~62 pt of display radius) asked for a 48 pt corner on a card + /// whose lines sit 10 pt from the edge: the arc reaches ~19 pt inward at the first line, so + /// the top and bottom lines rendered INSIDE the curve. Concentricity is only a virtue while + /// the radius is small next to the card; past that it is just a blob eating its own text. + /// 28 pt is the most this card's stack can wear (with `cardPadding` scaling alongside), and + /// devices whose display radius asks for less than that still get a truly concentric corner. private var cardCornerRadius: CGFloat { #if os(iOS) - return max(12, DeviceMetrics.displayCornerRadius - edgeInset) + return min(28, max(12, DeviceMetrics.displayCornerRadius - edgeInset)) #elseif os(tvOS) return 16 // scales with the roomier padding #else diff --git a/clients/apple/Sources/PunktfunkClient/Settings/AcknowledgementsView.swift b/clients/apple/Sources/PunktfunkClient/Settings/AcknowledgementsView.swift index 5108c6b8..d9f693ea 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/AcknowledgementsView.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/AcknowledgementsView.swift @@ -58,8 +58,8 @@ struct AcknowledgementsView: View { .font(.geist(Self.headlineFont, .semibold, relativeTo: .headline)) Text( "Punktfunk uses the open-source components below, each under its own license. " - + "On some platforms FFmpeg is additionally bundled under the LGPL v2.1+ " - + "(dynamically linked, replaceable)." + + "Video decoding uses the system's own VideoToolbox framework, so nothing " + + "is bundled for it — and no Punktfunk client bundles FFmpeg on any platform." ) .font(.geist(Self.captionFont, relativeTo: .caption)) .foregroundStyle(.secondary) diff --git a/clients/apple/Sources/PunktfunkClient/Settings/GamepadOptionBand.swift b/clients/apple/Sources/PunktfunkClient/Settings/GamepadOptionBand.swift new file mode 100644 index 00000000..ad43ad23 --- /dev/null +++ b/clients/apple/Sources/PunktfunkClient/Settings/GamepadOptionBand.swift @@ -0,0 +1,180 @@ +// The gamepad settings' "select" value as a REAL band: the options sit side by side on a drum +// segment curving about a vertical axis — the current one faces you flat, and a step rotates the +// next one in with perspective. The old presentation animated a single Text keyed by its value +// (an old-out/new-in crossfade that merely implied motion), which fell apart under fast repeated +// steps: each press restarted the fade. Here the drum's position is one continuous value driven +// by a spring, and SwiftUI's spring retargeting preserves velocity — rapid presses accumulate +// into one accelerating travel instead of five restarted crossfades. +// +// The band is LINEAR, not a ring (field verdict on the first cut): a ring showed the first +// option waiting to the right of the last one, which left/right can't reach (adjust clamps) — +// a promise the navigation doesn't keep. And on a 2-option ring the unselected option flipped +// sides with every step. So positions are fixed: option i sits i steps from the start, the ends +// are the ends, and A's wrap from the last option travels BACK across the list to the first. +// Options other than the facing one exist only while the drum is actually moving — at rest a row +// shows exactly its value (a resting neighbour under a long label rendered as overlapping, +// unreadable text). +// +// The band is purely presentational: stepping semantics (left/right clamps with a boundary thud, +// A cycles forward wrapping, disabled rows refuse input) stay in GamepadSettingsView's row +// closures. Font and ink come from the environment — the row applies the same value font/colour +// it always did, and the drum's own opacity ramp multiplies on top. + +import Foundation +import SwiftUI + +#if os(iOS) || os(macOS) || os(tvOS) + +struct GamepadOptionBand: View { + let options: [String] + /// The committed selection — the caller's clamp/wrap already applied. + let selection: Int + let focused: Bool + /// The band's footprint, FIXED by the row: a step must never reflow the row (the old + /// free-width value shifted the chevrons with every label), and the drum needs its stage + /// even when the facing label is short. + let width: CGFloat + + @Environment(\.accessibilityReduceMotion) private var reduceMotion + + /// Where the drum rests, in option steps — always chasing `Double(selection)`; only the + /// spring's interpolation ever puts it between integers. + @State private var drumPosition: Double + + init(options: [String], selection: Int, focused: Bool, width: CGFloat) { + self.options = options + self.selection = selection + self.focused = focused + self.width = width + _drumPosition = State(initialValue: Double(selection)) + } + + var body: some View { + Group { + if reduceMotion { + // No drum, no travel: today's quiet crossfade, minus even the 14 pt slip. + ZStack { + Text(current) + .lineLimit(1) + .id(selection) + .transition(.opacity) + } + .animation(.smooth(duration: 0.2), value: selection) + } else { + Drum( + options: options, + rotation: drumPosition, + target: drumPosition, + // Puts the ±1 neighbour ~40 % of the band off-centre, curling to the edge. + radius: width * 0.72) + } + } + .frame(width: width) + .clipped() + // Soft edges: the drum dissolves before it reaches the chevrons instead of ending on a cut. + .mask { + LinearGradient( + stops: [ + .init(color: .clear, location: 0), + .init(color: .black, location: 0.12), + .init(color: .black, location: 0.88), + .init(color: .clear, location: 1), + ], + startPoint: .leading, endPoint: .trailing) + } + .onChange(of: selection) { old, new in step(from: old, to: new) } + // The options list itself can mutate under the drum (a custom resolution appears, a + // controller connects, the buffer options re-derive from a new refresh rate) — re-seat + // without a travel. + .onChange(of: options.count) { _, _ in snap() } + // One element to VoiceOver — the neighbour texts are rendering, not content. + .accessibilityElement(children: .ignore) + .accessibilityLabel(current) + } + + private var current: String { + options.indices.contains(selection) ? options[selection] : "" + } + + /// A step (or A's wrap — which on a linear band is a fast travel back to the start) springs + /// the drum; anything else (an external write from the touch settings, a re-derived options + /// list) re-seats it — a travel to a value the user didn't step to would read as the UI + /// acting on its own. + private func step(from old: Int, to new: Int) { + let wrapped = options.count > 1 && old == options.count - 1 && new == 0 + guard (abs(new - old) == 1 || wrapped), !reduceMotion else { return snap() } + withAnimation(.spring(response: 0.32, dampingFraction: 0.78)) { + drumPosition = Double(new) + } + } + + private func snap() { + var tx = Transaction() + tx.disablesAnimations = true + withTransaction(tx) { drumPosition = Double(selection) } + } +} + +/// The rotating drum itself. `Animatable` so SwiftUI re-evaluates the body with the INTERPOLATED +/// rotation every frame of the spring — each option's offset/scale/opacity follows the real arc, +/// and options along the travel genuinely enter and leave mid-flight. (A plain `.animation` on +/// independent modifiers can't do that: each modifier would lerp its own endpoints and the +/// in-between options would never appear.) +private struct Drum: View, Animatable { + let options: [String] + /// The interpolated drum position, in option steps. + var rotation: Double + /// Where the spring is headed (jumps instantly on a step; only `rotation` chases it). The + /// distance between them is "how mid-flight are we" — the neighbours exist exactly as long + /// as the drum is moving, fading continuously as it lands, so a resting row is one flat + /// Text and a long label never sits under a resting neighbour. + let target: Double + /// Drum radius in points (from the band width — see the caller). + let radius: Double + + var animatableData: Double { + get { rotation } + set { rotation = newValue } + } + + /// Angular pitch between adjacent options on the drum. + private static let stepAngle = 34.0 * .pi / 180.0 + + var body: some View { + let flight = min(1, abs(rotation - target) * 3) + let content = ZStack { + ForEach(0.. 0.001 && abs(d) <= 2.5) { + option(i, distance: d, gate: flight) + } + } + } + #if os(tvOS) + // Flatten the transform stack while travelling — the 10-foot GPU already made these + // rows drop Liquid Glass, and five projected texts per step is the same class of cost. + content.drawingGroup() + #else + content + #endif + } + + @ViewBuilder private func option(_ i: Int, distance d: Double, gate: Double) -> some View { + let angle = d * Self.stepAngle + let depth = cos(angle) + // The facing option never gates: a resting row still shows its value. + let alpha = pow(max(depth, 0), 3) * (abs(d) < 0.5 ? 1 : gate) + Text(options[i]) + .lineLimit(1) + .scaleEffect(0.70 + 0.30 * depth) + // Foreshorten the label as it turns away — this is what sells the cylinder. + .rotation3DEffect(.radians(angle), axis: (x: 0, y: 1, z: 0), perspective: 0.4) + .offset(x: radius * sin(angle)) + .opacity(alpha) + .zIndex(depth) + } +} + +#endif diff --git a/clients/apple/Sources/PunktfunkClient/Settings/GamepadSettingsView.swift b/clients/apple/Sources/PunktfunkClient/Settings/GamepadSettingsView.swift index c23452cb..4358cddc 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/GamepadSettingsView.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/GamepadSettingsView.swift @@ -47,10 +47,18 @@ enum GpSettingsTab: String, CaseIterable, Hashable { struct GamepadSettingsView: View { @Environment(\.gamepadInk) private var ink @Environment(\.dismiss) private var dismiss + @Environment(\.gamepadHostedInShell) private var hostedInShell /// The saved-host store — the pin picker writes `setPinned` through it and the profile rows /// count pins from its live hosts. Threaded in from GamepadHomeView like the home screen /// itself (ContentView owns the instance). @ObservedObject var store: HostStore + /// How the in-place shell (iOS) closes this screen; nil (the macOS sheet, the tvOS cover) + /// falls back to the environment dismiss. See `performClose`. + var close: (() -> Void)? + /// Whether this screen owns the controller. The shell holds it false during a push/pop (the + /// console's input drop) and while the connect takeover is up; a system presentation never + /// needs the gate and keeps the default. + var controllerActive = true @AppStorage(DefaultsKey.streamWidth) private var width = 1920 @AppStorage(DefaultsKey.streamHeight) private var height = 1080 @AppStorage(DefaultsKey.streamHz) private var hz = 60 @@ -85,6 +93,7 @@ struct GamepadSettingsView: View { #endif #if os(iOS) @AppStorage(DefaultsKey.rumbleOnDevice) private var rumbleOnDevice = false + @AppStorage(DefaultsKey.gyroFromDevice) private var gyroFromDevice = false #endif @ObservedObject private var gamepads = GamepadManager.shared /// The profile catalog (ProfileStore.shared, like every other surface that reads it) — the @@ -126,7 +135,8 @@ struct GamepadSettingsView: View { onAdjust: { row, delta in adjust(id: row.id, by: delta) }, onActivate: { activate(id: $0.id) }, onBack: { back() }, - onShoulder: { step(tabBy: $0) } + onShoulder: { step(tabBy: $0) }, + isActive: controllerActive ) { row, focused in rowView(row, focused: focused) .frame(maxWidth: GamepadFormMetrics.rowMaxWidth) @@ -134,18 +144,20 @@ struct GamepadSettingsView: View { } .frame(maxWidth: .infinity) .safeAreaInset(edge: .top, spacing: 0) { - VStack(spacing: compact ? 4 : 8) { + VStack(alignment: .leading, spacing: gamepadHeaderSpacing(compact: compact)) { + // Leading, like a console section heading — centred read as a floating label, + // and a gamepad UI needs no close chrome next to it (B is the exit). Text(title) .font(.geist(gamepadTitleSize(compact: compact), .bold, relativeTo: .title)) .foregroundStyle(ink.fg) - .frame(maxWidth: .infinity) - .overlay(alignment: .trailing) { closeButton.padding(.trailing, 20) } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 24) // The picker is one layer deeper — its rows aren't sections of anything, so the // strip would be a control that does nothing while it's up. if pinTarget == nil { tabStrip } } .padding(.top, gamepadTitleTopPadding(compact: compact)) - .padding(.bottom, compact ? 4 : 8) + .padding(.bottom, gamepadTitleBottomPadding(compact: compact)) .background { GamepadTrayScrim(edge: .top) } } .safeAreaInset(edge: .bottom, alignment: .leading, spacing: 0) { @@ -167,8 +179,12 @@ struct GamepadSettingsView: View { } // The launcher's living field, calmed (GamepadFormBackground) — the glass rows keep real // colour and luminance to lens without the launcher's contrast, and the palette setting - // applies here too, so this screen previews the row you're stepping. - .background { GamepadFormBackground() } + // applies here too, so this screen previews the row you're stepping. Hosted in the + // shell, the field is the SHELL's (one persistent backdrop, calm-chased) — mounting a + // second would double the mesh and snap where the shell crossfades. + .background { + if !hostedInShell { GamepadFormBackground() } + } // Publish the palette's ink to this screen (text, glass, accent, scrims) — a // pale palette flips all of them, and no leaf should have to read the setting. .gamepadPaletteInk() @@ -177,6 +193,18 @@ struct GamepadSettingsView: View { gamepads.startDiscovery() } .onDisappear { gamepads.stopDiscovery() } + #if !os(tvOS) + // The visible close ✕ is gone (a gamepad UI exits with B) — this keeps a hardware + // keyboard's Esc and the macOS sheet's cancel working without chrome. + .background { + Button("Close") { performClose() } + .keyboardShortcut(.cancelAction) + .buttonStyle(.plain) + .frame(width: 0, height: 0) + .opacity(0) + .accessibilityHidden(true) + } + #endif } /// The section switcher. Horizontally scrollable so a narrow phone in landscape never has to @@ -229,10 +257,12 @@ struct GamepadSettingsView: View { .padding(.vertical, 7) .background { // One shared capsule that MOVES between pills, rather than one per pill fading - // in and out — the highlight travels the way the press did. + // in and out — the highlight travels the way the press did. A Liquid Glass + // surface (accent-tinted through consoleGlass), so the strip wears the same + // material language as the rows it sits above. if selected { - Capsule() - .fill(ink.accent(0.85)) + Color.clear + .consoleGlass(Capsule(), tint: ink.accent(0.85)) .matchedGeometryEffect(id: "tab", in: tabHighlight) } } @@ -274,22 +304,10 @@ struct GamepadSettingsView: View { focusID = landing } - /// Touch/click fallback for closing — the controller path is B, a hardware keyboard's Esc - /// rides the cancel action. - private var closeButton: some View { - Button { dismiss() } label: { - Image(systemName: "xmark") - .font(.system(size: GamepadFormMetrics.closeFont, weight: .semibold)) - .foregroundStyle(ink.fg) - .frame(width: GamepadFormMetrics.closeSide, height: GamepadFormMetrics.closeSide) - .glassBackground(Circle(), interactive: true) - .contentShape(Circle()) - } - .buttonStyle(.plain) - #if !os(tvOS) - .keyboardShortcut(.cancelAction) // unavailable on tvOS (Menu is the cancel there) - #endif - .accessibilityLabel("Close settings") + /// Close this screen through whichever mechanism presents it: the shell's layer pop on iOS, + /// the environment dismiss under a macOS sheet / tvOS cover. + private func performClose() { + if let close { close() } else { dismiss() } } /// "Settings", or "Pin “Work”" while the pin picker is up — the title is what says which @@ -337,7 +355,7 @@ struct GamepadSettingsView: View { pinTarget = nil focusID = "profile-\(profile.id)" } else { - dismiss() + performClose() } } @@ -363,24 +381,31 @@ struct GamepadSettingsView: View { .font(.system(size: m.chevronFont, weight: .semibold)) .foregroundStyle( ink.fg(focused && row.adjustable && row.enabled ? 0.6 : 0)) - // Keyed by the value so a change slides the new option in instead of - // hard-swapping the string — a QUIET horizontal slip following the user's - // motion (a right-step enters from the right), crossfading over ~14 pt. - // Deliberately not `.push`: that travels the whole container width, loud - // and visibly outside the row. The ZStack is the stable home the - // removed/inserted texts transition within. - let slide: CGFloat = lastAdjustDelta >= 0 ? 14 : -14 - ZStack { - Text(row.value) + if let labels = row.optionLabels, let idx = row.selectedIndex { + // A choice row's value is a REAL band — the options ride a rotating + // drum, so fast repeated steps spin it instead of restarting a fade. + GamepadOptionBand( + options: labels, selection: idx, focused: focused, width: bandWidth) .font(.geist(m.valueFont, .medium, relativeTo: .callout)) .foregroundStyle(focused ? ink.fg : ink.fg(0.6)) - .lineLimit(1) - .id(row.value) - .transition(.asymmetric( - insertion: .offset(x: slide).combined(with: .opacity), - removal: .offset(x: -slide).combined(with: .opacity))) + } else { + // The flat rows (profile pin counts, placeholders) keep the quiet slip: + // keyed by the value so a change slides the new string in following the + // user's motion, crossfading over ~14 pt. The ZStack is the stable home + // the removed/inserted texts transition within. + let slide: CGFloat = lastAdjustDelta >= 0 ? 14 : -14 + ZStack { + Text(row.value) + .font(.geist(m.valueFont, .medium, relativeTo: .callout)) + .foregroundStyle(focused ? ink.fg : ink.fg(0.6)) + .lineLimit(1) + .id(row.value) + .transition(.asymmetric( + insertion: .offset(x: slide).combined(with: .opacity), + removal: .offset(x: -slide).combined(with: .opacity))) + } + .animation(.smooth(duration: 0.22), value: row.value) } - .animation(.smooth(duration: 0.22), value: row.value) Image(systemName: "chevron.right") .font(.system(size: m.chevronFont, weight: .semibold)) .foregroundStyle( @@ -410,6 +435,17 @@ struct GamepadSettingsView: View { rows.first { $0.id == focusID }?.detail ?? " " } + /// The option band's fixed stage. A portrait phone is the one place the full 240 pt starves + /// the row's label (everywhere else the 620 pt row cap leaves room to spare), so it alone + /// narrows the stage. + private var bandWidth: CGFloat { + #if os(iOS) + hSizeClass == .compact && vSizeClass == .regular ? 170 : GamepadFormMetrics.bandWidth + #else + GamepadFormMetrics.bandWidth + #endif + } + // MARK: - Row model private struct Row: Identifiable { @@ -422,6 +458,11 @@ struct GamepadSettingsView: View { let value: String /// One-line explanation shown near the hint bar while this row is focused. let detail: String + /// A choice row's full option list (labels only — the tags stay inside the closures) + /// and where its drum currently rests. nil ⇒ the value renders as plain text (toggles, + /// actions, profiles — a two-position switch is not a drum; see GamepadOptionBand). + var optionLabels: [String]? + var selectedIndex: Int? /// Whether left/right means anything here — false hides the value's chevrons (the /// Profiles rows navigate, and the placeholder rows do nothing at all). var adjustable = true @@ -649,6 +690,22 @@ struct GamepadSettingsView: View { value: $rumbleOnDevice), at: at + 1) } + // The phone-gyro mirror sits beside the rumble mirror: same clip-on-pad audience, + // opposite data direction. Hidden where the device has no motion hardware; engages + // in-session only while player 1's controller reports no rotation rate of its own. + if DeviceGyro.isAvailable, + let anchor = list.firstIndex(where: { $0.id == "deviceRumble" }) + ?? list.firstIndex(where: { $0.id == "padType" }) { + list.insert( + toggleRow( + id: "deviceGyro", tab: .controller, + icon: "gyroscope", + label: "Gyro from this device", + detail: "When the controller has no gyro, send this device's motion " + + "sensors as player 1's — for clip-on pads without one of their own.", + value: $gyroFromDevice), + at: anchor + 1) + } #endif return list + profileRows } @@ -710,6 +767,8 @@ struct GamepadSettingsView: View { value: pinned ? "Pinned" : "Off", detail: "A pinned profile appears as its own card on the host — one press " + "connects with it.", + optionLabels: ["Off", "Pinned"], + selectedIndex: pinned ? 1 : 0, adjust: { delta in let target = delta > 0 guard pinned != target else { return false } @@ -776,6 +835,10 @@ struct GamepadSettingsView: View { id: id, tab: tab, icon: icon, label: label, value: index.map { options[$0].label } ?? "—", detail: detail, + // The band mounts only once the value is a known option — the "—" of an unknown + // current renders flat, and the first step's snap-to-first seats the drum. + optionLabels: index != nil ? options.map(\.label) : nil, + selectedIndex: index, enabled: enabled, adjust: { delta in // Unknown current value: snap to the first option on any step. @@ -803,6 +866,10 @@ struct GamepadSettingsView: View { id: id, tab: tab, icon: icon, label: label, value: value.wrappedValue ? "On" : "Off", detail: detail, + // Toggles ride the band too (field ask): Off sits left of On, matching the + // directional semantics below, so a right-step slides On in from the right. + optionLabels: ["Off", "On"], + selectedIndex: value.wrappedValue ? 1 : 0, enabled: enabled, adjust: { delta in // Directional semantics: left = off, right = on; a no-op reads as a boundary. diff --git a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Sections.swift b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Sections.swift index 93dbdf71..8ee5f380 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Sections.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Sections.swift @@ -712,6 +712,15 @@ extension SettingsView { Toggle("Rumble on this iPhone", isOn: $rumbleOnDevice) } } + // The rumble mirror's sibling, data flowing the other way: hidden where the + // device has no motion hardware, engages only while the player-1 controller + // reports no rotation rate of its own. + if !inProfileScope, DeviceGyro.isAvailable { + described("When the controller has no gyro of its own, sends this device's " + + "motion sensors as player 1's — for clip-on pads without one.") { + Toggle("Gyro from this device", isOn: $gyroFromDevice) + } + } #endif #if !os(tvOS) if !inProfileScope { diff --git a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView.swift b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView.swift index 88128ee7..e93581f2 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView.swift @@ -91,6 +91,7 @@ struct SettingsView: View { @AppStorage(DefaultsKey.pointerCapture) var pointerCapture = true @AppStorage(DefaultsKey.touchMode) var touchMode = TouchInputMode.trackpad.rawValue @AppStorage(DefaultsKey.rumbleOnDevice) var rumbleOnDevice = false + @AppStorage(DefaultsKey.gyroFromDevice) var gyroFromDevice = false // The sidebar selection drives the detail pane on iPad and the pushed sub-page on iPhone. // Width class decides the initial value: nil on iPhone (show the category list first), // General on iPad (a two-column layout should never open with an empty detail). diff --git a/clients/apple/Sources/PunktfunkClient/Support/GlassStyle.swift b/clients/apple/Sources/PunktfunkClient/Support/GlassStyle.swift index 6f429e44..f8b64170 100644 --- a/clients/apple/Sources/PunktfunkClient/Support/GlassStyle.swift +++ b/clients/apple/Sources/PunktfunkClient/Support/GlassStyle.swift @@ -70,12 +70,17 @@ extension View { // MARK: - Console glass (gamepad host tiles + settings rows) -/// Liquid Glass tuned for the gamepad UI's dark "console" surfaces — the host-carousel tiles and +/// Liquid Glass tuned for the gamepad UI's "console" surfaces — the host-carousel tiles and /// the settings rows. Unlike `glassBackground` (floating-overlay only, per HIG), this deliberately /// clads content tiles / dense rows: a chosen part of the 10-foot console look. `tint` washes the -/// glass toward a color (the brand violet on the focused / primary surface); `interactive` makes -/// it flex on press. The pre-26 fallback is `.ultraThinMaterial` forced dark — these surfaces -/// always sit on the near-black backdrop, so the material must stay dark even in a light appearance. +/// glass toward a color (the palette accent on the focused / primary surface); `interactive` makes +/// it flex on press. +/// +/// Every tier is WASHED with the palette's `ink.glass` — the same surface colour the console +/// fills its panels with — so switching the background palette recolours the surfaces, not just +/// the text on them. The wash alphas are tune-on-device values with one fixed direction: the +/// pale palettes' white frost needs MORE body than the dark glass (the console's 0.66-vs-0.62 +/// pair), because a thin white wash over a colourful field reads as haze, not as a surface. private struct ConsoleGlass: ViewModifier { let shape: S var tint: Color? @@ -86,16 +91,19 @@ private struct ConsoleGlass: ViewModifier { @Environment(\.gamepadInk) private var ink private var scheme: ColorScheme { ink.isLight ? .light : .dark } + /// The palette wash over the material tiers (the material itself supplies the blur body). + private var materialWash: Color { ink.glass(ink.isLight ? 0.55 : 0.40) } func body(content: Content) -> some View { #if os(tvOS) // ALWAYS the material fallback on tvOS: the gamepad settings list is 15+ of these // surfaces, and live Liquid Glass per row made the whole screen visibly laggy on the // Apple TV's GPU (same class of call GlassProminentButton already makes — glass fights - // the 10-foot platform). The tint rides an overlay so the focused row keeps its wash. + // the 10-foot platform). The wash and tint ride overlays — two flat fills, no GPU cost. content.background { shape.fill(.ultraThinMaterial) .environment(\.colorScheme, scheme) + .overlay { shape.fill(materialWash) } .overlay { if let tint { shape.fill(tint) } } @@ -104,7 +112,14 @@ private struct ConsoleGlass: ViewModifier { if #available(iOS 26, macOS 26, *) { content.glassEffect(glass, in: shape).environment(\.colorScheme, scheme) } else { - content.background { shape.fill(.ultraThinMaterial).environment(\.colorScheme, scheme) } + content.background { + shape.fill(.ultraThinMaterial) + .environment(\.colorScheme, scheme) + .overlay { shape.fill(materialWash) } + .overlay { + if let tint { shape.fill(tint) } + } + } } #endif } @@ -112,8 +127,13 @@ private struct ConsoleGlass: ViewModifier { #if !os(tvOS) @available(iOS 26, macOS 26, *) private var glass: Glass { - var g: Glass = .regular - if let tint { g = g.tint(tint) } + // Liquid Glass has ONE tint channel, so the palette wash and the caller's tint share + // it: mixed 60 % toward the caller's (the focused row must still read accented on + // every palette) over the palette base. If device QA finds the mixed focus wash too + // weak, the escape hatch is `tint ?? wash` — today's focused look, bit for bit. + let wash = ink.glass(ink.isLight ? 0.60 : 0.45) + var g: Glass = .regular.tint( + tint.map { wash.mix(with: $0, by: 0.6) } ?? wash) if interactive { g = g.interactive() } return g } @@ -121,9 +141,51 @@ private struct ConsoleGlass: ViewModifier { } extension View { - /// Liquid Glass for a dark console surface (a host tile / settings row), or `.ultraThinMaterial` - /// (forced dark) pre-26. Pass the surface's shape explicitly — glass defaults to a Capsule. + /// Liquid Glass for a console surface (a host tile / settings row), or `.ultraThinMaterial` + /// pre-26 — both washed with the palette's own glass colour, both frosting to the palette's + /// scheme. Pass the surface's shape explicitly — glass defaults to a Capsule. func consoleGlass(_ shape: S, tint: Color? = nil, interactive: Bool = false) -> some View { modifier(ConsoleGlass(shape: shape, tint: tint, interactive: interactive)) } } + +// MARK: - Console floating glass (the gamepad screens' close buttons) + +/// `glassBackground` for a floating control INSIDE the gamepad UI (the close ✕): same shape +/// contract, but washed with the palette's ink and frosted to the palette's scheme — plain +/// `glassBackground` follows the SYSTEM appearance, which leaves the frost dark under dark ink +/// when a pale palette is up. The non-gamepad floating surfaces (the HUD, the trust card, the +/// touch connect modal) keep plain `glassBackground`: they sit over video or the touch UI, +/// where the palette means nothing. +private struct ConsoleGlassBackground: ViewModifier { + let shape: S + var interactive = false + @Environment(\.gamepadInk) private var ink + + private var scheme: ColorScheme { ink.isLight ? .light : .dark } + + func body(content: Content) -> some View { + if #available(iOS 26, macOS 26, tvOS 26, *) { + content + .glassEffect( + (interactive ? Glass.regular.interactive() : .regular) + .tint(ink.glass(ink.isLight ? 0.60 : 0.45)), + in: shape) + .environment(\.colorScheme, scheme) + } else { + content.background { + shape.fill(.regularMaterial) + .environment(\.colorScheme, scheme) + .overlay { shape.fill(ink.glass(ink.isLight ? 0.55 : 0.40)) } + } + } + } +} + +extension View { + /// Palette-washed floating glass for the gamepad screens' own controls. Same fallback story + /// as `glassBackground` (`.regularMaterial` pre-26), plus the ink wash and scheme flip. + func consoleGlassBackground(_ shape: S, interactive: Bool = false) -> some View { + modifier(ConsoleGlassBackground(shape: shape, interactive: interactive)) + } +} diff --git a/clients/apple/Sources/PunktfunkKit/Audio/SessionAudio.swift b/clients/apple/Sources/PunktfunkKit/Audio/SessionAudio.swift index d79d12a1..60fd6ef4 100644 --- a/clients/apple/Sources/PunktfunkKit/Audio/SessionAudio.swift +++ b/clients/apple/Sources/PunktfunkKit/Audio/SessionAudio.swift @@ -483,19 +483,52 @@ public final class SessionAudio { } engine.attach(source) engine.connect(source, to: engine.mainMixerNode, format: format) - guard installMicTap(on: input, micUID: micUID, micChannel: micChannel) else { - // Mic chain unavailable (logged) — keep the session audible on the plain playback - // engine rather than playing through an idle voice processor. + + // The capture side must be PULLED, and only the render graph pulls anything. An input + // node carrying nothing but a tap is not part of that graph, so on the combined engine + // nobody drove it: the IO unit came up (the recording indicator lit for a beat, then went + // out as the input went idle) and NOT ONE BUFFER ever reached the tap — no error, no + // failed start, just a session that quietly sent no microphone at all. Routing the input + // through a silent sink puts it in the graph, which is what Apple's own voice-processing + // sample does. The split path never needed it: a capture-only engine has the input node + // AS its graph, so it is pulled by definition — which is why this only broke when the + // combined topology became the default. + // + // `outputVolume = 0` on the sink: the mic has to reach the graph, never the speaker. At + // any audible volume this is a microphone wired straight to the earpiece. + let micSink = AVAudioMixerNode() + engine.attach(micSink) + micSink.outputVolume = 0 + engine.connect(engine.inputNode, to: micSink, format: nil) + engine.connect(micSink, to: engine.mainMixerNode, format: nil) + + // BEFORE the tap reads a format. Enabling voice processing swaps the engine's IO unit + // for the VPIO one and renegotiates its formats, and until the engine is prepared the + // input node can still report the pre-swap state — 0 Hz / 0 channels included, which + // `installMicTap` (correctly) refuses as "no usable input device". Preparing first means + // the chain is built against what the voice processor will actually emit. + engine.prepare() + guard installMicTap(on: engine.inputNode, micUID: micUID, micChannel: micChannel) else { + // Mic chain unavailable on the VOICE-PROCESSED engine (logged). The mic outranks the + // echo cancellation, so fall back to the split path — its own engine, no voice + // processor, the topology that shipped before AEC existed — rather than dropping the + // uplink for the rest of the session. (The sibling failure above, where the voice + // processor won't engage at all, already does exactly this; this arm used to give up + // on the mic instead, which is how a whole session could go silent uplink-only.) + engine.stop() startPlayback(speakerUID: speakerUID) + startCapture(micUID: micUID, micChannel: micChannel) return } - engine.prepare() do { try engine.start() } catch { log.error("combined engine failed to start: \(error.localizedDescription)") - input.removeTap(onBus: 0) - startPlayback(speakerUID: speakerUID) // no echo cancellation beats no audio + engine.inputNode.removeTap(onBus: 0) + engine.stop() + // Same rule: a working mic without echo cancellation beats no mic at all. + startPlayback(speakerUID: speakerUID) + startCapture(micUID: micUID, micChannel: micChannel) return } stateLock.lock() @@ -533,8 +566,16 @@ public final class SessionAudio { } } #endif - guard installMicTap(on: input, micUID: micUID, micChannel: micChannel) else { return } + // Prepared before the tap reads a format, for the same reason the combined path does it: + // a node that hasn't been through `prepare()` can still report the pre-negotiation + // format (0 Hz / 0 channels on a device that is perfectly fine), which reads downstream + // as "no microphone". engine.prepare() + guard installMicTap(on: engine.inputNode, micUID: micUID, micChannel: micChannel) else { + log.error("mic uplink unavailable — this session sends no microphone audio") + engine.stop() + return + } do { try engine.start() } catch { diff --git a/clients/apple/Sources/PunktfunkKit/Gamepad/DeviceGyro.swift b/clients/apple/Sources/PunktfunkKit/Gamepad/DeviceGyro.swift new file mode 100644 index 00000000..442c6542 --- /dev/null +++ b/clients/apple/Sources/PunktfunkKit/Gamepad/DeviceGyro.swift @@ -0,0 +1,201 @@ +// The opt-in phone-gyro mirror (`DefaultsKey.gyroFromDevice`): when player 1's forwarded +// controller has no rotation sensor of its own, THIS device's IMU speaks for it on the wire's +// motion plane — for clip-on and third-party pads that ship without a gyro, where the phone +// body is rigidly attached to (or simply is) the thing in the player's hands. The sibling of +// `GamepadFeedback`'s rumble-on-device mirror, with the data flowing the other way. +// +// GamepadCapture owns the engage/stand-down decision (it knows the pad-0 slot and whether its +// controller reports a rotation rate); this class only turns CoreMotion on and off and converts +// samples. Two invariants it enforces itself: +// - one motion writer per pad: samples go out only between `start` and `stop`, and capture +// suppresses pad 0's controller-motion forwarding while this runs; +// - no stale rotation: `stop` sends a single zero-gyro sample after the last real one, so the +// host's virtual pad never keeps integrating an angular velocity this device stopped +// producing (the gyro-sweep "stale angular velocity re-sent forever" failure mode). +// +// Samples are CMDeviceMotion (sensor-fused: bias-corrected rotation rate, gravity split from +// user acceleration) at the ~100 Hz CoreMotion ceiling — below a DualSense's 250 Hz, but the +// host's motion plane is event-driven, not cadence-locked, so a slower producer just means +// fewer samples. Units and axis semantics match `GamepadCapture.forwardMotion` exactly (the +// `GamepadWire` constants; accel = gravity + user acceleration — the same convention, so a +// future sign/scale correction lands in one place for both sources). The one thing the phone +// adds is a frame remap: CoreMotion reports in the device's portrait frame, while the wire +// wants the controller frame the player sees (x right, y up, z out of the screen), so each +// sample is rotated by the current interface orientation — a phone clipped landscape must yaw +// when the player yaws, not roll. + +#if os(iOS) +import CoreMotion +import Foundation +import UIKit + +/// Device-frame → controller-frame axis remap for one interface orientation. CoreMotion's +/// frame is fixed to the portrait device (+x right edge, +y top, +z out of the screen); the +/// controller frame keeps +z (the screen always faces the player) and rotates x/y so they +/// mean "player's right" and "player's up". Derived, like the wire scale constants — pinned +/// by `DeviceGyroRemapTests`, correctable in one place if on-glass says otherwise. +/// File-scope rather than nested so the sample thread can use it without actor isolation. +enum DeviceGyroRemap { + case identity + /// Upside-down portrait: both in-plane axes flip. + case flipped + /// Landscape, device top to the player's LEFT (interface `.landscapeRight`): + /// player-right = device-bottom, player-up = device-right. + case topLeft + /// Landscape, device top to the player's RIGHT (interface `.landscapeLeft`). + case topRight + + init(_ orientation: UIInterfaceOrientation) { + switch orientation { + case .portraitUpsideDown: self = .flipped + case .landscapeRight: self = .topLeft + case .landscapeLeft: self = .topRight + default: self = .identity + } + } + + /// Rotate one device-frame vector (rotation rate or acceleration — both transform the + /// same way under an in-plane rotation) into the controller frame. + func apply(x: Float, y: Float, z: Float) -> (x: Float, y: Float, z: Float) { + switch self { + case .identity: return (x, y, z) + case .flipped: return (-x, -y, z) + case .topLeft: return (-y, x, z) + case .topRight: return (y, -x, z) + } + } +} + +@MainActor +public final class DeviceGyro { + /// Whether this device can source motion at all — gates the settings rows (a device + /// without an IMU would make the toggle a silent no-op, the rumble mirror's rule). + /// One shared probe: Apple recommends a single `CMMotionManager` per app, and the + /// settings UI asking per-render must not allocate one each time. + public static let isAvailable: Bool = CMMotionManager().isDeviceMotionAvailable + + /// Everything the sample thread touches, behind one lock: the orientation remap (written + /// on main when the device rotates), the last converted accel, and whether a real sample + /// went out (so `stop` knows it owes the wire a zero). Kept off the actor deliberately — + /// `forward` runs on the delivery queue. + private final class SampleState: @unchecked Sendable { + let lock = NSLock() + var remap: DeviceGyroRemap = .identity + var sentSample = false + /// Re-sent with the closing zero-gyro sample so "rotation stopped" doesn't also + /// overwrite a plausible gravity vector with free-fall. + var lastAccel: (Int16, Int16, Int16) = (0, 0, 0) + } + + /// Ship one converted sample (wire pad 0). Must be thread-safe — invoked from the + /// delivery queue (`PunktfunkConnection.sendMotion` locks internally). + private let send: @Sendable (_ gyro: (Int16, Int16, Int16), _ accel: (Int16, Int16, Int16)) -> Void + + private let motion = CMMotionManager() + /// Dedicated serial delivery queue — deliberately NOT main (the controller path's + /// main-queue delivery is a known jitter source; the mirror starts clean). + private let queue: OperationQueue = { + let q = OperationQueue() + q.name = "punktfunk.device-gyro" + q.maxConcurrentOperationCount = 1 + return q + }() + + private let state = SampleState() + private var orientationObserver: NSObjectProtocol? + + /// Whether the mirror is between `start` and `stop` — read by GamepadCapture to keep the + /// controller path off pad 0's motion while this runs. + public private(set) var isRunning = false + + public init( + send: @escaping @Sendable (_ gyro: (Int16, Int16, Int16), _ accel: (Int16, Int16, Int16)) -> Void + ) { + self.send = send + } + + /// Begin sourcing pad-0 motion from this device. Idempotent. + public func start() { + guard !isRunning, motion.isDeviceMotionAvailable else { return } + isRunning = true + updateRemap() + // Interface orientation only changes alongside a device-orientation notification, so + // this is the one signal needed; re-reading the scene keeps a rotation lock stable. + orientationObserver = NotificationCenter.default.addObserver( + forName: UIDevice.orientationDidChangeNotification, object: nil, queue: .main + ) { [weak self] _ in + MainActor.assumeIsolated { self?.updateRemap() } + } + // CoreMotion's practical ceiling; requesting faster just clamps. + motion.deviceMotionUpdateInterval = 1.0 / 100.0 + motion.startDeviceMotionUpdates(to: queue) { [state, send] m, _ in + guard let m else { return } + Self.forward(m, state: state, send: send) + } + } + + /// Stop sourcing and, if anything was sent, park the host pad's rotation at zero. The + /// zero rides the same serial queue as the samples, so it is guaranteed last — without + /// blocking the caller. + public func stop() { + guard isRunning else { return } + isRunning = false + motion.stopDeviceMotionUpdates() + if let o = orientationObserver { + NotificationCenter.default.removeObserver(o) + orientationObserver = nil + } + queue.addOperation { [state, send] in + state.lock.lock() + let owed = state.sentSample + state.sentSample = false + let accel = state.lastAccel + state.lock.unlock() + if owed { send((0, 0, 0), accel) } + } + } + + private func updateRemap() { + let o = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .first?.interfaceOrientation ?? .portrait + state.lock.lock() + state.remap = DeviceGyroRemap(o) + state.lock.unlock() + } + + /// Runs on the delivery queue: remap, scale, ship. + nonisolated private static func forward( + _ m: CMDeviceMotion, state: SampleState, + send: (_ gyro: (Int16, Int16, Int16), _ accel: (Int16, Int16, Int16)) -> Void + ) { + state.lock.lock() + let r = state.remap + state.lock.unlock() + let rot = r.apply( + x: Float(m.rotationRate.x), y: Float(m.rotationRate.y), z: Float(m.rotationRate.z)) + // Same total-acceleration convention as GamepadCapture.forwardMotion. + let acc = r.apply( + x: Float(m.gravity.x + m.userAcceleration.x), + y: Float(m.gravity.y + m.userAcceleration.y), + z: Float(m.gravity.z + m.userAcceleration.z)) + let gs = GamepadWire.gyroLSBPerRadS + let as_ = GamepadWire.accelLSBPerG + let gyro = ( + GamepadWire.motionRaw(rot.x, scale: gs), + GamepadWire.motionRaw(rot.y, scale: gs), + GamepadWire.motionRaw(rot.z, scale: gs) + ) + let accel = ( + GamepadWire.motionRaw(acc.x, scale: as_), + GamepadWire.motionRaw(acc.y, scale: as_), + GamepadWire.motionRaw(acc.z, scale: as_) + ) + state.lock.lock() + state.lastAccel = accel + state.sentSample = true + state.lock.unlock() + send(gyro, accel) + } +} +#endif diff --git a/clients/apple/Sources/PunktfunkKit/Gamepad/GamepadCapture.swift b/clients/apple/Sources/PunktfunkKit/Gamepad/GamepadCapture.swift index 4ea7812b..0cb3b6db 100644 --- a/clients/apple/Sources/PunktfunkKit/Gamepad/GamepadCapture.swift +++ b/clients/apple/Sources/PunktfunkKit/Gamepad/GamepadCapture.swift @@ -67,6 +67,13 @@ public final class GamepadCapture { var axes: [Int32] = [0, 0, 0, 0, 0, 0] var fingerActive: [Bool] = [false, false] var lastMotionNs: UInt64 = 0 + /// A motion sample went out on this pad — `flush` then owes the wire a zero-gyro + /// sample: the host holds motion as STATE and re-emits it, so a nonzero angular + /// velocity left behind reads as endless rotation (the gyro-sweep latch). + var motionSent = false + /// The last accel sent, re-used by the flush zero so "rotation stopped" doesn't + /// also replace a plausible gravity vector with free-fall. + var lastAccel: (Int16, Int16, Int16) = (0, 0, 0) // Hold-Select→guide gesture state (pf-client-core's `SelectGesture`, adapted to // this class's mask-diff model): a Select pressed ALONE is held out of the mask // until it resolves into a tap (delivered on release) or — past `guideHold` — a @@ -153,6 +160,15 @@ public final class GamepadCapture { /// everywhere but macOS). See `guideHold`. public let guideGesture: Bool + #if os(iOS) + /// Opt-in phone-gyro mirror (`DefaultsKey.gyroFromDevice`): while player 1's forwarded + /// controller has no rotation sensor, this device's IMU sources pad 0's motion instead — + /// for clip-on pads without a gyro. Session-scoped (the setting is read once here); nil + /// when off, unavailable, or forwarding is off (the mirror is wire-only, so with nothing + /// to send there is nothing to mirror). Engage/stand-down lives in `updateDeviceGyro`. + private let deviceGyro: DeviceGyro? + #endif + public init( connection: PunktfunkConnection, manager: GamepadManager, forwarding: Bool = true, systemForward: Bool = true, guideGesture: Bool = false @@ -162,6 +178,17 @@ public final class GamepadCapture { self.forwarding = forwarding self.systemForward = systemForward self.guideGesture = guideGesture + #if os(iOS) + if forwarding, DeviceGyro.isAvailable, + UserDefaults.standard.bool(forKey: DefaultsKey.gyroFromDevice) { + deviceGyro = DeviceGyro { [weak connection] gyro, accel in + // Thread-safe (sendMotion locks); pad 0 by the same rule as the rumble mirror. + connection?.sendMotion(pad: 0, gyro: gyro, accel: accel) + } + } else { + deviceGyro = nil + } + #endif } public func start() { @@ -187,6 +214,9 @@ public final class GamepadCapture { MainActor.assumeIsolated { self?.suspended = true self?.releaseAll() + // The mirror pauses with capture (its stop parks the host pad's rotation + // at zero — an overlay pull-down must not leave the game spinning). + self?.updateDeviceGyro() } }) observers.append(NotificationCenter.default.addObserver( @@ -199,11 +229,15 @@ public final class GamepadCapture { for slot in self.slots { if let ext = slot.controller.extendedGamepad { self.sync(slot, ext) } } + self.updateDeviceGyro() } }) } public func stop() { + #if os(iOS) + deviceGyro?.stop() + #endif closeAllSlots() forwardedSub = nil observers.forEach { NotificationCenter.default.removeObserver($0) } @@ -224,6 +258,8 @@ public final class GamepadCapture { } // A chord-holding pad may have just unplugged — re-evaluate so a stale hold disarms. updateEscapeChord() + // Pad 0 may have changed hands — re-evaluate whether this device's IMU speaks for it. + updateDeviceGyro() } /// Open one forwarded controller on its assigned wire index: attach GC handlers, claim its @@ -561,6 +597,13 @@ public final class GamepadCapture { private func forwardMotion(_ slot: Slot, _ m: GCMotion) { guard !suspended else { return } + #if os(iOS) + // While the phone-gyro mirror speaks for pad 0, the controller's own motion — + // necessarily rotation-less, that's the engage condition — stays off the wire: + // two writers on one pad's motion state would fight, and this accel-only stream + // would keep stomping the mirror's gyro with zeros. + if slot.pad == 0, deviceGyro?.isRunning == true { return } + #endif let now = DispatchTime.now().uptimeNanoseconds guard now &- slot.lastMotionNs >= Self.motionIntervalNs else { return } slot.lastMotionNs = now @@ -579,18 +622,35 @@ public final class GamepadCapture { } let gs = GamepadWire.gyroLSBPerRadS let as_ = GamepadWire.accelLSBPerG - wire?.sendMotion( - pad: UInt8(slot.pad), - gyro: ( - GamepadWire.motionRaw(Float(m.rotationRate.x), scale: gs), - GamepadWire.motionRaw(Float(m.rotationRate.y), scale: gs), - GamepadWire.motionRaw(Float(m.rotationRate.z), scale: gs) - ), - accel: ( - GamepadWire.motionRaw(ax, scale: as_), - GamepadWire.motionRaw(ay, scale: as_), - GamepadWire.motionRaw(az, scale: as_) - )) + let gyro = ( + GamepadWire.motionRaw(Float(m.rotationRate.x), scale: gs), + GamepadWire.motionRaw(Float(m.rotationRate.y), scale: gs), + GamepadWire.motionRaw(Float(m.rotationRate.z), scale: gs) + ) + let accel = ( + GamepadWire.motionRaw(ax, scale: as_), + GamepadWire.motionRaw(ay, scale: as_), + GamepadWire.motionRaw(az, scale: as_) + ) + if wire != nil { + slot.motionSent = true + slot.lastAccel = accel + } + wire?.sendMotion(pad: UInt8(slot.pad), gyro: gyro, accel: accel) + } + + /// Engage or stand down the phone-gyro mirror: it speaks for pad 0 exactly while a + /// forwarded controller holds that index but can't rotate for itself — no `GCMotion`, + /// or a motion object without a rotation rate (gravity-only pads, e.g. an Xbox pad on + /// iOS). Re-evaluated on every reconcile and on suspend/resume; `DeviceGyro.stop` + /// parks the host pad's rotation at zero, so standing down never strands a spin. + private func updateDeviceGyro() { + #if os(iOS) + guard let gyro = deviceGyro else { return } + let pad0 = slots.first { $0.pad == 0 } + let wants = !suspended && pad0 != nil && pad0!.controller.motion?.hasRotationRate != true + if wants { gyro.start() } else { gyro.stop() } + #endif } /// Arm the disconnect timer when ANY forwarded pad holds the full escape chord, disarm the @@ -634,6 +694,14 @@ public final class GamepadCapture { wire?.sendTouchpad(pad: UInt8(slot.pad), finger: UInt8(f), active: false, x: 0, y: 0) slot.fingerActive[f] = false } + // Motion is host-side STATE, re-emitted until replaced — a nonzero angular velocity + // left behind reads as endless rotation (the gyro-sweep latch: Control Center + // pull-down froze the last sample for as long as the overlay stayed up). Rest means + // zero rotation; the last accel is kept so gravity doesn't become free-fall. + if slot.motionSent { + slot.motionSent = false + wire?.sendMotion(pad: UInt8(slot.pad), gyro: (0, 0, 0), accel: slot.lastAccel) + } } /// Flush every open slot's held state (app deactivation) — keeps the slots open (GC just stops diff --git a/clients/apple/Sources/PunktfunkKit/Resources/THIRD-PARTY-NOTICES.txt b/clients/apple/Sources/PunktfunkKit/Resources/THIRD-PARTY-NOTICES.txt index fecd7fe9..5646bef3 100644 --- a/clients/apple/Sources/PunktfunkKit/Resources/THIRD-PARTY-NOTICES.txt +++ b/clients/apple/Sources/PunktfunkKit/Resources/THIRD-PARTY-NOTICES.txt @@ -7,7 +7,9 @@ below. Each is distributed under its own permissive license; the full license te follow the manifest. This file is generated by scripts/gen-third-party-notices.py (or `cargo about`, see about.toml) — do not edit by hand. -Total third-party crates: 566 +Scope: the Rust crates linked by punktfunk-core — not the whole punktfunk workspace. + +Total third-party crates: 256 ---------------------------------------------------------------------------- VENDORED THIRD-PARTY SOURCE (inside first-party crates) @@ -23,65 +25,30 @@ VENDORED THIRD-PARTY SOURCE (inside first-party crates) ---------------------------------------------------------------------------- MANIFEST (crate version — SPDX license — source) ---------------------------------------------------------------------------- - adler2 2.0.1 — 0BSD OR MIT OR Apache-2.0 — https://github.com/oyvindln/adler2 aead 0.5.2 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits aes 0.8.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/block-ciphers aes-gcm 0.10.3 — Apache-2.0 OR MIT — https://github.com/RustCrypto/AEADs aho-corasick 1.1.4 — Unlicense OR MIT — https://github.com/BurntSushi/aho-corasick - android_log-sys 0.3.2 — MIT OR Apache-2.0 — https://github.com/rust-mobile/android_log-sys-rs - android_logger 0.14.1 — MIT OR Apache-2.0 — https://github.com/rust-mobile/android_logger-rs anes 0.1.6 — MIT OR Apache-2.0 — https://github.com/zrzka/anes-rs - annotate-snippets 0.11.5 — MIT OR Apache-2.0 — https://github.com/rust-lang/annotate-snippets-rs anstream 1.0.0 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git anstyle 1.0.14 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git anstyle-parse 1.0.0 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git anstyle-query 1.1.5 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git anstyle-wincon 3.0.11 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git - anyhow 1.0.103 — MIT OR Apache-2.0 — https://github.com/dtolnay/anyhow - ash 0.38.0+1.3.281 — MIT OR Apache-2.0 — https://github.com/ash-rs/ash - ashpd 0.13.12 — MIT — https://github.com/bilelmoussaoui/ashpd - asn1-rs 0.6.2 — MIT OR Apache-2.0 — https://github.com/rusticata/asn1-rs.git - asn1-rs-derive 0.5.1 — MIT OR Apache-2.0 — https://github.com/rusticata/asn1-rs.git - asn1-rs-impl 0.2.0 — MIT/Apache-2.0 — https://github.com/rusticata/asn1-rs.git - async-broadcast 0.7.2 — MIT OR Apache-2.0 — https://github.com/smol-rs/async-broadcast - async-channel 2.5.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-channel - async-executor 1.14.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-executor - async-io 2.6.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-io - async-lock 3.4.2 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-lock - async-process 2.5.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-process - async-recursion 1.1.1 — MIT OR Apache-2.0 — https://github.com/dcchut/async-recursion - async-signal 0.2.14 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-signal - async-task 4.7.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-task - async-trait 0.1.89 — MIT OR Apache-2.0 — https://github.com/dtolnay/async-trait - atomic-waker 1.1.2 — Apache-2.0 OR MIT — https://github.com/smol-rs/atomic-waker audiopus_sys 0.2.2 — ISC — https://github.com/lakelezz/audiopus_sys.git autocfg 1.5.1 — Apache-2.0 OR MIT — https://github.com/cuviper/autocfg - axum 0.8.9 — MIT — https://github.com/tokio-rs/axum - axum-core 0.5.6 — MIT — https://github.com/tokio-rs/axum - axum-server 0.8.0 — MIT — https://github.com/programatik29/axum-server base64 0.22.1 — MIT OR Apache-2.0 — https://github.com/marshallpierce/rust-base64 - base64ct 1.8.3 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats - bindgen 0.72.1 — BSD-3-Clause — https://github.com/rust-lang/rust-bindgen bit-set 0.8.0 — Apache-2.0 OR MIT — https://github.com/contain-rs/bit-set bit-vec 0.8.0 — Apache-2.0 OR MIT — https://github.com/contain-rs/bit-vec bitflags 2.13.0 — MIT OR Apache-2.0 — https://github.com/bitflags/bitflags block-buffer 0.10.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils block-padding 0.3.3 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils - blocking 1.6.2 — Apache-2.0 OR MIT — https://github.com/smol-rs/blocking bumpalo 3.20.3 — MIT OR Apache-2.0 — https://github.com/fitzgen/bumpalo - bytemuck 1.25.0 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/bytemuck - bytemuck_derive 1.10.2 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/bytemuck - byteorder-lite 0.1.0 — Unlicense OR MIT — https://github.com/image-rs/byteorder-lite bytes 1.12.0 — MIT — https://github.com/tokio-rs/bytes - cairo-rs 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core - cairo-sys-rs 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core cast 0.3.0 — MIT OR Apache-2.0 — https://github.com/japaric/cast.rs - cbc 0.1.2 — MIT OR Apache-2.0 — https://github.com/RustCrypto/block-modes cbindgen 0.29.4 — MPL-2.0 — https://github.com/mozilla/cbindgen cc 1.2.65 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs cesu8 1.1.0 — Apache-2.0/MIT — https://github.com/emk/cesu8-rs - cexpr 0.6.0 — Apache-2.0/MIT — https://github.com/jethrogb/rust-cexpr - cfg-expr 0.20.8 — MIT OR Apache-2.0 — https://github.com/EmbarkStudios/cfg-expr cfg-if 1.0.4 — MIT OR Apache-2.0 — https://github.com/rust-lang/cfg-if cfg_aliases 0.2.1 — MIT — https://github.com/katharostech/cfg_aliases chacha20 0.9.1 — Apache-2.0 OR MIT — https://github.com/RustCrypto/stream-ciphers @@ -90,136 +57,62 @@ MANIFEST (crate version — SPDX license — source) ciborium-io 0.2.2 — Apache-2.0 — https://github.com/enarx/ciborium ciborium-ll 0.2.2 — Apache-2.0 — https://github.com/enarx/ciborium cipher 0.4.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits - clang-sys 1.8.1 — Apache-2.0 — https://github.com/KyleMayes/clang-sys clap 4.6.1 — MIT OR Apache-2.0 — https://github.com/clap-rs/clap clap_builder 4.6.0 — MIT OR Apache-2.0 — https://github.com/clap-rs/clap clap_lex 1.1.0 — MIT OR Apache-2.0 — https://github.com/clap-rs/clap cmake 0.1.58 — MIT OR Apache-2.0 — https://github.com/rust-lang/cmake-rs - color_quant 1.1.0 — MIT — https://github.com/image-rs/color_quant.git colorchoice 1.0.5 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git combine 4.6.7 — MIT — https://github.com/Marwes/combine - concurrent-queue 2.5.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/concurrent-queue const-oid 0.9.6 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/const-oid - convert_case 0.8.0 — MIT — https://github.com/rutrum/convert-case - cookie-factory 0.3.3 — MIT — https://github.com/rust-bakery/cookie-factory core-foundation 0.10.1 — MIT OR Apache-2.0 — https://github.com/servo/core-foundation-rs core-foundation-sys 0.8.7 — MIT OR Apache-2.0 — https://github.com/servo/core-foundation-rs cpufeatures 0.2.17 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils - crc32fast 1.5.0 — MIT OR Apache-2.0 — https://github.com/srijs/rust-crc32fast criterion 0.5.1 — Apache-2.0 OR MIT — https://github.com/bheisler/criterion.rs criterion-plot 0.5.0 — MIT/Apache-2.0 — https://github.com/bheisler/criterion.rs + crossbeam-deque 0.8.6 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam + crossbeam-epoch 0.9.20 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam crossbeam-utils 0.8.21 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam crunchy 0.2.4 — MIT — https://github.com/eira-fransham/crunchy crypto-common 0.1.7 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits ctr 0.9.2 — MIT OR Apache-2.0 — https://github.com/RustCrypto/block-modes curve25519-dalek 4.1.3 — BSD-3-Clause — https://github.com/dalek-cryptography/curve25519-dalek/tree/main/curve25519-dalek curve25519-dalek-derive 0.1.1 — MIT/Apache-2.0 — https://github.com/dalek-cryptography/curve25519-dalek - data-encoding 2.11.0 — MIT — https://github.com/ia0/data-encoding - der 0.7.10 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/der - der-parser 9.0.0 — MIT/Apache-2.0 — https://github.com/rusticata/der-parser.git deranged 0.5.8 — MIT OR Apache-2.0 — https://github.com/jhpratt/deranged digest 0.10.7 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits - displaydoc 0.2.6 — MIT OR Apache-2.0 — https://github.com/yaahc/displaydoc - downcast-rs 1.2.1 — MIT/Apache-2.0 — https://github.com/marcianx/downcast-rs either 1.16.0 — MIT OR Apache-2.0 — https://github.com/rayon-rs/either - endi 1.1.1 — MIT — https://github.com/zeenix/endi - enumflags2 0.7.12 — MIT OR Apache-2.0 — https://github.com/meithecatte/enumflags2 - enumflags2_derive 0.7.12 — MIT OR Apache-2.0 — https://github.com/meithecatte/enumflags2 - env_filter 0.1.4 — MIT OR Apache-2.0 — https://github.com/rust-cli/env_logger equivalent 1.0.2 — Apache-2.0 OR MIT — https://github.com/indexmap-rs/equivalent errno 0.3.14 — MIT OR Apache-2.0 — https://github.com/lambda-fairy/rust-errno - event-listener 5.4.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/event-listener - event-listener-strategy 0.5.4 — Apache-2.0 OR MIT — https://github.com/smol-rs/event-listener-strategy - fallible-iterator 0.3.0 — MIT/Apache-2.0 — https://github.com/sfackler/rust-fallible-iterator - fallible-streaming-iterator 0.1.9 — MIT/Apache-2.0 — https://github.com/sfackler/fallible-streaming-iterator fastbloom 0.14.1 — MIT OR Apache-2.0 — https://github.com/tomtomwombat/fastbloom/ fastrand 2.4.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/fastrand - fdeflate 0.3.7 — MIT OR Apache-2.0 — https://github.com/image-rs/fdeflate - ffmpeg-next 8.1.0 — WTFPL — https://github.com/zmwangx/rust-ffmpeg - ffmpeg-sys-next 8.1.0 — WTFPL — https://github.com/zmwangx/rust-ffmpeg-sys fiat-crypto 0.2.9 — MIT OR Apache-2.0 OR BSD-1-Clause — https://github.com/mit-plv/fiat-crypto - field-offset 0.3.6 — MIT OR Apache-2.0 — https://github.com/Diggsey/rust-field-offset - filetime 0.2.29 — MIT/Apache-2.0 — https://github.com/alexcrichton/filetime find-msvc-tools 0.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs fixedbitset 0.5.7 — MIT OR Apache-2.0 — https://github.com/petgraph/fixedbitset - flate2 1.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/flate2-rs - flume 0.12.0 — Apache-2.0/MIT — https://github.com/zesterer/flume fnv 1.0.7 — Apache-2.0 / MIT — https://github.com/servo/rust-fnv foldhash 0.2.0 — Zlib — https://github.com/orlp/foldhash - form_urlencoded 1.2.2 — MIT OR Apache-2.0 — https://github.com/servo/rust-url - fragile 2.1.0 — Apache-2.0 — https://github.com/mitsuhiko/fragile - fs-err 3.3.0 — MIT OR Apache-2.0 — https://github.com/andrewhickman/fs-err - futures 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs futures-channel 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs futures-core 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs - futures-executor 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs futures-io 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs - futures-lite 2.6.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/futures-lite futures-macro 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs futures-sink 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs futures-task 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs futures-util 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs - gdk-pixbuf 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core - gdk-pixbuf-sys 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core - gdk4 0.11.2 — MIT — https://github.com/gtk-rs/gtk4-rs - gdk4-sys 0.11.2 — MIT — https://github.com/gtk-rs/gtk4-rs generic-array 0.14.7 — MIT — https://github.com/fizyk20/generic-array.git - gethostname 1.1.0 — Apache-2.0 — https://codeberg.org/swsnr/gethostname.rs.git getrandom 0.2.17 — MIT OR Apache-2.0 — https://github.com/rust-random/getrandom getrandom 0.3.4 — MIT OR Apache-2.0 — https://github.com/rust-random/getrandom getrandom 0.4.3 — MIT OR Apache-2.0 — https://github.com/rust-random/getrandom ghash 0.5.1 — Apache-2.0 OR MIT — https://github.com/RustCrypto/universal-hashes - gif 0.14.2 — MIT OR Apache-2.0 — https://github.com/image-rs/image-gif - gio 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core - gio-sys 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core - glib 0.22.7 — MIT — https://github.com/gtk-rs/gtk-rs-core - glib-build-tools 0.22.8 — MIT — https://github.com/gtk-rs/gtk-rs-core - glib-macros 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core - glib-sys 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core - glob 0.3.3 — MIT OR Apache-2.0 — https://github.com/rust-lang/glob - gobject-sys 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core - graphene-rs 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core - graphene-sys 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core - gsk4 0.11.1 — MIT — https://github.com/gtk-rs/gtk4-rs - gsk4-sys 0.11.1 — MIT — https://github.com/gtk-rs/gtk4-rs - gtk4 0.11.3 — MIT — https://github.com/gtk-rs/gtk4-rs - gtk4-macros 0.11.0 — MIT — https://github.com/gtk-rs/gtk4-rs - gtk4-sys 0.11.3 — MIT — https://github.com/gtk-rs/gtk4-rs - h2 0.4.15 — MIT — https://github.com/hyperium/h2 half 2.7.1 — MIT OR Apache-2.0 — https://github.com/VoidStarKat/half-rs - hashbrown 0.16.1 — MIT OR Apache-2.0 — https://github.com/rust-lang/hashbrown hashbrown 0.17.1 — MIT OR Apache-2.0 — https://github.com/rust-lang/hashbrown - hashlink 0.12.0 — MIT OR Apache-2.0 — https://github.com/djc/hashlink heck 0.5.0 — MIT OR Apache-2.0 — https://github.com/withoutboats/heck hermit-abi 0.5.2 — MIT OR Apache-2.0 — https://github.com/hermit-os/hermit-rs - hex 0.4.3 — MIT OR Apache-2.0 — https://github.com/KokaKiwi/rust-hex hkdf 0.12.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/KDFs/ hmac 0.12.1 — MIT OR Apache-2.0 — https://github.com/RustCrypto/MACs - http 1.4.2 — MIT OR Apache-2.0 — https://github.com/hyperium/http - http-body 1.0.1 — MIT — https://github.com/hyperium/http-body - http-body-util 0.1.3 — MIT — https://github.com/hyperium/http-body - httparse 1.10.1 — MIT OR Apache-2.0 — https://github.com/seanmonstar/httparse - httpdate 1.0.3 — MIT OR Apache-2.0 — https://github.com/pyfisch/httpdate - hyper 1.10.1 — MIT — https://github.com/hyperium/hyper - hyper-util 0.1.20 — MIT — https://github.com/hyperium/hyper-util - icu_collections 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x - icu_locale_core 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x - icu_normalizer 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x - icu_normalizer_data 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x - icu_properties 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x - icu_properties_data 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x - icu_provider 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x - idna 1.1.0 — MIT OR Apache-2.0 — https://github.com/servo/rust-url/ - idna_adapter 1.2.2 — Apache-2.0 OR MIT — https://github.com/hsivonen/idna_adapter if-addrs 0.13.4 — MIT OR BSD-3-Clause — https://github.com/messense/if-addrs - if-addrs 0.15.0 — MIT OR BSD-3-Clause — https://github.com/messense/if-addrs - image 0.25.10 — MIT OR Apache-2.0 — https://github.com/image-rs/image indexmap 2.14.0 — Apache-2.0 OR MIT — https://github.com/indexmap-rs/indexmap inout 0.1.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils is-terminal 0.4.17 — MIT — https://github.com/sunfishcode/is-terminal is_terminal_polyfill 1.70.2 — MIT OR Apache-2.0 — https://github.com/polyfill-rs/is_terminal_polyfill itertools 0.10.5 — MIT/Apache-2.0 — https://github.com/rust-itertools/itertools - itertools 0.13.0 — MIT OR Apache-2.0 — https://github.com/rust-itertools/itertools itoa 1.0.18 — MIT OR Apache-2.0 — https://github.com/dtolnay/itoa jni 0.21.1 — MIT/Apache-2.0 — https://github.com/jni-rs/jni-rs jni-sys 0.3.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys @@ -227,107 +120,47 @@ MANIFEST (crate version — SPDX license — source) jni-sys-macros 0.4.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys jobserver 0.1.34 — MIT OR Apache-2.0 — https://github.com/rust-lang/jobserver-rs js-sys 0.3.103 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/js-sys - khronos-egl 6.0.0 — MIT/Apache-2.0 — https://github.com/timothee-haudebourg/khronos-egl - ksni 0.3.5 — Unlicense — https://github.com/iovxw/ksni - lazy_static 1.5.0 — MIT OR Apache-2.0 — https://github.com/rust-lang-nursery/lazy-static.rs - libadwaita 0.9.1 — MIT — https://gitlab.gnome.org/World/Rust/libadwaita-rs - libadwaita-sys 0.9.1 — MIT — https://gitlab.gnome.org/World/Rust/libadwaita-rs libc 0.2.186 — MIT OR Apache-2.0 — https://github.com/rust-lang/libc - libloading 0.8.9 — ISC — https://github.com/nagisa/rust_libloading/ libm 0.2.16 — MIT — https://github.com/rust-lang/compiler-builtins - libspa 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs - libspa-sys 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs - libsqlite3-sys 0.38.1 — MIT — https://github.com/rusqlite/rusqlite linux-raw-sys 0.12.1 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/sunfishcode/linux-raw-sys - litemap 0.8.2 — Unicode-3.0 — https://github.com/unicode-org/icu4x lock_api 0.4.14 — MIT OR Apache-2.0 — https://github.com/Amanieu/parking_lot log 0.4.33 — MIT OR Apache-2.0 — https://github.com/rust-lang/log lru-slab 0.1.2 — MIT OR Apache-2.0 OR Zlib — https://github.com/Ralith/lru-slab - mac_address 1.1.8 — MIT OR Apache-2.0 — https://github.com/rep-nop/mac_address - matchers 0.2.0 — MIT — https://github.com/hawkw/matchers - matchit 0.8.4 — MIT AND BSD-3-Clause — https://github.com/ibraheemdev/matchit - mdns-sd 0.20.1 — Apache-2.0 OR MIT — https://github.com/keepsimple1/mdns-sd memchr 2.8.2 — Unlicense OR MIT — https://github.com/BurntSushi/memchr - memmap2 0.9.11 — MIT OR Apache-2.0 — https://github.com/RazrFalcon/memmap2-rs - memoffset 0.9.1 — MIT — https://github.com/Gilnaa/memoffset - mime 0.3.17 — MIT OR Apache-2.0 — https://github.com/hyperium/mime - minimal-lexical 0.2.1 — MIT/Apache-2.0 — https://github.com/Alexhuszagh/minimal-lexical - miniz_oxide 0.8.9 — MIT OR Zlib OR Apache-2.0 — https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide mio 1.2.1 — MIT — https://github.com/tokio-rs/mio - moxcms 0.8.1 — BSD-3-Clause OR Apache-2.0 — https://github.com/awxkee/moxcms.git - nasm-rs 0.3.2 — MIT OR Apache-2.0 — https://github.com/medek/nasm-rs - ndk 0.9.0 — MIT OR Apache-2.0 — https://github.com/rust-mobile/ndk - ndk-sys 0.6.0+11769913 — MIT OR Apache-2.0 — https://github.com/rust-mobile/ndk - nix 0.29.0 — MIT — https://github.com/nix-rust/nix - nix 0.30.1 — MIT — https://github.com/nix-rust/nix - nom 7.1.3 — MIT — https://github.com/Geal/nom - nom 8.0.0 — MIT — https://github.com/rust-bakery/nom - nu-ansi-term 0.50.3 — MIT — https://github.com/nushell/nu-ansi-term - num-bigint 0.4.6 — MIT OR Apache-2.0 — https://github.com/rust-num/num-bigint - num-bigint-dig 0.8.6 — MIT/Apache-2.0 — https://github.com/dignifiedquire/num-bigint num-conv 0.2.2 — MIT OR Apache-2.0 — https://github.com/jhpratt/num-conv - num-derive 0.4.2 — MIT OR Apache-2.0 — https://github.com/rust-num/num-derive - num-integer 0.1.46 — MIT OR Apache-2.0 — https://github.com/rust-num/num-integer - num-iter 0.1.45 — MIT OR Apache-2.0 — https://github.com/rust-num/num-iter num-traits 0.2.19 — MIT OR Apache-2.0 — https://github.com/rust-num/num-traits - num_cpus 1.17.0 — MIT OR Apache-2.0 — https://github.com/seanmonstar/num_cpus - num_enum 0.7.6 — BSD-3-Clause OR MIT OR Apache-2.0 — https://github.com/illicitonion/num_enum - num_enum_derive 0.7.6 — BSD-3-Clause OR MIT OR Apache-2.0 — https://github.com/illicitonion/num_enum - oid-registry 0.7.1 — MIT OR Apache-2.0 — https://github.com/rusticata/oid-registry.git once_cell 1.21.4 — MIT OR Apache-2.0 — https://github.com/matklad/once_cell once_cell_polyfill 1.70.2 — MIT OR Apache-2.0 — https://github.com/polyfill-rs/once_cell_polyfill oorandom 11.1.5 — MIT — https://hg.sr.ht/~icefox/oorandom opaque-debug 0.3.1 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils - openh264 0.9.3 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs - openh264-sys2 0.9.6 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs openssl-probe 0.2.1 — MIT OR Apache-2.0 — https://github.com/rustls/openssl-probe opus 0.3.1 — MIT/Apache-2.0 — https://github.com/SpaceManiac/opus-rs - ordered-stream 0.2.0 — MIT OR Apache-2.0 — https://github.com/danieldg/ordered-stream - pango 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core - pango-sys 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core - parking 2.2.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/parking parking_lot 0.12.5 — MIT OR Apache-2.0 — https://github.com/Amanieu/parking_lot parking_lot_core 0.9.12 — MIT OR Apache-2.0 — https://github.com/Amanieu/parking_lot - paste 1.0.15 — MIT OR Apache-2.0 — https://github.com/dtolnay/paste - pastey 0.2.3 — MIT OR Apache-2.0 — https://github.com/as1100k/pastey pem 3.0.6 — MIT — https://github.com/jcreekmore/pem-rs.git - pem-rfc7468 0.7.0 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/pem-rfc7468 - percent-encoding 2.3.2 — MIT OR Apache-2.0 — https://github.com/servo/rust-url/ pin-project-lite 0.2.17 — Apache-2.0 OR MIT — https://github.com/taiki-e/pin-project-lite - piper 0.2.5 — MIT OR Apache-2.0 — https://github.com/smol-rs/piper - pipewire 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs - pipewire-sys 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs - pkcs1 0.7.5 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/pkcs1 - pkcs8 0.10.2 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/pkcs8 pkg-config 0.3.33 — MIT OR Apache-2.0 — https://github.com/rust-lang/pkg-config-rs - png 0.18.1 — MIT OR Apache-2.0 — https://github.com/image-rs/image-png - polling 3.11.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/polling poly1305 0.8.0 — Apache-2.0 OR MIT — https://github.com/RustCrypto/universal-hashes polyval 0.6.2 — Apache-2.0 OR MIT — https://github.com/RustCrypto/universal-hashes - potential_utf 0.1.5 — Unicode-3.0 — https://github.com/unicode-org/icu4x powerfmt 0.2.0 — MIT OR Apache-2.0 — https://github.com/jhpratt/powerfmt ppv-lite86 0.2.21 — MIT OR Apache-2.0 — https://github.com/cryptocorrosion/cryptocorrosion - prettyplease 0.2.37 — MIT OR Apache-2.0 — https://github.com/dtolnay/prettyplease - proc-macro-crate 3.5.0 — MIT OR Apache-2.0 — https://github.com/bkchr/proc-macro-crate proc-macro2 1.0.106 — MIT OR Apache-2.0 — https://github.com/dtolnay/proc-macro2 proptest 1.11.0 — MIT OR Apache-2.0 — https://github.com/proptest-rs/proptest - pxfm 0.1.30 — BSD-3-Clause OR Apache-2.0 — https://github.com/awxkee/pxfm quick-error 1.2.3 — MIT/Apache-2.0 — http://github.com/tailhook/quick-error - quick-xml 0.39.4 — MIT — https://github.com/tafia/quick-xml quinn 0.11.11 — MIT OR Apache-2.0 — https://github.com/quinn-rs/quinn quinn-proto 0.11.15 — MIT OR Apache-2.0 — https://github.com/quinn-rs/quinn quinn-udp 0.5.14 — MIT OR Apache-2.0 — https://github.com/quinn-rs/quinn quote 1.0.46 — MIT OR Apache-2.0 — https://github.com/dtolnay/quote r-efi 5.3.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi r-efi 6.0.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi - rand 0.8.6 — MIT OR Apache-2.0 — https://github.com/rust-random/rand rand 0.9.4 — MIT OR Apache-2.0 — https://github.com/rust-random/rand - rand_chacha 0.3.1 — MIT OR Apache-2.0 — https://github.com/rust-random/rand rand_chacha 0.9.0 — MIT OR Apache-2.0 — https://github.com/rust-random/rand rand_core 0.6.4 — MIT OR Apache-2.0 — https://github.com/rust-random/rand rand_core 0.9.5 — MIT OR Apache-2.0 — https://github.com/rust-random/rand rand_xorshift 0.4.0 — MIT OR Apache-2.0 — https://github.com/rust-random/rngs - raw-window-handle 0.6.2 — MIT OR Apache-2.0 OR Zlib — https://github.com/rust-windowing/raw-window-handle + rayon 1.12.0 — MIT OR Apache-2.0 — https://github.com/rayon-rs/rayon + rayon-core 1.13.0 — MIT OR Apache-2.0 — https://github.com/rayon-rs/rayon rcgen 0.13.2 — MIT OR Apache-2.0 — https://github.com/rustls/rcgen readme-rustdocifier 0.1.1 — MIT — https://github.com/malaire/readme-rustdocifier redox_syscall 0.5.18 — MIT — https://gitlab.redox-os.org/redox-os/syscall @@ -335,19 +168,9 @@ MANIFEST (crate version — SPDX license — source) regex 1.12.4 — MIT OR Apache-2.0 — https://github.com/rust-lang/regex regex-automata 0.4.14 — MIT OR Apache-2.0 — https://github.com/rust-lang/regex regex-syntax 0.8.11 — MIT OR Apache-2.0 — https://github.com/rust-lang/regex - reis 0.6.1 — MIT — https://github.com/ids1024/reis - relm4 0.11.0 — Apache-2.0 OR MIT — https://github.com/Relm4/Relm4 - relm4-css 0.11.0 — Apache-2.0 OR MIT — https://github.com/Relm4/Relm4 - relm4-macros 0.11.0 — Apache-2.0 OR MIT — https://github.com/Relm4/Relm4 ring 0.17.14 — Apache-2.0 AND ISC — https://github.com/briansmith/ring - roxmltree 0.21.1 — MIT OR Apache-2.0 — https://github.com/RazrFalcon/roxmltree - rpkg-config 0.1.2 — Zlib OR MIT OR Apache-2.0 — https://github.com/maia-s/rpkg-config-rs - rsa 0.9.10 — MIT OR Apache-2.0 — https://github.com/RustCrypto/RSA - rsqlite-vfs 0.1.1 — MIT - rusqlite 0.40.1 — MIT — https://github.com/rusqlite/rusqlite rustc-hash 2.1.2 — Apache-2.0 OR MIT — https://github.com/rust-lang/rustc-hash rustc_version 0.4.1 — MIT OR Apache-2.0 — https://github.com/djc/rustc-version-rs - rusticata-macros 4.1.0 — MIT/Apache-2.0 — https://github.com/rusticata/rusticata-macros.git rustix 1.1.4 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/rustix rustls 0.23.41 — Apache-2.0 OR ISC OR MIT — https://github.com/rustls/rustls rustls-native-certs 0.8.4 — Apache-2.0 OR ISC OR MIT — https://github.com/rustls/rustls-native-certs @@ -357,21 +180,9 @@ MANIFEST (crate version — SPDX license — source) rustls-webpki 0.103.13 — ISC — https://github.com/rustls/webpki rustversion 1.0.22 — MIT OR Apache-2.0 — https://github.com/dtolnay/rustversion rusty-fork 0.3.1 — MIT/Apache-2.0 — https://github.com/altsysrq/rusty-fork - rusty_enet 0.4.0 — MIT — https://github.com/jabuwu/rusty_enet - ryu 1.0.23 — Apache-2.0 OR BSL-1.0 — https://github.com/dtolnay/ryu - safe_arch 0.7.4 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/safe_arch same-file 1.0.6 — Unlicense/MIT — https://github.com/BurntSushi/same-file schannel 0.1.29 — MIT — https://github.com/steffengy/schannel-rs scopeguard 1.2.0 — MIT OR Apache-2.0 — https://github.com/bluss/scopeguard - sdl3 0.18.4 — MIT — https://github.com/vhspace/sdl3-rs - sdl3-image-src 3.4.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-image-sys 0.6.4+SDL-image-3.4.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-mixer-src 3.2.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-mixer-sys 0.6.3+SDL-mixer-3.2.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-src 3.4.10 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-sys 0.6.6+SDL-3.4.10 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-ttf-src 3.2.2 — Zlib — https://github.com/maia-s/sdl3-sys-rs - sdl3-ttf-sys 0.6.1+SDL-ttf-3.2.2 — Zlib — https://codeberg.org/maia/sdl3-sys-rs security-framework 3.7.0 — MIT OR Apache-2.0 — https://github.com/kornelski/rust-security-framework security-framework-sys 2.17.0 — MIT OR Apache-2.0 — https://github.com/kornelski/rust-security-framework semver 1.0.28 — MIT OR Apache-2.0 — https://github.com/dtolnay/semver @@ -379,149 +190,58 @@ MANIFEST (crate version — SPDX license — source) serde_core 1.0.228 — MIT OR Apache-2.0 — https://github.com/serde-rs/serde serde_derive 1.0.228 — MIT OR Apache-2.0 — https://github.com/serde-rs/serde serde_json 1.0.150 — MIT OR Apache-2.0 — https://github.com/serde-rs/json - serde_path_to_error 0.1.20 — MIT OR Apache-2.0 — https://github.com/dtolnay/path-to-error - serde_repr 0.1.20 — MIT OR Apache-2.0 — https://github.com/dtolnay/serde-repr - serde_spanned 0.6.9 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml serde_spanned 1.1.1 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml - serde_urlencoded 0.7.1 — MIT/Apache-2.0 — https://github.com/nox/serde_urlencoded sha2 0.10.9 — MIT OR Apache-2.0 — https://github.com/RustCrypto/hashes - sharded-slab 0.1.7 — MIT — https://github.com/hawkw/sharded-slab - shlex 1.3.0 — MIT OR Apache-2.0 — https://github.com/comex/rust-shlex shlex 2.0.1 — MIT OR Apache-2.0 — https://github.com/comex/rust-shlex signal-hook-registry 1.4.8 — MIT OR Apache-2.0 — https://github.com/vorner/signal-hook - signature 2.2.0 — Apache-2.0 OR MIT — https://github.com/RustCrypto/traits/tree/master/signature - simd-adler32 0.3.9 — MIT — https://github.com/mcountryman/simd-adler32 siphasher 1.0.3 — MIT/Apache-2.0 — https://github.com/jedisct1/rust-siphash - skia-bindings 0.87.0 — MIT — https://github.com/rust-skia/rust-skia - skia-safe 0.87.0 — MIT — https://github.com/rust-skia/rust-skia slab 0.4.12 — MIT — https://github.com/tokio-rs/slab smallvec 1.15.2 — MIT OR Apache-2.0 — https://github.com/servo/rust-smallvec - socket-pktinfo 0.4.0 — MIT — https://github.com/pixsper/socket-pktinfo socket2 0.6.4 — MIT OR Apache-2.0 — https://github.com/rust-lang/socket2 spake2 0.4.0 — MIT OR Apache-2.0 — https://github.com/RustCrypto/PAKEs/tree/master/spake2 - spin 0.9.8 — MIT — https://github.com/mvdnes/spin-rs.git - spki 0.7.3 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/spki - sqlite-wasm-rs 0.5.5 — MIT — https://github.com/Spxg/sqlite-wasm-rs - stable_deref_trait 1.2.1 — MIT OR Apache-2.0 — https://github.com/storyyeller/stable_deref_trait strsim 0.11.1 — MIT — https://github.com/rapidfuzz/strsim-rs subtle 2.6.1 — BSD-3-Clause — https://github.com/dalek-cryptography/subtle syn 2.0.118 — MIT OR Apache-2.0 — https://github.com/dtolnay/syn - sync_wrapper 1.0.2 — Apache-2.0 — https://github.com/Actyx/sync_wrapper - synstructure 0.13.2 — MIT — https://github.com/mystor/synstructure - system-deps 7.0.8 — MIT OR Apache-2.0 — https://github.com/gdesmott/system-deps - tar 0.4.46 — MIT OR Apache-2.0 — https://github.com/composefs/tar-rs - target-lexicon 0.13.5 — Apache-2.0 WITH LLVM-exception — https://github.com/bytecodealliance/target-lexicon tempfile 3.27.0 — MIT OR Apache-2.0 — https://github.com/Stebalien/tempfile - test_reactor 0.0.0 — UNKNOWN thiserror 1.0.69 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror thiserror 2.0.18 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror thiserror-impl 1.0.69 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror thiserror-impl 2.0.18 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror - thread_local 1.1.9 — MIT OR Apache-2.0 — https://github.com/Amanieu/thread_local-rs time 0.3.51 — MIT OR Apache-2.0 — https://github.com/time-rs/time time-core 0.1.9 — MIT OR Apache-2.0 — https://github.com/time-rs/time time-macros 0.2.30 — MIT OR Apache-2.0 — https://github.com/time-rs/time - tinystr 0.8.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x tinytemplate 1.2.1 — Apache-2.0 OR MIT — https://github.com/bheisler/TinyTemplate tinyvec 1.11.0 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/tinyvec tinyvec_macros 0.1.1 — MIT OR Apache-2.0 OR Zlib — https://github.com/Soveu/tinyvec_macros tokio 1.52.3 — MIT — https://github.com/tokio-rs/tokio tokio-macros 2.7.0 — MIT — https://github.com/tokio-rs/tokio - tokio-rustls 0.26.4 — MIT OR Apache-2.0 — https://github.com/rustls/tokio-rustls - tokio-util 0.7.18 — MIT — https://github.com/tokio-rs/tokio - toml 0.8.23 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml toml 0.9.12+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml - toml 1.1.2+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml - toml_datetime 0.6.11 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml toml_datetime 0.7.5+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml - toml_datetime 1.1.1+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml - toml_edit 0.22.27 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml - toml_edit 0.25.12+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml toml_parser 1.1.2+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml - toml_write 0.1.2 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml toml_writer 1.1.1+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml - tower 0.5.3 — MIT — https://github.com/tower-rs/tower - tower-layer 0.3.3 — MIT — https://github.com/tower-rs/tower - tower-service 0.3.3 — MIT — https://github.com/tower-rs/tower tracing 0.1.44 — MIT — https://github.com/tokio-rs/tracing tracing-attributes 0.1.31 — MIT — https://github.com/tokio-rs/tracing tracing-core 0.1.36 — MIT — https://github.com/tokio-rs/tracing - tracing-log 0.2.0 — MIT — https://github.com/tokio-rs/tracing - tracing-subscriber 0.3.23 — MIT — https://github.com/tokio-rs/tracing typenum 1.20.1 — MIT OR Apache-2.0 — https://github.com/paholg/typenum - uds_windows 1.2.1 — MIT — https://github.com/haraldh/rust_uds_windows unarray 0.1.4 — MIT OR Apache-2.0 — https://github.com/cameron1024/unarray unicode-ident 1.0.24 — (MIT OR Apache-2.0) AND Unicode-3.0 — https://github.com/dtolnay/unicode-ident - unicode-segmentation 1.13.3 — MIT OR Apache-2.0 — https://github.com/unicode-rs/unicode-segmentation - unicode-width 0.2.2 — MIT OR Apache-2.0 — https://github.com/unicode-rs/unicode-width universal-hash 0.5.1 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits untrusted 0.9.0 — ISC — https://github.com/briansmith/untrusted - ureq 2.12.1 — MIT OR Apache-2.0 — https://github.com/algesten/ureq - url 2.5.8 — MIT OR Apache-2.0 — https://github.com/servo/rust-url - utf8_iter 1.0.4 — Apache-2.0 OR MIT — https://github.com/hsivonen/utf8_iter utf8parse 0.2.2 — Apache-2.0 OR MIT — https://github.com/alacritty/vte - utoipa 5.5.0 — MIT OR Apache-2.0 — https://github.com/juhaku/utoipa - utoipa-axum 0.2.0 — MIT OR Apache-2.0 — https://github.com/juhaku/utoipa - utoipa-gen 5.5.0 — MIT OR Apache-2.0 — https://github.com/juhaku/utoipa - utoipa-scalar 0.3.0 — MIT OR Apache-2.0 — https://github.com/juhaku/utoipa - uuid 1.23.4 — Apache-2.0 OR MIT — https://github.com/uuid-rs/uuid valuable 0.1.1 — MIT — https://github.com/tokio-rs/valuable - vcpkg 0.2.15 — MIT/Apache-2.0 — https://github.com/mcgoo/vcpkg-rs - version-compare 0.2.1 — MIT — https://gitlab.com/timvisee/version-compare version_check 0.9.5 — MIT/Apache-2.0 — https://github.com/SergioBenitez/version_check wait-timeout 0.2.1 — MIT/Apache-2.0 — https://github.com/alexcrichton/wait-timeout walkdir 2.5.0 — Unlicense/MIT — https://github.com/BurntSushi/walkdir - wasapi 0.23.0 — MIT — https://github.com/HEnquist/wasapi-rs wasi 0.11.1+wasi-snapshot-preview1 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/wasi wasip2 1.0.4+wasi-0.2.12 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/wasi-rs wasm-bindgen 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen wasm-bindgen-macro 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/macro wasm-bindgen-macro-support 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/macro-support wasm-bindgen-shared 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/shared - wayland-backend 0.3.15 — MIT — https://github.com/smithay/wayland-rs - wayland-client 0.31.14 — MIT — https://github.com/smithay/wayland-rs - wayland-protocols 0.32.13 — MIT — https://github.com/smithay/wayland-rs - wayland-protocols-misc 0.3.12 — MIT — https://github.com/smithay/wayland-rs - wayland-protocols-wlr 0.3.12 — MIT — https://github.com/smithay/wayland-rs - wayland-scanner 0.31.10 — MIT — https://github.com/smithay/wayland-rs - wayland-sys 0.31.11 — MIT — https://github.com/smithay/wayland-rs web-time 1.1.0 — MIT OR Apache-2.0 — https://github.com/daxpedda/web-time webpki-root-certs 1.0.8 — CDLA-Permissive-2.0 — https://github.com/rustls/webpki-roots - webpki-roots 0.26.11 — CDLA-Permissive-2.0 — https://github.com/rustls/webpki-roots - webpki-roots 1.0.8 — CDLA-Permissive-2.0 — https://github.com/rustls/webpki-roots - weezl 0.1.12 — MIT OR Apache-2.0 — https://github.com/image-rs/weezl - wide 0.7.33 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/wide - widestring 1.2.1 — MIT OR Apache-2.0 — https://github.com/VoidStarKat/widestring-rs - winapi 0.3.9 — MIT/Apache-2.0 — https://github.com/retep998/winapi-rs - winapi-i686-pc-windows-gnu 0.4.0 — MIT/Apache-2.0 — https://github.com/retep998/winapi-rs winapi-util 0.1.11 — Unlicense OR MIT — https://github.com/BurntSushi/winapi-util - winapi-x86_64-pc-windows-gnu 0.4.0 — MIT/Apache-2.0 — https://github.com/retep998/winapi-rs - windows 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-canvas 0.0.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-collections 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-collections 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-composition 0.0.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-core 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-core 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-future 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-future 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-implement 0.60.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-implement 0.60.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-interface 0.59.3 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-interface 0.59.3 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows-link 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-link 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-numerics 0.3.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-numerics 0.3.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-reactor 0.0.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-reactor-setup 0.0.0 — MIT OR Apache-2.0 - windows-reference 0.1.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-result 0.4.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-result 0.4.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-service 0.7.0 — MIT OR Apache-2.0 — https://github.com/mullvad/windows-service-rs - windows-strings 0.5.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-strings 0.5.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows-sys 0.45.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows-sys 0.52.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows-sys 0.59.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs @@ -530,10 +250,6 @@ MANIFEST (crate version — SPDX license — source) windows-targets 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows-targets 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows-targets 0.53.5 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-threading 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-threading 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-time 0.1.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs - windows-window 0.0.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows_aarch64_gnullvm 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows_aarch64_gnullvm 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs windows_aarch64_gnullvm 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs @@ -559,62 +275,22 @@ MANIFEST (crate version — SPDX license — source) windows_x86_64_msvc 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs winnow 0.7.15 — MIT — https://github.com/winnow-rs/winnow winnow 1.0.3 — MIT — https://github.com/winnow-rs/winnow - winreg 0.56.0 — MIT — https://github.com/gentoo90/winreg-rs - winresource 0.1.31 — MIT — https://github.com/BenjaminRi/winresource wit-bindgen 0.57.1 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/wit-bindgen - writeable 0.6.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x - x11rb 0.13.2 — MIT OR Apache-2.0 — https://github.com/psychon/x11rb - x11rb-protocol 0.13.2 — MIT OR Apache-2.0 — https://github.com/psychon/x11rb - x509-parser 0.16.0 — MIT OR Apache-2.0 — https://github.com/rusticata/x509-parser.git - xattr 1.6.1 — MIT OR Apache-2.0 — https://github.com/Stebalien/xattr - xkbcommon 0.8.0 — MIT — https://github.com/rust-x-bindings/xkbcommon-rs - xkeysym 0.2.1 — MIT OR Apache-2.0 OR Zlib — https://github.com/notgull/xkeysym yasna 0.5.2 — MIT OR Apache-2.0 — https://github.com/qnighy/yasna.rs - yoke 0.8.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x - yoke-derive 0.8.2 — Unicode-3.0 — https://github.com/unicode-org/icu4x - zbus 5.16.0 — MIT — https://github.com/z-galaxy/zbus/ - zbus_macros 5.16.0 — MIT — https://github.com/z-galaxy/zbus/ - zbus_names 4.3.2 — MIT — https://github.com/z-galaxy/zbus/ zerocopy 0.8.52 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy zerocopy-derive 0.8.52 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy - zerofrom 0.1.8 — Unicode-3.0 — https://github.com/unicode-org/icu4x - zerofrom-derive 0.1.7 — Unicode-3.0 — https://github.com/unicode-org/icu4x zeroize 1.9.0 — Apache-2.0 OR MIT — https://github.com/RustCrypto/utils - zerotrie 0.2.4 — Unicode-3.0 — https://github.com/unicode-org/icu4x - zerovec 0.11.6 — Unicode-3.0 — https://github.com/unicode-org/icu4x - zerovec-derive 0.11.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x zmij 1.0.21 — MIT — https://github.com/dtolnay/zmij - zune-core 0.5.1 — MIT OR Apache-2.0 OR Zlib — https://github.com/etemesi254/zune-image - zune-jpeg 0.5.15 — MIT OR Apache-2.0 OR Zlib — https://github.com/etemesi254/zune-image/tree/dev/crates/zune-jpeg - zvariant 5.12.0 — MIT — https://github.com/z-galaxy/zbus/ - zvariant_derive 5.12.0 — MIT — https://github.com/z-galaxy/zbus/ - zvariant_utils 3.4.0 — MIT — https://github.com/z-galaxy/zbus/ ---------------------------------------------------------------------------- Crates whose package did not embed a license file (SPDX + source only) ---------------------------------------------------------------------------- anes 0.1.6 — MIT OR Apache-2.0 — https://github.com/zrzka/anes-rs - asn1-rs-impl 0.2.0 — MIT/Apache-2.0 — https://github.com/rusticata/asn1-rs.git - cookie-factory 0.3.3 — MIT — https://github.com/rust-bakery/cookie-factory - ffmpeg-sys-next 8.1.0 — WTFPL — https://github.com/zmwangx/rust-ffmpeg-sys jni-sys-macros 0.4.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys - ndk 0.9.0 — MIT OR Apache-2.0 — https://github.com/rust-mobile/ndk - ndk-sys 0.6.0+11769913 — MIT OR Apache-2.0 — https://github.com/rust-mobile/ndk - openh264 0.9.3 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs - openh264-sys2 0.9.6 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs r-efi 5.3.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi r-efi 6.0.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi - rsqlite-vfs 0.1.1 — MIT rustls-platform-verifier-android 0.1.1 — MIT OR Apache-2.0 — https://github.com/rustls/rustls-platform-verifier - sdl3-image-src 3.4.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-mixer-src 3.2.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs - sdl3-ttf-src 3.2.2 — Zlib — https://github.com/maia-s/sdl3-sys-rs - skia-bindings 0.87.0 — MIT — https://github.com/rust-skia/rust-skia - skia-safe 0.87.0 — MIT — https://github.com/rust-skia/rust-skia - test_reactor 0.0.0 — UNKNOWN valuable 0.1.1 — MIT — https://github.com/tokio-rs/valuable - winapi-i686-pc-windows-gnu 0.4.0 — MIT/Apache-2.0 — https://github.com/retep998/winapi-rs - winapi-x86_64-pc-windows-gnu 0.4.0 — MIT/Apache-2.0 — https://github.com/retep998/winapi-rs yasna 0.5.2 — MIT OR Apache-2.0 — https://github.com/qnighy/yasna.rs ============================================================================ @@ -622,258 +298,7 @@ FULL LICENSE TEXTS (deduplicated) ============================================================================ ---------------------------------------------------------------------------- -The following license (LICENSE-0BSD) applies to: adler2 2.0.1 ----------------------------------------------------------------------------- -Copyright (C) Jonas Schievink - -Permission to use, copy, modify, and/or distribute this software for -any purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN -AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT -OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: adler2 2.0.1, proc-macro-crate 3.5.0 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - https://www.apache.org/licenses/LICENSE-2.0 - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: adler2 2.0.1, anyhow 1.0.103, async-channel 2.5.0, async-executor 1.14.0, async-io 2.6.0, async-lock 3.4.2, async-process 2.5.0, async-recursion 1.1.1, async-signal 0.2.14, async-task 4.7.1, async-trait 0.1.89, atomic-waker 1.1.2, blocking 1.6.2, concurrent-queue 2.5.0, curve25519-dalek-derive 0.1.1, displaydoc 0.2.6, endi 1.1.1, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, flume 0.12.0, fs-err 3.3.0, futures-lite 2.6.1, hermit-abi 0.5.2, is-terminal 0.4.17, itoa 1.0.18, khronos-egl 6.0.0, linux-raw-sys 0.12.1, minimal-lexical 0.2.1, num_enum 0.7.6, num_enum_derive 0.7.6, once_cell 1.21.4, ordered-stream 0.2.0, parking 2.2.1, paste 1.0.15, pastey 0.2.3, pin-project-lite 0.2.17, piper 0.2.5, polling 3.11.0, prettyplease 0.2.37, proc-macro-crate 3.5.0, proc-macro2 1.0.106, quote 1.0.46, reis 0.6.1, relm4 0.11.0, relm4-css 0.11.0, relm4-macros 0.11.0, rsa 0.9.10, rustc-hash 2.1.2, rustix 1.1.4, rustversion 1.0.22, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, serde_path_to_error 0.1.20, serde_repr 0.1.20, syn 2.0.118, system-deps 7.0.8, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1, zmij 1.0.21, zvariant_utils 3.4.0 ----------------------------------------------------------------------------- -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 following license (LICENSE-APACHE) applies to: aead 0.5.2, aes 0.8.4, aes-gcm 0.10.3, base64ct 1.8.3, block-buffer 0.10.4, block-padding 0.3.3, cbc 0.1.2, chacha20 0.9.1, chacha20poly1305 0.10.1, cipher 0.4.4, const-oid 0.9.6, cpufeatures 0.2.17, crypto-common 0.1.7, ctr 0.9.2, der 0.7.10, digest 0.10.7, ghash 0.5.1, hkdf 0.12.4, hmac 0.12.1, inout 0.1.4, opaque-debug 0.3.1, pem-rfc7468 0.7.0, pkcs1 0.7.5, pkcs8 0.10.2, poly1305 0.8.0, polyval 0.6.2, sha2 0.10.9, signature 2.2.0, spake2 0.4.0, spki 0.7.3, universal-hash 0.5.1 +The following license (LICENSE-APACHE) applies to: aead 0.5.2, aes 0.8.4, aes-gcm 0.10.3, block-buffer 0.10.4, block-padding 0.3.3, chacha20 0.9.1, chacha20poly1305 0.10.1, cipher 0.4.4, const-oid 0.9.6, cpufeatures 0.2.17, crypto-common 0.1.7, ctr 0.9.2, digest 0.10.7, ghash 0.5.1, hkdf 0.12.4, hmac 0.12.1, inout 0.1.4, opaque-debug 0.3.1, poly1305 0.8.0, polyval 0.6.2, sha2 0.10.9, spake2 0.4.0, universal-hash 0.5.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -1178,7 +603,7 @@ You may use this code under the terms of either license. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: aho-corasick 1.1.4, byteorder-lite 0.1.0, memchr 2.8.2, walkdir 2.5.0 +The following license (LICENSE-MIT) applies to: aho-corasick 1.1.4, memchr 2.8.2, walkdir 2.5.0 ---------------------------------------------------------------------------- The MIT License (MIT) @@ -1204,7 +629,7 @@ THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (UNLICENSE) applies to: aho-corasick 1.1.4, byteorder-lite 0.1.0, ksni 0.3.5, memchr 2.8.2, same-file 1.0.6, walkdir 2.5.0, winapi-util 0.1.11 +The following license (UNLICENSE) applies to: aho-corasick 1.1.4, memchr 2.8.2, same-file 1.0.6, walkdir 2.5.0, winapi-util 0.1.11 ---------------------------------------------------------------------------- This is free and unencumbered software released into the public domain. @@ -1233,467 +658,7 @@ For more information, please refer to ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: android_log-sys 0.3.2 ----------------------------------------------------------------------------- -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, -and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by -the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all -other entities that control, are controlled by, or are under common -control with that entity. For the purposes of this definition, -"control" means (i) the power, direct or indirect, to cause the -direction or management of such entity, whether by contract or -otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity -exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, -including but not limited to software source code, documentation -source, and configuration files. - -"Object" form shall mean any form resulting from mechanical -transformation or translation of a Source form, including but -not limited to compiled object code, generated documentation, -and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or -Object form, made available under the License, as indicated by a -copyright notice that is included in or attached to the work -(an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object -form, that is based on (or derived from) the Work and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. For the purposes -of this License, Derivative Works shall not include works that remain -separable from, or merely link (or bind by name) to the interfaces of, -the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including -the original version of the Work and any modifications or additions -to that Work or Derivative Works thereof, that is intentionally -submitted to Licensor for inclusion in the Work by the copyright owner -or by an individual or Legal Entity authorized to submit on behalf of -the copyright owner. For the purposes of this definition, "submitted" -means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, -and issue tracking systems that are managed by, or on behalf of, the -Licensor for the purpose of discussing and improving the Work, but -excluding communication that is conspicuously marked or otherwise -designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity -on behalf of whom a Contribution has been received by Licensor and -subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the -Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except as stated in this section) patent license to make, have made, -use, offer to sell, sell, import, and otherwise transfer the Work, -where such license applies only to those patent claims licensable -by such Contributor that are necessarily infringed by their -Contribution(s) alone or by combination of their Contribution(s) -with the Work to which such Contribution(s) was submitted. If You -institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work -or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses -granted to You under this License for that Work shall terminate -as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the -Work or Derivative Works thereof in any medium, with or without -modifications, and in Source or Object form, provided that You -meet the following conditions: - -(a) You must give any other recipients of the Work or -Derivative Works a copy of this License; and - -(b) You must cause any modified files to carry prominent notices -stating that You changed the files; and - -(c) You must retain, in the Source form of any Derivative Works -that You distribute, all copyright, patent, trademark, and -attribution notices from the Source form of the Work, -excluding those notices that do not pertain to any part of -the Derivative Works; and - -(d) If the Work includes a "NOTICE" text file as part of its -distribution, then any Derivative Works that You distribute must -include a readable copy of the attribution notices contained -within such NOTICE file, excluding those notices that do not -pertain to any part of the Derivative Works, in at least one -of the following places: within a NOTICE text file distributed -as part of the Derivative Works; within the Source form or -documentation, if provided along with the Derivative Works; or, -within a display generated by the Derivative Works, if and -wherever such third-party notices normally appear. The contents -of the NOTICE file are for informational purposes only and -do not modify the License. You may add Your own attribution -notices within Derivative Works that You distribute, alongside -or as an addendum to the NOTICE text from the Work, provided -that such additional attribution notices cannot be construed -as modifying the License. - -You may add Your own copyright statement to Your modifications and -may provide additional or different license terms and conditions -for use, reproduction, or distribution of Your modifications, or -for any such Derivative Works as a whole, provided Your use, -reproduction, and distribution of the Work otherwise complies with -the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, -any Contribution intentionally submitted for inclusion in the Work -by You to the Licensor shall be under the terms and conditions of -this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify -the terms of any separate license agreement you may have executed -with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade -names, trademarks, service marks, or product names of the Licensor, -except as required for reasonable and customary use in describing the -origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or -agreed to in writing, Licensor provides the Work (and each -Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied, including, without limitation, any warranties or conditions -of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A -PARTICULAR PURPOSE. You are solely responsible for determining the -appropriateness of using or redistributing the Work and assume any -risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, -whether in tort (including negligence), contract, or otherwise, -unless required by applicable law (such as deliberate and grossly -negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, -incidental, or consequential damages of any character arising as a -result of this License or out of the use or inability to use the -Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all -other commercial damages or losses), even if such Contributor -has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing -the Work or Derivative Works thereof, You may choose to offer, -and charge a fee for, acceptance of support, warranty, indemnity, -or other liability obligations and/or rights consistent with this -License. However, in accepting such obligations, You may act only -on Your own behalf and on Your sole responsibility, not on behalf -of any other Contributor, and only if You agree to indemnify, -defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason -of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following -boilerplate notice, with the fields enclosed by brackets "{}" -replaced with your own identifying information. (Don't include -the brackets!) The text should be enclosed in the appropriate -comment syntax for the file format. We also recommend that a -file or class name and description of purpose be included on the -same "printed page" as the copyright notice for easier -identification within third-party archives. - -Copyright 2016 The android_log_sys Developers - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: android_log-sys 0.3.2 ----------------------------------------------------------------------------- -Copyright (c) 2016 The android_log_sys Developers - -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 following license (LICENSE-APACHE) applies to: android_logger 0.14.1 ----------------------------------------------------------------------------- -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, -and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by -the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all -other entities that control, are controlled by, or are under common -control with that entity. For the purposes of this definition, -"control" means (i) the power, direct or indirect, to cause the -direction or management of such entity, whether by contract or -otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity -exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, -including but not limited to software source code, documentation -source, and configuration files. - -"Object" form shall mean any form resulting from mechanical -transformation or translation of a Source form, including but -not limited to compiled object code, generated documentation, -and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or -Object form, made available under the License, as indicated by a -copyright notice that is included in or attached to the work -(an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object -form, that is based on (or derived from) the Work and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. For the purposes -of this License, Derivative Works shall not include works that remain -separable from, or merely link (or bind by name) to the interfaces of, -the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including -the original version of the Work and any modifications or additions -to that Work or Derivative Works thereof, that is intentionally -submitted to Licensor for inclusion in the Work by the copyright owner -or by an individual or Legal Entity authorized to submit on behalf of -the copyright owner. For the purposes of this definition, "submitted" -means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, -and issue tracking systems that are managed by, or on behalf of, the -Licensor for the purpose of discussing and improving the Work, but -excluding communication that is conspicuously marked or otherwise -designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity -on behalf of whom a Contribution has been received by Licensor and -subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the -Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except as stated in this section) patent license to make, have made, -use, offer to sell, sell, import, and otherwise transfer the Work, -where such license applies only to those patent claims licensable -by such Contributor that are necessarily infringed by their -Contribution(s) alone or by combination of their Contribution(s) -with the Work to which such Contribution(s) was submitted. If You -institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work -or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses -granted to You under this License for that Work shall terminate -as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the -Work or Derivative Works thereof in any medium, with or without -modifications, and in Source or Object form, provided that You -meet the following conditions: - -(a) You must give any other recipients of the Work or -Derivative Works a copy of this License; and - -(b) You must cause any modified files to carry prominent notices -stating that You changed the files; and - -(c) You must retain, in the Source form of any Derivative Works -that You distribute, all copyright, patent, trademark, and -attribution notices from the Source form of the Work, -excluding those notices that do not pertain to any part of -the Derivative Works; and - -(d) If the Work includes a "NOTICE" text file as part of its -distribution, then any Derivative Works that You distribute must -include a readable copy of the attribution notices contained -within such NOTICE file, excluding those notices that do not -pertain to any part of the Derivative Works, in at least one -of the following places: within a NOTICE text file distributed -as part of the Derivative Works; within the Source form or -documentation, if provided along with the Derivative Works; or, -within a display generated by the Derivative Works, if and -wherever such third-party notices normally appear. The contents -of the NOTICE file are for informational purposes only and -do not modify the License. You may add Your own attribution -notices within Derivative Works that You distribute, alongside -or as an addendum to the NOTICE text from the Work, provided -that such additional attribution notices cannot be construed -as modifying the License. - -You may add Your own copyright statement to Your modifications and -may provide additional or different license terms and conditions -for use, reproduction, or distribution of Your modifications, or -for any such Derivative Works as a whole, provided Your use, -reproduction, and distribution of the Work otherwise complies with -the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, -any Contribution intentionally submitted for inclusion in the Work -by You to the Licensor shall be under the terms and conditions of -this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify -the terms of any separate license agreement you may have executed -with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade -names, trademarks, service marks, or product names of the Licensor, -except as required for reasonable and customary use in describing the -origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or -agreed to in writing, Licensor provides the Work (and each -Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied, including, without limitation, any warranties or conditions -of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A -PARTICULAR PURPOSE. You are solely responsible for determining the -appropriateness of using or redistributing the Work and assume any -risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, -whether in tort (including negligence), contract, or otherwise, -unless required by applicable law (such as deliberate and grossly -negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, -incidental, or consequential damages of any character arising as a -result of this License or out of the use or inability to use the -Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all -other commercial damages or losses), even if such Contributor -has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing -the Work or Derivative Works thereof, You may choose to offer, -and charge a fee for, acceptance of support, warranty, indemnity, -or other liability obligations and/or rights consistent with this -License. However, in accepting such obligations, You may act only -on Your own behalf and on Your sole responsibility, not on behalf -of any other Contributor, and only if You agree to indemnify, -defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason -of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following -boilerplate notice, with the fields enclosed by brackets "{}" -replaced with your own identifying information. (Don't include -the brackets!) The text should be enclosed in the appropriate -comment syntax for the file format. We also recommend that a -file or class name and description of purpose be included on the -same "printed page" as the copyright notice for easier -identification within third-party archives. - -Copyright 2016 The android_logger Developers - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: android_logger 0.14.1 ----------------------------------------------------------------------------- -Copyright (c) 2016 The android_logger Developers - -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 following license (LICENSE-APACHE) applies to: annotate-snippets 0.11.5, anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, crc32fast 1.5.0, env_filter 0.1.4, fallible-iterator 0.3.0, fallible-streaming-iterator 0.1.9, hex 0.4.3, is_terminal_polyfill 1.70.2, jni-sys 0.3.1, jni-sys 0.4.1, once_cell_polyfill 1.70.2, quick-error 1.2.3, serde_spanned 0.6.9, serde_spanned 1.1.1, toml 0.8.23, toml 0.9.12+spec-1.1.0, toml 1.1.2+spec-1.1.0, toml_datetime 0.6.11, toml_datetime 0.7.5+spec-1.1.0, toml_datetime 1.1.1+spec-1.1.0, toml_edit 0.22.27, toml_edit 0.25.12+spec-1.1.0, toml_parser 1.1.2+spec-1.1.0, toml_write 0.1.2, toml_writer 1.1.1+spec-1.1.0, winapi 0.3.9 +The following license (LICENSE-APACHE) applies to: anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, is_terminal_polyfill 1.70.2, jni-sys 0.3.1, jni-sys 0.4.1, once_cell_polyfill 1.70.2, quick-error 1.2.3, serde_spanned 1.1.1, toml 0.9.12+spec-1.1.0, toml_datetime 0.7.5+spec-1.1.0, toml_parser 1.1.2+spec-1.1.0, toml_writer 1.1.1+spec-1.1.0 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -1899,7 +864,7 @@ Apache License ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: annotate-snippets 0.11.5, anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, env_filter 0.1.4, is_terminal_polyfill 1.70.2, once_cell_polyfill 1.70.2, serde_spanned 0.6.9, serde_spanned 1.1.1, toml 0.8.23, toml 0.9.12+spec-1.1.0, toml 1.1.2+spec-1.1.0, toml_datetime 0.6.11, toml_datetime 0.7.5+spec-1.1.0, toml_datetime 1.1.1+spec-1.1.0, toml_edit 0.22.27, toml_edit 0.25.12+spec-1.1.0, toml_parser 1.1.2+spec-1.1.0, toml_write 0.1.2, toml_writer 1.1.1+spec-1.1.0 +The following license (LICENSE-MIT) applies to: anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, is_terminal_polyfill 1.70.2, once_cell_polyfill 1.70.2, serde_spanned 1.1.1, toml 0.9.12+spec-1.1.0, toml_datetime 0.7.5+spec-1.1.0, toml_parser 1.1.2+spec-1.1.0, toml_writer 1.1.1+spec-1.1.0 ---------------------------------------------------------------------------- Copyright (c) Individual contributors @@ -1923,316 +888,27 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: anyhow 1.0.103, async-trait 0.1.89, fastbloom 0.14.1, itoa 1.0.18, libc 0.2.186, num_enum 0.7.6, num_enum_derive 0.7.6, paste 1.0.15, pastey 0.2.3, prettyplease 0.2.37, proc-macro2 1.0.106, quote 1.0.46, relm4 0.11.0, relm4-css 0.11.0, relm4-macros 0.11.0, rustc-hash 2.1.2, rustversion 1.0.22, ryu 1.0.23, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, serde_path_to_error 0.1.20, serde_repr 0.1.20, serde_urlencoded 0.7.1, syn 2.0.118, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, utf8parse 0.2.2 +The following license (LICENSE.md) applies to: audiopus_sys 0.2.2 ---------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +ISC License -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Copyright (c) 2019, Lakelezz -1. Definitions. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: ash 0.38.0+1.3.281 ----------------------------------------------------------------------------- -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of this License; and -You must cause any modified files to carry prominent notices stating that You changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - -You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -Copyright 2016 Maik Klein - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: ash 0.38.0+1.3.281 ----------------------------------------------------------------------------- -Copyright (c) 2016 ASH - -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 following license (LICENSE) applies to: ashpd 0.13.12 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2020 Bilal Elmoussaoui - -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 following license (LICENSE-APACHE) applies to: asn1-rs 0.6.2, asn1-rs-derive 0.5.1, async-channel 2.5.0, async-executor 1.14.0, async-io 2.6.0, async-lock 3.4.2, async-process 2.5.0, async-recursion 1.1.1, async-signal 0.2.14, async-task 4.7.1, atomic-waker 1.1.2, autocfg 1.5.1, base64 0.22.1, bitflags 2.13.0, blocking 1.6.2, bumpalo 3.20.3, cast 0.3.0, cc 1.2.65, cexpr 0.6.0, cfg-if 1.0.4, cmake 0.1.58, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, der-parser 9.0.0, displaydoc 0.2.6, either 1.16.0, equivalent 1.0.2, errno 0.3.14, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, filetime 0.2.29, find-msvc-tools 0.1.9, fixedbitset 0.5.7, flate2 1.1.9, fnv 1.0.7, form_urlencoded 1.2.2, fs-err 3.3.0, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glob 0.3.3, hashbrown 0.16.1, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, httparse 1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.0, itertools 0.10.5, itertools 0.13.0, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, khronos-egl 6.0.0, lazy_static 1.5.0, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, mime 0.3.17, nasm-rs 0.3.2, num-bigint 0.4.6, num-bigint-dig 0.8.6, num-derive 0.4.2, num-integer 0.1.46, num-iter 0.1.45, num-traits 0.2.19, num_cpus 1.17.0, oid-registry 0.7.1, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, ordered-stream 0.2.0, parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5, pkg-config 0.3.33, png 0.18.1, polling 3.11.0, proptest 1.11.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, roxmltree 0.21.1, rsa 0.9.10, rustc_version 0.4.1, rusticata-macros 4.1.0, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, stable_deref_trait 1.2.1, system-deps 7.0.8, tar 0.4.46, tempfile 3.27.0, thread_local 1.1.9, tinytemplate 1.2.1, unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, uuid 1.23.4, vcpkg 0.2.15, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, weezl 0.1.12, wit-bindgen 0.57.1, x509-parser 0.16.0, xattr 1.6.1 +The following license (LICENSE-APACHE) applies to: autocfg 1.5.1, base64 0.22.1, bitflags 2.13.0, bumpalo 3.20.3, cast 0.3.0, cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, either 1.16.0, equivalent 1.0.2, errno 0.3.14, fastrand 2.4.1, find-msvc-tools 0.1.9, fixedbitset 0.5.7, fnv 1.0.7, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, indexmap 2.14.0, itertools 0.10.5, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, parking_lot 0.12.5, parking_lot_core 0.9.12, pkg-config 0.3.33, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, rustc_version 0.4.1, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, tempfile 3.27.0, tinytemplate 1.2.1, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, wit-bindgen 0.57.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -2437,327 +1113,6 @@ See the License for the specific language governing permissions and limitations under the License. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: asn1-rs 0.6.2, asn1-rs-derive 0.5.1, der-parser 9.0.0, oid-registry 0.7.1, rusticata-macros 4.1.0, x509-parser 0.16.0 ----------------------------------------------------------------------------- -Copyright (c) 2017 Pierre Chifflier - -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 following license (LICENSE-APACHE) applies to: async-broadcast 0.7.2 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - Copyright 2020 Yoshua Wuyts - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: async-broadcast 0.7.2 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2020 Yoshua Wuyts - -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 following license (LICENSE-THIRD-PARTY) applies to: atomic-waker 1.1.2, futures-lite 2.6.1 ----------------------------------------------------------------------------- -=============================================================================== - -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -=============================================================================== - -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors - -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 following license (LICENSE.md) applies to: audiopus_sys 0.2.2 ----------------------------------------------------------------------------- -ISC License - -Copyright (c) 2019, Lakelezz - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - ---------------------------------------------------------------------------- The following license (LICENSE-MIT) applies to: autocfg 1.5.1 ---------------------------------------------------------------------------- @@ -2788,92 +1143,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: axum 0.8.9 ----------------------------------------------------------------------------- -Copyright (c) 2019 axum 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 following license (LICENSE) applies to: axum-core 0.5.6 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2019–2025 axum 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 following license (LICENSE) applies to: axum-server 0.8.0 ----------------------------------------------------------------------------- -Copyright 2021 Axum Server 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 following license (LICENSE-MIT) applies to: base64 0.22.1 ---------------------------------------------------------------------------- @@ -2900,37 +1169,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: base64ct 1.8.3 ----------------------------------------------------------------------------- -Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com) -Copyright (c) 2021-2025 The RustCrypto Project Developers - -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 following license (bazzite.txt) applies to: Bazzite logo (vendored, assets/os-icons) ---------------------------------------------------------------------------- @@ -2951,41 +1189,7 @@ purposes only; their use does not imply endorsement. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: bindgen 0.72.1 ----------------------------------------------------------------------------- -BSD 3-Clause License - -Copyright (c) 2013, Jyun-Yan You -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: bit-set 0.8.0, bit-vec 0.8.0, cfg-expr 0.20.8, downcast-rs 1.2.1, hashlink 0.12.0, minimal-lexical 0.2.1 +The following license (LICENSE-APACHE) applies to: bit-set 0.8.0, bit-vec 0.8.0 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -3221,7 +1425,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: bitflags 2.13.0, glob 0.3.3, log 0.4.33, num-bigint 0.4.6, num-bigint-dig 0.8.6, num-derive 0.4.2, num-integer 0.1.46, num-iter 0.1.45, num-traits 0.2.19, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11 +The following license (LICENSE-MIT) applies to: bitflags 2.13.0, log 0.4.33, num-traits 0.2.19, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11 ---------------------------------------------------------------------------- Copyright (c) 2014 The Rust Project Developers @@ -3310,102 +1514,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: bytemuck 1.25.0, bytemuck_derive 1.10.2, safe_arch 0.7.4 ----------------------------------------------------------------------------- -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - - "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: bytemuck 1.25.0, bytemuck_derive 1.10.2 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2019 Daniel "Lokathor" Gee. - -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 (including the next paragraph) 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 following license (LICENSE-ZLIB) applies to: bytemuck 1.25.0, bytemuck_derive 1.10.2, tinyvec 1.11.0 ----------------------------------------------------------------------------- -Copyright (c) 2019 Daniel "Lokathor" Gee. - -This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. - - ---------------------------------------------------------------------------- The following license (LICENSE) applies to: bytes 1.12.0 ---------------------------------------------------------------------------- @@ -3436,47 +1544,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (COPYRIGHT) applies to: cairo-rs 0.22.0, gdk-pixbuf 0.22.0, gdk4 0.11.2, gio 0.22.6, glib 0.22.7, glib-build-tools 0.22.8, glib-macros 0.22.6, graphene-rs 0.22.0, gsk4 0.11.1, gtk4 0.11.3, gtk4-macros 0.11.0, pango 0.22.6 ----------------------------------------------------------------------------- -The gtk-rs Project is licensed under the MIT license, see the LICENSE file or -. - -Copyrights in the gtk-rs Project project are retained by their contributors. -No copyright assignment is required to contribute to the gtk-rs Project -project. - -For full authorship information, see the version control history. - -This project provides interoperability with various GNOME libraries but -doesn't distribute any parts of them. Distributing compiled libraries and -executables that link to those libraries may be subject to terms of the GNU -LGPL or other licenses. For more information check the license of each GNOME -library. - - ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: cairo-rs 0.22.0, cairo-sys-rs 0.22.0, gdk-pixbuf 0.22.0, gdk-pixbuf-sys 0.22.0, gdk4 0.11.2, gdk4-sys 0.11.2, gio 0.22.6, gio-sys 0.22.0, glib 0.22.7, glib-build-tools 0.22.8, glib-macros 0.22.6, glib-sys 0.22.6, gobject-sys 0.22.6, graphene-rs 0.22.0, graphene-sys 0.22.0, gsk4 0.11.1, gsk4-sys 0.11.1, gtk4 0.11.3, gtk4-macros 0.11.0, gtk4-sys 0.11.3, libadwaita 0.9.1, libadwaita-sys 0.9.1, pango 0.22.6, pango-sys 0.22.0 ----------------------------------------------------------------------------- -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 following license (LICENSE-MIT) applies to: cast 0.3.0 ---------------------------------------------------------------------------- @@ -3507,37 +1574,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: cbc 0.1.2, ctr 0.9.2 ----------------------------------------------------------------------------- -Copyright (c) 2018-2022 RustCrypto Developers -Copyright (c) 2018 Artyom Pavlov - -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 following license (LICENSE) applies to: cbindgen 0.29.4 ---------------------------------------------------------------------------- @@ -3917,7 +1953,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, filetime 0.2.29, find-msvc-tools 0.1.9, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 +The following license (LICENSE-MIT) applies to: cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, find-msvc-tools 0.1.9, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 ---------------------------------------------------------------------------- Copyright (c) 2014 Alex Crichton @@ -4373,66 +2409,6 @@ their own copyright notices and license terms: copyright itself, held by the contributor. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: cexpr 0.6.0 ----------------------------------------------------------------------------- -(C) Copyright 2016 Jethro G. Beekman - -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 following license (LICENSE-MIT) applies to: cfg-expr 0.20.8 ----------------------------------------------------------------------------- -Copyright (c) 2019 Embark Studios - -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 following license (LICENSE) applies to: cfg_aliases 0.2.1 ---------------------------------------------------------------------------- @@ -4509,7 +2485,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: ciborium 0.2.2, ciborium-io 0.2.2, ciborium-ll 0.2.2, clang-sys 1.8.1, flume 0.12.0, fragile 2.1.0, lru-slab 0.1.2, quinn 0.11.11, quinn-proto 0.11.15, quinn-udp 0.5.14, rpkg-config 0.1.2, rustls-platform-verifier 0.6.2, tinyvec 1.11.0, unarray 0.1.4, ureq 2.12.1, utf8_iter 1.0.4, utoipa 5.5.0, utoipa-axum 0.2.0, utoipa-gen 5.5.0, utoipa-scalar 0.3.0, x11rb 0.13.2, x11rb-protocol 0.13.2, zeroize 1.9.0, zune-core 0.5.1, zune-jpeg 0.5.15 +The following license (LICENSE) applies to: ciborium 0.2.2, ciborium-io 0.2.2, ciborium-ll 0.2.2, lru-slab 0.1.2, quinn 0.11.11, quinn-proto 0.11.15, quinn-udp 0.5.14, rustls-platform-verifier 0.6.2, tinyvec 1.11.0, unarray 0.1.4, zeroize 1.9.0 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -4744,32 +2720,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: color_quant 1.1.0 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2016 PistonDevelopers - -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 following license (LICENSE) applies to: combine 4.6.7 ---------------------------------------------------------------------------- @@ -4826,32 +2776,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: convert_case 0.8.0 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2025 rutrum - -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 following license (LICENSE-MIT) applies to: core-foundation 0.10.1, core-foundation-sys 0.8.7 ---------------------------------------------------------------------------- @@ -4912,32 +2836,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: crc32fast 1.5.0 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2018 Sam Rijs, Alex Crichton and 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 following license (LICENSE-MIT) applies to: criterion 0.5.1, criterion-plot 0.5.0 ---------------------------------------------------------------------------- @@ -4969,7 +2867,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: crossbeam-utils 0.8.21 +The following license (LICENSE-MIT) applies to: crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21 ---------------------------------------------------------------------------- The MIT License (MIT) @@ -5056,6 +2954,37 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: ctr 0.9.2 +---------------------------------------------------------------------------- +Copyright (c) 2018-2022 RustCrypto Developers +Copyright (c) 2018 Artyom Pavlov + +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 following license (LICENSE) applies to: curve25519-dalek 4.1.3 ---------------------------------------------------------------------------- @@ -5127,37 +3056,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: data-encoding 2.11.0 +The following license (LICENSE-MIT) applies to: curve25519-dalek-derive 0.1.1, fastrand 2.4.1, hermit-abi 0.5.2, is-terminal 0.4.17, itoa 1.0.18, linux-raw-sys 0.12.1, once_cell 1.21.4, pin-project-lite 0.2.17, proc-macro2 1.0.106, quote 1.0.46, rustc-hash 2.1.2, rustix 1.1.4, rustversion 1.0.22, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, syn 2.0.118, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1, zmij 1.0.21 ---------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2015-2020 Julien Cretin -Copyright (c) 2017-2020 Google Inc. - -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 following license (LICENSE-MIT) applies to: der 0.7.10, pkcs8 0.10.2 ----------------------------------------------------------------------------- -Copyright (c) 2020-2023 The RustCrypto Project Developers - 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 @@ -5444,37 +3344,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: downcast-rs 1.2.1 ----------------------------------------------------------------------------- -Copyright (c) 2020 Ashish Myles and 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 following license (LICENSE-MIT) applies to: either 1.16.0, itertools 0.10.5, itertools 0.13.0 +The following license (LICENSE-MIT) applies to: either 1.16.0, itertools 0.10.5 ---------------------------------------------------------------------------- Copyright (c) 2015 @@ -5503,212 +3373,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: enumflags2 0.7.12 ----------------------------------------------------------------------------- -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of this License; and -You must cause any modified files to carry prominent notices stating that You changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - -You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - - -Copyright 2017-2023 Maik Klein, Maja Kądziołka - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: enumflags2 0.7.12 ----------------------------------------------------------------------------- -Copyright (c) 2017-2023 Maik Klein, Maja Kądziołka - -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 following license (LICENSE-APACHE) applies to: enumflags2_derive 0.7.12 ----------------------------------------------------------------------------- -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of this License; and -You must cause any modified files to carry prominent notices stating that You changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - -You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - - -Copyright [2017] [Maik Klein] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: enumflags2_derive 0.7.12 ----------------------------------------------------------------------------- -Copyright (c) 2017 Maik Klein - -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 following license (LICENSE-MIT) applies to: equivalent 1.0.2 ---------------------------------------------------------------------------- @@ -5770,51 +3434,184 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: fallible-iterator 0.3.0 +The following license (LICENSE-APACHE) applies to: fastbloom 0.14.1, itoa 1.0.18, libc 0.2.186, proc-macro2 1.0.106, quote 1.0.46, rustc-hash 2.1.2, rustversion 1.0.22, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, syn 2.0.118, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, utf8parse 0.2.2 ---------------------------------------------------------------------------- -Copyright (c) 2015 The rust-openssl-verify Developers +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -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: +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +1. Definitions. -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. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: fallible-streaming-iterator 0.1.9 ----------------------------------------------------------------------------- -Copyright (c) 2016 The fallible-streaming-iterator Developers + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -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: + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -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. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS ---------------------------------------------------------------------------- @@ -5847,233 +3644,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: fdeflate 0.3.7, field-offset 0.3.6, half 2.7.1, image 0.25.10, miniz_oxide 0.8.9, num-conv 0.2.2, pin-project-lite 0.2.17, raw-window-handle 0.6.2, sync_wrapper 1.0.2, time 0.3.51, time-core 0.1.9, time-macros 0.2.30, widestring 1.2.1 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: fdeflate 0.3.7, image 0.25.10 ----------------------------------------------------------------------------- -MIT License - -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 following license (LICENSE) applies to: ffmpeg-next 8.1.0 ----------------------------------------------------------------------------- -DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 - - Everyone is permitted to copy and distribute verbatim or modified - copies of this license document, and changing it is allowed as long - as the name is changed. - - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. You just DO WHAT THE FUCK YOU WANT TO. - - ---------------------------------------------------------------------------- The following license (COPYRIGHT) applies to: fiat-crypto 0.2.9 ---------------------------------------------------------------------------- @@ -6160,32 +3730,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: field-offset 0.3.6 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2016-2021 Diggory Blake, and other 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 following license (LICENSE-MIT) applies to: fixedbitset 0.5.7 ---------------------------------------------------------------------------- @@ -6216,36 +3760,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: flate2 1.1.9 ----------------------------------------------------------------------------- -Copyright (c) 2014-2026 Alex Crichton - -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 following license (LICENSE-MIT) applies to: fnv 1.0.7 ---------------------------------------------------------------------------- @@ -6323,37 +3837,7 @@ identification purposes only; their use does not imply endorsement. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: form_urlencoded 1.2.2 ----------------------------------------------------------------------------- -Copyright (c) 2013-2016 The rust-url developers - -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 following license (LICENSE-APACHE) applies to: futures 0.3.32, futures-channel 0.3.32, futures-core 0.3.32, futures-executor 0.3.32, futures-io 0.3.32, futures-macro 0.3.32, futures-sink 0.3.32, futures-task 0.3.32, futures-util 0.3.32 +The following license (LICENSE-APACHE) applies to: futures-channel 0.3.32, futures-core 0.3.32, futures-io 0.3.32, futures-macro 0.3.32, futures-sink 0.3.32, futures-task 0.3.32, futures-util 0.3.32 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -6560,7 +4044,7 @@ limitations under the License. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: futures 0.3.32, futures-channel 0.3.32, futures-core 0.3.32, futures-executor 0.3.32, futures-io 0.3.32, futures-macro 0.3.32, futures-sink 0.3.32, futures-task 0.3.32, futures-util 0.3.32 +The following license (LICENSE-MIT) applies to: futures-channel 0.3.32, futures-core 0.3.32, futures-io 0.3.32, futures-macro 0.3.32, futures-sink 0.3.32, futures-task 0.3.32, futures-util 0.3.32 ---------------------------------------------------------------------------- Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors @@ -6617,7 +4101,7 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: getrandom 0.2.17, getrandom 0.3.4, getrandom 0.4.3, rand_chacha 0.3.1 +The following license (LICENSE-APACHE) applies to: getrandom 0.2.17, getrandom 0.3.4, getrandom 0.4.3 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -6945,32 +4429,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: gif 0.14.2 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2015 nwin - -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 following license (LICENSE) applies to: Granite subset (vendored, crates/pyrowave-sys) ---------------------------------------------------------------------------- @@ -6997,37 +4455,188 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: h2 0.4.15 +The following license (LICENSE-APACHE) applies to: half 2.7.1, num-conv 0.2.2, pin-project-lite 0.2.17, time 0.3.51, time-core 0.1.9, time-macros 0.2.30 ---------------------------------------------------------------------------- -Copyright (c) 2017 h2 authors +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -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: + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. + 1. Definitions. -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. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: half 2.7.1, widestring 1.2.1 +The following license (LICENSE-MIT) applies to: half 2.7.1 ---------------------------------------------------------------------------- MIT License @@ -7051,7 +4660,7 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: hashbrown 0.16.1, hashbrown 0.17.1 +The following license (LICENSE-MIT) applies to: hashbrown 0.17.1 ---------------------------------------------------------------------------- Copyright (c) 2016 Amanieu d'Antras @@ -7081,38 +4690,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: hashlink 0.12.0 ----------------------------------------------------------------------------- -This work is derived in part from the `linked-hash-map` crate, Copyright (c) -2015 The Rust Project Developers - -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 following license (LICENSE-MIT) applies to: heck 0.5.0, unicode-segmentation 1.13.3, unicode-width 0.2.2 +The following license (LICENSE-MIT) applies to: heck 0.5.0 ---------------------------------------------------------------------------- Copyright (c) 2015 The Rust Project Developers @@ -7141,31 +4719,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: hex 0.4.3 ----------------------------------------------------------------------------- -Copyright (c) 2013-2014 The Rust Project Developers. -Copyright (c) 2015-2020 The rust-hex Developers - -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 following license (LICENSE-MIT) applies to: hkdf 0.12.4 ---------------------------------------------------------------------------- @@ -7198,716 +4751,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: http 1.4.2 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright 2017 http-rs authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: http 1.4.2 ----------------------------------------------------------------------------- -Copyright (c) 2017 http-rs authors - -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 following license (LICENSE) applies to: http-body 1.0.1 ----------------------------------------------------------------------------- -Copyright (c) 2019-2024 Sean McArthur & Hyper 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 following license (LICENSE) applies to: http-body-util 0.1.3 ----------------------------------------------------------------------------- -Copyright (c) 2019-2025 Sean McArthur & Hyper 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 following license (LICENSE-MIT) applies to: httparse 1.10.1, num_cpus 1.17.0 ----------------------------------------------------------------------------- -Copyright (c) 2015-2025 Sean McArthur - -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 following license (LICENSE-APACHE) applies to: httpdate 1.0.3 ----------------------------------------------------------------------------- -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, -and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by -the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all -other entities that control, are controlled by, or are under common -control with that entity. For the purposes of this definition, -"control" means (i) the power, direct or indirect, to cause the -direction or management of such entity, whether by contract or -otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity -exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, -including but not limited to software source code, documentation -source, and configuration files. - -"Object" form shall mean any form resulting from mechanical -transformation or translation of a Source form, including but -not limited to compiled object code, generated documentation, -and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or -Object form, made available under the License, as indicated by a -copyright notice that is included in or attached to the work -(an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object -form, that is based on (or derived from) the Work and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. For the purposes -of this License, Derivative Works shall not include works that remain -separable from, or merely link (or bind by name) to the interfaces of, -the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including -the original version of the Work and any modifications or additions -to that Work or Derivative Works thereof, that is intentionally -submitted to Licensor for inclusion in the Work by the copyright owner -or by an individual or Legal Entity authorized to submit on behalf of -the copyright owner. For the purposes of this definition, "submitted" -means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, -and issue tracking systems that are managed by, or on behalf of, the -Licensor for the purpose of discussing and improving the Work, but -excluding communication that is conspicuously marked or otherwise -designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity -on behalf of whom a Contribution has been received by Licensor and -subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the -Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except as stated in this section) patent license to make, have made, -use, offer to sell, sell, import, and otherwise transfer the Work, -where such license applies only to those patent claims licensable -by such Contributor that are necessarily infringed by their -Contribution(s) alone or by combination of their Contribution(s) -with the Work to which such Contribution(s) was submitted. If You -institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work -or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses -granted to You under this License for that Work shall terminate -as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the -Work or Derivative Works thereof in any medium, with or without -modifications, and in Source or Object form, provided that You -meet the following conditions: - -(a) You must give any other recipients of the Work or -Derivative Works a copy of this License; and - -(b) You must cause any modified files to carry prominent notices -stating that You changed the files; and - -(c) You must retain, in the Source form of any Derivative Works -that You distribute, all copyright, patent, trademark, and -attribution notices from the Source form of the Work, -excluding those notices that do not pertain to any part of -the Derivative Works; and - -(d) If the Work includes a "NOTICE" text file as part of its -distribution, then any Derivative Works that You distribute must -include a readable copy of the attribution notices contained -within such NOTICE file, excluding those notices that do not -pertain to any part of the Derivative Works, in at least one -of the following places: within a NOTICE text file distributed -as part of the Derivative Works; within the Source form or -documentation, if provided along with the Derivative Works; or, -within a display generated by the Derivative Works, if and -wherever such third-party notices normally appear. The contents -of the NOTICE file are for informational purposes only and -do not modify the License. You may add Your own attribution -notices within Derivative Works that You distribute, alongside -or as an addendum to the NOTICE text from the Work, provided -that such additional attribution notices cannot be construed -as modifying the License. - -You may add Your own copyright statement to Your modifications and -may provide additional or different license terms and conditions -for use, reproduction, or distribution of Your modifications, or -for any such Derivative Works as a whole, provided Your use, -reproduction, and distribution of the Work otherwise complies with -the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, -any Contribution intentionally submitted for inclusion in the Work -by You to the Licensor shall be under the terms and conditions of -this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify -the terms of any separate license agreement you may have executed -with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade -names, trademarks, service marks, or product names of the Licensor, -except as required for reasonable and customary use in describing the -origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or -agreed to in writing, Licensor provides the Work (and each -Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied, including, without limitation, any warranties or conditions -of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A -PARTICULAR PURPOSE. You are solely responsible for determining the -appropriateness of using or redistributing the Work and assume any -risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, -whether in tort (including negligence), contract, or otherwise, -unless required by applicable law (such as deliberate and grossly -negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, -incidental, or consequential damages of any character arising as a -result of this License or out of the use or inability to use the -Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all -other commercial damages or losses), even if such Contributor -has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing -the Work or Derivative Works thereof, You may choose to offer, -and charge a fee for, acceptance of support, warranty, indemnity, -or other liability obligations and/or rights consistent with this -License. However, in accepting such obligations, You may act only -on Your own behalf and on Your sole responsibility, not on behalf -of any other Contributor, and only if You agree to indemnify, -defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason -of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following -boilerplate notice, with the fields enclosed by brackets "[]" -replaced with your own identifying information. (Don't include -the brackets!) The text should be enclosed in the appropriate -comment syntax for the file format. We also recommend that a -file or class name and description of purpose be included on the -same "printed page" as the copyright notice for easier -identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: httpdate 1.0.3 ----------------------------------------------------------------------------- -Copyright (c) 2016 Pyfisch - -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 following license (LICENSE) applies to: hyper 1.10.1 ----------------------------------------------------------------------------- -Copyright (c) 2014-2026 Sean McArthur - -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 following license (LICENSE) applies to: hyper-util 0.1.20 ----------------------------------------------------------------------------- -Copyright (c) 2023-2025 Sean McArthur - -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 following license (LICENSE) applies to: icu_collections 2.2.0, icu_locale_core 2.2.0, icu_normalizer 2.2.0, icu_normalizer_data 2.2.0, icu_properties 2.2.0, icu_properties_data 2.2.0, icu_provider 2.2.0, litemap 0.8.2, potential_utf 0.1.5, tinystr 0.8.3, writeable 0.6.3, yoke 0.8.3, yoke-derive 0.8.2, zerofrom 0.1.8, zerofrom-derive 0.1.7, zerotrie 0.2.4, zerovec 0.11.6, zerovec-derive 0.11.3 ----------------------------------------------------------------------------- -UNICODE LICENSE V3 - -COPYRIGHT AND PERMISSION NOTICE - -Copyright © 2020-2024 Unicode, Inc. - -NOTICE TO USER: Carefully read the following legal agreement. BY -DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR -SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE -TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT -DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of data files and any associated documentation (the "Data Files") or -software and any associated documentation (the "Software") to deal in the -Data Files or Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, and/or sell -copies of the Data Files or Software, and to permit persons to whom the -Data Files or Software are furnished to do so, provided that either (a) -this copyright and permission notice appear with all copies of the Data -Files or Software, or (b) this copyright and permission notice appear in -associated Documentation. - -THE DATA FILES AND SOFTWARE ARE 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 OF -THIRD PARTY RIGHTS. - -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE -BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA -FILES OR SOFTWARE. - -Except as contained in this notice, the name of a copyright holder shall -not be used in advertising or otherwise to promote the sale, use or other -dealings in these Data Files or Software without prior written -authorization of the copyright holder. - -SPDX-License-Identifier: Unicode-3.0 - -— - -Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. -ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: idna 1.1.0, percent-encoding 2.3.2, url 2.5.8 ----------------------------------------------------------------------------- -Copyright (c) 2013-2025 The rust-url developers - -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 following license (LICENSE-MIT) applies to: idna_adapter 1.2.2 ----------------------------------------------------------------------------- -Copyright (c) The rust-url developers - -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 following license (LICENSE-BSD) applies to: if-addrs 0.13.4, if-addrs 0.15.0 +The following license (LICENSE-BSD) applies to: if-addrs 0.13.4 ---------------------------------------------------------------------------- Copyright 2018 MaidSafe.net limited. Copyright 2020 messense @@ -7924,7 +4768,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: if-addrs 0.13.4, if-addrs 0.15.0 +The following license (LICENSE-MIT) applies to: if-addrs 0.13.4 ---------------------------------------------------------------------------- Copyright 2018 MaidSafe.net limited. Copyright 2020 messense @@ -8075,36 +4919,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: lazy_static 1.5.0 ----------------------------------------------------------------------------- -Copyright (c) 2010 The Rust Project Developers - -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 following license (LICENSE-MIT) applies to: libc 0.2.186 ---------------------------------------------------------------------------- @@ -8135,23 +4949,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: libloading 0.8.9 ----------------------------------------------------------------------------- -Copyright © 2015, Simonas Kazlauskas - -Permission to use, copy, modify, and/or distribute this software for any purpose with or without -fee is hereby granted, provided that the above copyright notice and this permission notice appear -in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS -SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE -AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, -NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -THIS SOFTWARE. - - ---------------------------------------------------------------------------- The following license (LICENSE.txt) applies to: libm 0.2.16 ---------------------------------------------------------------------------- @@ -8415,55 +5212,6 @@ have been licensed under extremely permissive terms. Copyright notices are retained in src/* files where relevant. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: libspa 0.9.2, libspa-sys 0.9.2, pipewire 0.9.2, pipewire-sys 0.9.2 ----------------------------------------------------------------------------- -Copyright The pipewire-rs 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 (including the next -paragraph) 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 following license (LICENSE) applies to: libsqlite3-sys 0.38.1, rusqlite 0.40.1 ----------------------------------------------------------------------------- -Copyright (c) 2014 The rusqlite developers - -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 following license (COPYRIGHT) applies to: linux-raw-sys 0.12.1 ---------------------------------------------------------------------------- @@ -8499,7 +5247,7 @@ at your option. ---------------------------------------------------------------------------- -The following license (LICENSE-Apache-2.0_WITH_LLVM-exception) applies to: linux-raw-sys 0.12.1, rustix 1.1.4, target-lexicon 0.13.5, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1 +The following license (LICENSE-Apache-2.0_WITH_LLVM-exception) applies to: linux-raw-sys 0.12.1, rustix 1.1.4, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -8722,7 +5470,7 @@ Software. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: lock_api 0.4.14, nasm-rs 0.3.2, parking_lot 0.12.5, parking_lot_core 0.9.12, rustc_version 0.4.1, thread_local 1.1.9 +The following license (LICENSE-MIT) applies to: lock_api 0.4.14, parking_lot 0.12.5, parking_lot_core 0.9.12, rustc_version 0.4.1 ---------------------------------------------------------------------------- Copyright (c) 2016 The Rust Project Developers @@ -8787,945 +5535,6 @@ the following restrictions: 3. This notice may not be removed or altered from any source distribution. ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE2.0) applies to: mac_address 1.1.8 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2018 Wesley Norris - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: mac_address 1.1.8 ----------------------------------------------------------------------------- -Copyright © 2018 Wesley Norris - -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 following license (LICENSE) applies to: matchers 0.2.0 ----------------------------------------------------------------------------- -Copyright (c) 2019 Eliza Weisman - -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 following license (LICENSE) applies to: matchit 0.8.4 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2022 Ibraheem Ahmed - -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 following license (LICENSE.httprouter) applies to: matchit 0.8.4 ----------------------------------------------------------------------------- -BSD 3-Clause License - -Copyright (c) 2013, Julien Schmidt -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: mdns-sd 0.20.1 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [2021-2022] [Han Xu, keepsimple@gmail.com] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: mdns-sd 0.20.1 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2021-2022, Han Xu, keepsimple@gmail.com - -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 following license (LICENSE-APACHE) applies to: memmap2 0.9.11 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [2015] [Dan Burkert] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: memmap2 0.9.11 ----------------------------------------------------------------------------- -Copyright (c) 2020 Yevhenii Reizner -Copyright (c) 2015 Dan Burkert - -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 following license (LICENSE) applies to: memoffset 0.9.1 ----------------------------------------------------------------------------- -Copyright (c) 2017 Gilad Naaman - -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 following license (LICENSE-MIT) applies to: mime 0.3.17 ----------------------------------------------------------------------------- -Copyright (c) 2014 Sean McArthur - -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 following license (LICENSE.md) applies to: minimal-lexical 0.2.1 ----------------------------------------------------------------------------- -Minimal-lexical is dual licensed under the Apache 2.0 license as well as the MIT -license. See the LICENCE-MIT and the LICENCE-APACHE files for the licenses. - ---- - -`src/bellerophon.rs` is loosely based off the Golang implementation, -found [here](https://github.com/golang/go/blob/b10849fbb97a2244c086991b4623ae9f32c212d0/src/strconv/extfloat.go). -That code (used if the `compact` feature is enabled) is subject to a -[3-clause BSD license](https://github.com/golang/go/blob/b10849fbb97a2244c086991b4623ae9f32c212d0/LICENSE): - -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: miniz_oxide 0.8.9 ----------------------------------------------------------------------------- -MIT License - -Copyright 2013-2014 RAD Game Tools and Valve Software -Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC -Copyright (c) 2017 Frommi -Copyright (c) 2017-2024 oyvindln - - -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 following license (LICENSE-MIT.md) applies to: miniz_oxide 0.8.9 ----------------------------------------------------------------------------- -MIT License - -Copyright 2013-2014 RAD Game Tools and Valve Software -Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC -Copyright (c) 2017 Frommi -Copyright (c) 2017-2024 oyvindln - -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 following license (LICENSE-ZLIB.md) applies to: miniz_oxide 0.8.9 ----------------------------------------------------------------------------- -Copyright 2013-2014 RAD Game Tools and Valve Software -Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC -Copyright (c) 2020 Frommi -Copyright (c) 2017-2024 oyvindln - -This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. - - ---------------------------------------------------------------------------- The following license (LICENSE) applies to: mio 1.2.1 ---------------------------------------------------------------------------- @@ -9750,321 +5559,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE.md) applies to: moxcms 0.8.1, pxfm 0.1.30 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2024 Radzivon Bartoshyk - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE.md) applies to: moxcms 0.8.1, pxfm 0.1.30 ----------------------------------------------------------------------------- -Copyright (c) Radzivon Bartoshyk. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: nix 0.29.0, nix 0.30.1 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2015 Carl Lerche + nix-rust Authors - -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 following license (LICENSE) applies to: nom 7.1.3, nom 8.0.0 ----------------------------------------------------------------------------- -Copyright (c) 2014-2019 Geoffroy Couprie - -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 following license (LICENSE) applies to: nu-ansi-term 0.50.3 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2014 Benjamin Sago -Copyright (c) 2021-2022 The Nushell Project Developers - -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 following license (LICENSE-MIT) applies to: num-conv 0.2.2 ---------------------------------------------------------------------------- @@ -10089,38 +5583,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-BSD) applies to: num_enum 0.7.6, num_enum_derive 0.7.6 ----------------------------------------------------------------------------- -Copyright (c) 2018, Daniel Wagner-Hall -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of num_enum nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ---------------------------------------------------------------------------- The following license (LICENSE-MIT) applies to: oorandom 11.1.5 ---------------------------------------------------------------------------- @@ -10201,20 +5663,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-THIRD-PARTY) applies to: parking 2.2.1 ----------------------------------------------------------------------------- -=============================================================================== - -Copyright 2014-2020 The Rust Project Developers - -Licensed under the Apache License, Version 2.0 or the MIT license -, at your -option. All files in the project carrying such notice may not be -copied, modified, or distributed except according to those terms. - - ---------------------------------------------------------------------------- The following license (LICENSE.md) applies to: pem 3.0.6 ---------------------------------------------------------------------------- @@ -10241,96 +5689,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: pem-rfc7468 0.7.0 ----------------------------------------------------------------------------- -Copyright (c) 2021 The RustCrypto Project Developers - -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 following license (LICENSE-MIT) applies to: pkcs1 0.7.5, spki 0.7.3 ----------------------------------------------------------------------------- -Copyright (c) 2021-2023 The RustCrypto Project Developers - -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 following license (LICENSE-MIT) applies to: png 0.18.1 ----------------------------------------------------------------------------- -Copyright (c) 2015 nwin - -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 following license (LICENSE-MIT) applies to: poly1305 0.8.0 ---------------------------------------------------------------------------- @@ -10936,34 +6294,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT.md) applies to: quick-xml 0.39.4 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2016 Johann Tuffe - -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 following license (LICENSE-MIT) applies to: quinn 0.11.11, quinn-proto 0.11.15, quinn-udp 0.5.14 ---------------------------------------------------------------------------- @@ -10977,7 +6307,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ---------------------------------------------------------------------------- -The following license (COPYRIGHT) applies to: rand 0.8.6, rand 0.9.4, rand_chacha 0.3.1, rand_chacha 0.9.0, rand_core 0.6.4, rand_core 0.9.5, rand_xorshift 0.4.0 +The following license (COPYRIGHT) applies to: rand 0.9.4, rand_chacha 0.9.0, rand_core 0.6.4, rand_core 0.9.5, rand_xorshift 0.4.0 ---------------------------------------------------------------------------- Copyrights in the Rand project are retained by their contributors. No copyright assignment is required to contribute to the Rand project. @@ -10994,7 +6324,7 @@ published under these same licenses. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: rand 0.8.6, rand 0.9.4, rand_chacha 0.9.0, rand_xorshift 0.4.0 +The following license (LICENSE-APACHE) applies to: rand 0.9.4, rand_chacha 0.9.0, rand_xorshift 0.4.0 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -11175,7 +6505,7 @@ END OF TERMS AND CONDITIONS ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: rand 0.8.6, rand 0.9.4, rand_chacha 0.3.1, rand_chacha 0.9.0, rand_core 0.6.4, rand_core 0.9.5, rand_xorshift 0.4.0 +The following license (LICENSE-MIT) applies to: rand 0.9.4, rand_chacha 0.9.0, rand_core 0.6.4, rand_core 0.9.5, rand_xorshift 0.4.0 ---------------------------------------------------------------------------- Copyright 2018 Developers of the Rand project Copyright (c) 2014 The Rust Project Developers @@ -11398,45 +6728,33 @@ APPENDIX: How to apply the Apache License to your work. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT.md) applies to: raw-window-handle 0.6.2 +The following license (LICENSE-MIT) applies to: rayon 1.12.0, rayon-core 1.13.0 ---------------------------------------------------------------------------- -MIT License +Copyright (c) 2010 The Rust Project Developers -Copyright (c) 2019 Osspial +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: -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 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 following license (LICENSE-ZLIB.md) applies to: raw-window-handle 0.6.2 ----------------------------------------------------------------------------- -Copyright (c) 2020 Osspial - -This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. +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. ---------------------------------------------------------------------------- @@ -12102,82 +7420,6 @@ OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: roxmltree 0.21.1 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2018 Yevhenii Reizner - -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 following license (LICENSE-MIT.md) applies to: rpkg-config 0.1.2 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2024 Maia S. R. - -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 following license (LICENSE-ZLIB.md) applies to: rpkg-config 0.1.2, sdl3-src 3.4.10 ----------------------------------------------------------------------------- -zlib License - -(C) 2024 Maia S. R. - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. - - ---------------------------------------------------------------------------- The following license (COPYRIGHT) applies to: rustix 1.1.4 ---------------------------------------------------------------------------- @@ -12562,76 +7804,6 @@ The files under third-party/chromium are licensed as described in third-party/chromium/LICENSE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: rusty_enet 0.4.0 ----------------------------------------------------------------------------- -Copyright (c) 2002-2020 Lee Salzman - -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 following license (LICENSE-BOOST) applies to: ryu 1.0.23 ----------------------------------------------------------------------------- -Boost Software License - Version 1.0 - August 17th, 2003 - -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: - -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -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, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT.md) applies to: safe_arch 0.7.4 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2023 Daniel "Lokathor" Gee. - -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 (including the next paragraph) 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 following license (LICENSE-ZLIB.md) applies to: safe_arch 0.7.4, wide 0.7.33 ----------------------------------------------------------------------------- -Copyright (c) 2020 Daniel "Lokathor" Gee. - -This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. - - ---------------------------------------------------------------------------- The following license (LICENSE-MIT) applies to: same-file 1.0.6, winapi-util 0.1.11 ---------------------------------------------------------------------------- @@ -12700,79 +7872,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: sdl3 0.18.4 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2013 Mozilla Foundation - -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 following license (LICENSE.md) applies to: sdl3-image-sys 0.6.4+SDL-image-3.4.4, sdl3-mixer-sys 0.6.3+SDL-mixer-3.2.4, sdl3-ttf-sys 0.6.1+SDL-ttf-3.2.2 ----------------------------------------------------------------------------- -zlib License - -(C) 2025 Maia S Ravn - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. - - ----------------------------------------------------------------------------- -The following license (LICENSE.md) applies to: sdl3-sys 0.6.6+SDL-3.4.10 ----------------------------------------------------------------------------- -zlib License - -(C) 2024-2025 Maia S Ravn - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. - - ---------------------------------------------------------------------------- The following license (LICENSE-MIT) applies to: security-framework 3.7.0, security-framework-sys 2.17.0 ---------------------------------------------------------------------------- @@ -12798,36 +7897,6 @@ 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 following license (LICENSE-MIT) applies to: serde_urlencoded 0.7.1 ----------------------------------------------------------------------------- -Copyright (c) 2016 Anthony Ramine - -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 following license (LICENSE-MIT) applies to: sha2 0.10.9 ---------------------------------------------------------------------------- @@ -12861,31 +7930,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: sharded-slab 0.1.7 ----------------------------------------------------------------------------- -Copyright (c) 2019 Eliza Weisman - -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 following license (LICENSE-APACHE) applies to: shlex 1.3.0, shlex 2.0.1 +The following license (LICENSE-APACHE) applies to: shlex 2.0.1 ---------------------------------------------------------------------------- Copyright 2015 Nicholas Allegra (comex). @@ -12903,7 +7948,7 @@ limitations under the License. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: shlex 1.3.0, shlex 2.0.1 +The following license (LICENSE-MIT) applies to: shlex 2.0.1 ---------------------------------------------------------------------------- The MIT License (MIT) @@ -12958,62 +8003,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: signature 2.2.0 ----------------------------------------------------------------------------- -Copyright (c) 2018-2023 RustCrypto Developers - -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 following license (LICENSE.md) applies to: simd-adler32 0.3.9 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) [2021] [Marvin Countryman] - -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 following license (simple-icons.txt) applies to: Simple Icons (vendored, assets/os-icons) ---------------------------------------------------------------------------- @@ -13102,32 +8091,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: socket-pktinfo 0.4.0 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2025 Pixsper - -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 following license (LICENSE-MIT) applies to: spake2 0.4.0 ---------------------------------------------------------------------------- @@ -13154,88 +8117,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: spin 0.9.8 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) 2014 Mathijs van de Nes - -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 following license (LICENSE) applies to: sqlite-wasm-rs 0.5.5 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2024 Spxg - -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 following license (LICENSE-MIT) applies to: stable_deref_trait 1.2.1 ----------------------------------------------------------------------------- -Copyright (c) 2017 Robert Grosse - -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 following license (LICENSE) applies to: strsim 0.11.1 ---------------------------------------------------------------------------- @@ -13299,49 +8180,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: synstructure 0.13.2 ----------------------------------------------------------------------------- -Copyright 2016 Nika Layzell - -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 following license (LICENSE-MIT) applies to: tar 0.4.46 ----------------------------------------------------------------------------- -Copyright (c) The tar-rs Project 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 following license (LICENSE-MIT) applies to: tempfile 3.27.0, xattr 1.6.1 +The following license (LICENSE-MIT) applies to: tempfile 3.27.0 ---------------------------------------------------------------------------- Copyright (c) 2015 Steven Allen @@ -13434,6 +8273,22 @@ The above copyright notice and this permission notice shall be included in all c 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 following license (LICENSE-ZLIB.md) applies to: tinyvec 1.11.0 +---------------------------------------------------------------------------- +Copyright (c) 2019 Daniel "Lokathor" Gee. + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + + ---------------------------------------------------------------------------- The following license (LICENSE-APACHE.md) applies to: tinyvec_macros 0.1.1 ---------------------------------------------------------------------------- @@ -13691,7 +8546,7 @@ freely, subject to the following restrictions: ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: tokio 1.52.3, tokio-util 0.7.18 +The following license (LICENSE) applies to: tokio 1.52.3 ---------------------------------------------------------------------------- MIT License @@ -13744,273 +8599,7 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: tokio-rustls 0.26.4 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright 2017 quininer kel - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: tokio-rustls 0.26.4 ----------------------------------------------------------------------------- -Copyright (c) 2017 quininer kel - -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 following license (LICENSE) applies to: tower 0.5.3, tower-layer 0.3.3, tower-service 0.3.3 ----------------------------------------------------------------------------- -Copyright (c) 2019 Tower 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 following license (LICENSE) applies to: tracing 0.1.44, tracing-attributes 0.1.31, tracing-core 0.1.36, tracing-log 0.2.0, tracing-subscriber 0.3.23 +The following license (LICENSE) applies to: tracing 0.1.44, tracing-attributes 0.1.31, tracing-core 0.1.36 ---------------------------------------------------------------------------- Copyright (c) 2019 Tokio Contributors @@ -14277,110 +8866,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: uds_windows 1.2.1 ----------------------------------------------------------------------------- -MIT License - - Copyright (c) Microsoft Corporation. All rights reserved. - - 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 following license (THIRDPARTYNOTICES) applies to: uds_windows 1.2.1 ----------------------------------------------------------------------------- -Third Party Notices - -*** - -rust -Copyright 2016 The Rust Project Developers -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. - -*** - -mio -Copyright (c) 2014 Carl Lerche and other MIO 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. - -*** - -miow -Copyright (c) 2014 Alex Crichton -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 following license (LICENSE-MIT) applies to: unarray 0.1.4 ---------------------------------------------------------------------------- @@ -14451,18 +8936,6 @@ dealings in these Data Files or Software without prior written authorization of the copyright holder. ----------------------------------------------------------------------------- -The following license (COPYRIGHT) applies to: unicode-segmentation 1.13.3, unicode-width 0.2.2 ----------------------------------------------------------------------------- -Licensed under the Apache License, Version 2.0 - or the MIT -license , -at your option. All files in the project carrying such -notice may not be copied, modified, or distributed except -according to those terms. - - ---------------------------------------------------------------------------- The following license (LICENSE-MIT) applies to: universal-hash 0.5.1 ---------------------------------------------------------------------------- @@ -14511,109 +8984,6 @@ The following license (LICENSE.txt) applies to: untrusted 0.9.0 // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: ureq 2.12.1 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) 2019 Martin Algesten - -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 following license (COPYRIGHT) applies to: utf8_iter 1.0.4 ----------------------------------------------------------------------------- -Copyright Mozilla Foundation - -Licensed under the Apache License (Version 2.0), or the MIT license, -(the "Licenses") at your option. You may not use this file except in -compliance with one of the Licenses. You may obtain copies of the -Licenses at: - - https://www.apache.org/licenses/LICENSE-2.0 - https://opensource.org/licenses/MIT - -Unless required by applicable law or agreed to in writing, software -distributed under the Licenses is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the Licenses for the specific language governing permissions and -limitations under the Licenses. - --- - -Test code is dedicated to the Public Domain when so designated (see -the individual files for PD/CC0-dedicated sections). - --- - -The implementation for Utf8CharIndices was adapted from the -CharIndices implementation of the Rust standard library at revision -ab32548539ec38a939c1b58599249f3b54130026 -(https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/library/core/src/str/iter.rs). - -Excerpt from https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/COPYRIGHT , -which refers to -https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/LICENSE-APACHE -and -https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/LICENSE-MIT -: - -For full authorship information, see the version control history or -https://thanks.rust-lang.org - -Except as otherwise noted (below and/or in individual files), Rust is -licensed under the Apache License, Version 2.0 or - or the MIT license - or , at your option. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: utf8_iter 1.0.4 ----------------------------------------------------------------------------- -Copyright Mozilla Foundation - -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 following license (LICENSE-MIT) applies to: utf8parse 0.2.2 ---------------------------------------------------------------------------- @@ -14644,118 +9014,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: utoipa 5.5.0, utoipa-axum 0.2.0, utoipa-gen 5.5.0, utoipa-scalar 0.3.0 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright © 2021 - - -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 following license (LICENSE-MIT) applies to: uuid 1.23.4 ----------------------------------------------------------------------------- -Copyright (c) 2014 The Rust Project Developers -Copyright (c) 2018 Ashley Mannix, Christopher Armstrong, Dylan DPC, Hunar Roop Kahlon - -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 following license (LICENSE-MIT) applies to: vcpkg 0.2.15 ----------------------------------------------------------------------------- -Copyright (c) 2017 Jim McGrath - -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 following license (LICENSE) applies to: version-compare 0.2.1 ----------------------------------------------------------------------------- -Copyright (c) 2017 Tim Visée - -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 following license (LICENSE-MIT) applies to: version_check 0.9.5 ---------------------------------------------------------------------------- @@ -14827,54 +9085,6 @@ Full license text of these licenses is available at: * MIT: https://opensource.org/licenses/MIT ----------------------------------------------------------------------------- -The following license (LICENSE.txt) applies to: wasapi 0.23.0 ----------------------------------------------------------------------------- -Copyright (c) 2020 Henrik Enquist - -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 following license (LICENSE.txt) applies to: wayland-backend 0.3.15, wayland-client 0.31.14, wayland-protocols 0.32.13, wayland-protocols-misc 0.3.12, wayland-protocols-wlr 0.3.12, wayland-scanner 0.31.10, wayland-sys 0.31.11 ----------------------------------------------------------------------------- -Copyright (c) 2015 Elinor Berger - -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 following license (LICENSE-APACHE) applies to: web-time 1.1.0 ---------------------------------------------------------------------------- @@ -15108,7 +9318,7 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: webpki-root-certs 1.0.8, webpki-roots 0.26.11, webpki-roots 1.0.8 +The following license (LICENSE) applies to: webpki-root-certs 1.0.8 ---------------------------------------------------------------------------- # Community Data License Agreement - Permissive - Version 2.0 @@ -15174,57 +9384,7 @@ insights. ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: weezl 0.1.12 ----------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) HeroicKatora 2020 - -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 following license (LICENSE-MIT) applies to: winapi 0.3.9 ----------------------------------------------------------------------------- -Copyright (c) 2015-2018 The winapi-rs Developers - -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 following license (license-apache-2.0) applies to: windows 0.62.2, windows-canvas 0.0.0, windows-collections 0.3.2, windows-composition 0.0.0, windows-core 0.62.2, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-numerics 0.3.1, windows-reactor 0.0.0, windows-reactor-setup 0.0.0, windows-reference 0.1.0, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.45.0, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.60.2, windows-sys 0.61.2, windows-targets 0.42.2, windows-targets 0.52.6, windows-targets 0.53.5, windows-threading 0.2.1, windows-time 0.1.0, windows-window 0.0.0, windows_aarch64_gnullvm 0.42.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_gnullvm 0.53.1, windows_aarch64_msvc 0.42.2, windows_aarch64_msvc 0.52.6, windows_aarch64_msvc 0.53.1, windows_i686_gnu 0.42.2, windows_i686_gnu 0.52.6, windows_i686_gnu 0.53.1, windows_i686_gnullvm 0.52.6, windows_i686_gnullvm 0.53.1, windows_i686_msvc 0.42.2, windows_i686_msvc 0.52.6, windows_i686_msvc 0.53.1, windows_x86_64_gnu 0.42.2, windows_x86_64_gnu 0.52.6, windows_x86_64_gnu 0.53.1, windows_x86_64_gnullvm 0.42.2, windows_x86_64_gnullvm 0.52.6, windows_x86_64_gnullvm 0.53.1, windows_x86_64_msvc 0.42.2, windows_x86_64_msvc 0.52.6, windows_x86_64_msvc 0.53.1 +The following license (license-apache-2.0) applies to: windows-link 0.2.1, windows-sys 0.45.0, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.60.2, windows-sys 0.61.2, windows-targets 0.42.2, windows-targets 0.52.6, windows-targets 0.53.5, windows_aarch64_gnullvm 0.42.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_gnullvm 0.53.1, windows_aarch64_msvc 0.42.2, windows_aarch64_msvc 0.52.6, windows_aarch64_msvc 0.53.1, windows_i686_gnu 0.42.2, windows_i686_gnu 0.52.6, windows_i686_gnu 0.53.1, windows_i686_gnullvm 0.52.6, windows_i686_gnullvm 0.53.1, windows_i686_msvc 0.42.2, windows_i686_msvc 0.52.6, windows_i686_msvc 0.53.1, windows_x86_64_gnu 0.42.2, windows_x86_64_gnu 0.52.6, windows_x86_64_gnu 0.53.1, windows_x86_64_gnullvm 0.42.2, windows_x86_64_gnullvm 0.52.6, windows_x86_64_gnullvm 0.53.1, windows_x86_64_msvc 0.42.2, windows_x86_64_msvc 0.52.6, windows_x86_64_msvc 0.53.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -15430,7 +9590,7 @@ Apache License ---------------------------------------------------------------------------- -The following license (license-mit) applies to: windows 0.62.2, windows-canvas 0.0.0, windows-collections 0.3.2, windows-composition 0.0.0, windows-core 0.62.2, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-numerics 0.3.1, windows-reactor 0.0.0, windows-reactor-setup 0.0.0, windows-reference 0.1.0, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.45.0, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.60.2, windows-sys 0.61.2, windows-targets 0.42.2, windows-targets 0.52.6, windows-targets 0.53.5, windows-threading 0.2.1, windows-time 0.1.0, windows-window 0.0.0, windows_aarch64_gnullvm 0.42.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_gnullvm 0.53.1, windows_aarch64_msvc 0.42.2, windows_aarch64_msvc 0.52.6, windows_aarch64_msvc 0.53.1, windows_i686_gnu 0.42.2, windows_i686_gnu 0.52.6, windows_i686_gnu 0.53.1, windows_i686_gnullvm 0.52.6, windows_i686_gnullvm 0.53.1, windows_i686_msvc 0.42.2, windows_i686_msvc 0.52.6, windows_i686_msvc 0.53.1, windows_x86_64_gnu 0.42.2, windows_x86_64_gnu 0.52.6, windows_x86_64_gnu 0.53.1, windows_x86_64_gnullvm 0.42.2, windows_x86_64_gnullvm 0.52.6, windows_x86_64_gnullvm 0.53.1, windows_x86_64_msvc 0.42.2, windows_x86_64_msvc 0.52.6, windows_x86_64_msvc 0.53.1 +The following license (license-mit) applies to: windows-link 0.2.1, windows-sys 0.45.0, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.60.2, windows-sys 0.61.2, windows-targets 0.42.2, windows-targets 0.52.6, windows-targets 0.53.5, windows_aarch64_gnullvm 0.42.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_gnullvm 0.53.1, windows_aarch64_msvc 0.42.2, windows_aarch64_msvc 0.52.6, windows_aarch64_msvc 0.53.1, windows_i686_gnu 0.42.2, windows_i686_gnu 0.52.6, windows_i686_gnu 0.53.1, windows_i686_gnullvm 0.52.6, windows_i686_gnullvm 0.53.1, windows_i686_msvc 0.42.2, windows_i686_msvc 0.52.6, windows_i686_msvc 0.53.1, windows_x86_64_gnu 0.42.2, windows_x86_64_gnu 0.52.6, windows_x86_64_gnu 0.53.1, windows_x86_64_gnullvm 0.42.2, windows_x86_64_gnullvm 0.52.6, windows_x86_64_gnullvm 0.53.1, windows_x86_64_msvc 0.42.2, windows_x86_64_msvc 0.52.6, windows_x86_64_msvc 0.53.1 ---------------------------------------------------------------------------- MIT License @@ -15455,242 +9615,6 @@ MIT License SOFTWARE ----------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: windows-service 0.7.0 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright 2018 Amagicom AB - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: windows-service 0.7.0 ----------------------------------------------------------------------------- -Copyright (c) 2017 Amagicom AB - -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 following license (LICENSE-MIT) applies to: winnow 0.7.15, winnow 1.0.3 ---------------------------------------------------------------------------- @@ -15714,396 +9638,6 @@ 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 following license (LICENSE) applies to: winreg 0.56.0 ----------------------------------------------------------------------------- -Copyright (c) 2015 Igor Shaula - -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 following license (LICENSE) applies to: winresource 0.1.31 ----------------------------------------------------------------------------- -Copyright 2016 Max Resch - -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 following license (LICENSE-MIT) applies to: x11rb 0.13.2, x11rb-protocol 0.13.2 ----------------------------------------------------------------------------- -Copyright 2019 x11rb Contributers - -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 following license (LICENSE) applies to: xkbcommon 0.8.0 ----------------------------------------------------------------------------- -Copyright (c) 2016 Remi Thebault - -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 following license (LICENSE-APACHE) applies to: xkeysym 0.2.1 ----------------------------------------------------------------------------- -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2022-2023 John Nunley - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - ----------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: xkeysym 0.2.1 ----------------------------------------------------------------------------- -Copyright (c) 2022-2023 John Nunley - -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 following license (LICENSE-ZLIB) applies to: xkeysym 0.2.1 ----------------------------------------------------------------------------- -Copyright (c) 2022-2023 John Nunley - -This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. - - ----------------------------------------------------------------------------- -The following license (LICENSE) applies to: zbus 5.16.0, zbus_macros 5.16.0, zbus_names 4.3.2, zvariant 5.12.0, zvariant_derive 5.12.0 ----------------------------------------------------------------------------- -Copyright (c) 2024 Zeeshan Ali Khan & zbus 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 following license (LICENSE-APACHE) applies to: zerocopy 0.8.52, zerocopy-derive 0.8.52 ---------------------------------------------------------------------------- @@ -16398,53 +9932,3 @@ 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 following license (LICENSE-MIT) applies to: zune-core 0.5.1, zune-jpeg 0.5.15 ----------------------------------------------------------------------------- -MIT License - -Copyright (c) zune-image developers - -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 following license (LICENSE-ZLIB) applies to: zune-core 0.5.1, zune-jpeg 0.5.15 ----------------------------------------------------------------------------- -zlib License - -(C) zune-image developers - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. - diff --git a/clients/apple/Sources/PunktfunkShared/DefaultsKeys.swift b/clients/apple/Sources/PunktfunkShared/DefaultsKeys.swift index 79bb74f6..6a67b893 100644 --- a/clients/apple/Sources/PunktfunkShared/DefaultsKeys.swift +++ b/clients/apple/Sources/PunktfunkShared/DefaultsKeys.swift @@ -193,6 +193,14 @@ public enum DefaultsKey { /// once per session by `GamepadFeedback`. The toggle is shown only where the device actually /// has a haptic actuator (no iPad/Mac/TV). public static let rumbleOnDevice = "punktfunk.rumbleOnDevice" + /// Use this device's own gyroscope as player 1's motion when the forwarded controller has + /// none of its own — for clip-on and third-party pads without an IMU, where the device body + /// moves with the player's hands. The rumble mirror's sibling, data flowing the other way. + /// Off by default (opt-in); read once per session by `GamepadCapture`, whose `DeviceGyro` + /// mirror engages only while pad 0's controller reports no rotation rate (a real gyro pad + /// always wins). The toggle is shown only where the device has motion hardware + /// (`DeviceGyro.isAvailable`). + public static let gyroFromDevice = "punktfunk.gyroFromDevice" /// Auto-wake on connect: when connecting to a saved host that isn't advertising on mDNS, fire /// Wake-on-LAN and, if the dial fails, wait for it to come back before retrying (the "Waking…" /// overlay). On by default. Turn off if a host that's already on just isn't seen on mDNS (a diff --git a/clients/apple/Tests/PunktfunkKitTests/DeviceGyroRemapTests.swift b/clients/apple/Tests/PunktfunkKitTests/DeviceGyroRemapTests.swift new file mode 100644 index 00000000..bfaa2a8a --- /dev/null +++ b/clients/apple/Tests/PunktfunkKitTests/DeviceGyroRemapTests.swift @@ -0,0 +1,62 @@ +// Pins the phone-gyro mirror's device→controller frame remap (DeviceGyro.swift). The matrix is +// derived (like the wire scale constants), so these tests are the contract: if on-glass says an +// axis is wrong, fix the enum AND these expectations together. + +#if os(iOS) +import UIKit +import XCTest + +@testable import PunktfunkKit + +final class DeviceGyroRemapTests: XCTestCase { + /// A distinct vector per axis so a swapped or flipped component can't cancel out. + private let v: (x: Float, y: Float, z: Float) = (1, 2, 3) + + func testPortraitIsIdentity() { + let r = DeviceGyroRemap.identity.apply(x: v.x, y: v.y, z: v.z) + XCTAssertEqual([r.x, r.y, r.z], [1, 2, 3]) + } + + func testUpsideDownFlipsInPlane() { + let r = DeviceGyroRemap.flipped.apply(x: v.x, y: v.y, z: v.z) + XCTAssertEqual([r.x, r.y, r.z], [-1, -2, 3]) + } + + /// Device top to the player's LEFT: player-right = device-bottom (−y), player-up = + /// device-right (+x). z (out of the screen) never changes — the screen faces the player. + func testTopLeftLandscape() { + let r = DeviceGyroRemap.topLeft.apply(x: v.x, y: v.y, z: v.z) + XCTAssertEqual([r.x, r.y, r.z], [-2, 1, 3]) + } + + /// Device top to the player's RIGHT: player-right = device-top (+y), player-up = + /// device-left (−x). + func testTopRightLandscape() { + let r = DeviceGyroRemap.topRight.apply(x: v.x, y: v.y, z: v.z) + XCTAssertEqual([r.x, r.y, r.z], [2, -1, 3]) + } + + /// Interface orientation → remap: `.landscapeRight` means the Home edge is on the + /// player's right, i.e. the device top points LEFT (and vice versa). + func testOrientationMapping() { + XCTAssertEqual(DeviceGyroRemap(.portrait), .identity) + XCTAssertEqual(DeviceGyroRemap(.portraitUpsideDown), .flipped) + XCTAssertEqual(DeviceGyroRemap(.landscapeRight), .topLeft) + XCTAssertEqual(DeviceGyroRemap(.landscapeLeft), .topRight) + XCTAssertEqual(DeviceGyroRemap(.unknown), .identity) + } + + /// Every remap must stay a proper rotation (right-handed): x̂ × ŷ = ẑ after mapping. + func testHandednessPreserved() { + for remap in [DeviceGyroRemap.identity, .flipped, .topLeft, .topRight] { + let x = remap.apply(x: 1, y: 0, z: 0) + let y = remap.apply(x: 0, y: 1, z: 0) + // Cross product of the two mapped in-plane basis vectors. + let cross = ( + x: x.y * 0 - 0 * y.y, y: 0 * y.x - x.x * 0, z: x.x * y.y - x.y * y.x + ) + XCTAssertEqual(cross.z, 1, "left-handed remap: \(remap)") + } + } +} +#endif diff --git a/clients/linux/Cargo.toml b/clients/linux/Cargo.toml index 1aae33b7..da67a2f2 100644 --- a/clients/linux/Cargo.toml +++ b/clients/linux/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "punktfunk-client-linux" -description = "Native Linux punktfunk/1 client — GTK4/libadwaita shell, FFmpeg decode, PipeWire audio, SDL3 gamepads" +description = "Native Linux punktfunk/1 client — GTK4/libadwaita shell, PipeWire audio, SDL3 gamepads; streaming runs in the spawned punktfunk-session binary" version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/clients/linux/README.md b/clients/linux/README.md index a73c26eb..bbbb9aed 100644 --- a/clients/linux/README.md +++ b/clients/linux/README.md @@ -12,9 +12,11 @@ Built in Rust end to end (no C ABI): the shell shares its plumbing with the sess ## Features -- **Zero-copy hardware decode** — the session presenter decodes via **Vulkan Video** on every GPU - vendor (including NVIDIA), falling back to FFmpeg VAAPI → DRM-PRIME dmabuf and then software when - Vulkan Video is unavailable. +- **Zero-copy hardware decode, and it's ours** — the session presenter decodes with Punktfunk's own + decoders; no FFmpeg is linked or bundled. **Vulkan Video** (`pf-vkdecode`, decoding onto the + presenter's own device) leads on NVIDIA and AMD, **VAAPI** (`pf-vaadec` driving a dlopen'd libva, + exporting DRM-PRIME dmabufs) leads on Intel, whichever isn't first is the fallback, and an + OpenH264/rav1d CPU rung is last. - **Your display's native mode** — the host builds a virtual output at exactly your WxH@Hz; no scaling, no letterboxing. Steady 60 fps at 1080p60, ~6 ms capture→decoded on the LAN. - **Audio both ways** — PipeWire playback with a jitter ring, plus mic uplink to the host. @@ -50,8 +52,11 @@ Per-device install steps and pairing walkthrough: ## Build & run from source -Requires GTK ≥ 4.16, libadwaita ≥ 1.5, FFmpeg 7 or 8 (with VAAPI for hardware decode), PipeWire, -and SDL3 (with hidapi) development packages. +Requires GTK ≥ 4.16, libadwaita ≥ 1.5, PipeWire, and SDL3 (with hidapi) development packages, +plus a C compiler (the CPU rung builds OpenH264 from source). No *decoder* development package +is needed: libva and the Vulkan loader are both opened at runtime rather than linked, so +hardware decode is a fact about the box you **run** on — a Vulkan loader and your GPU's driver, +and libva for the VAAPI rung — not about the one you build on. ```sh # from the repo root @@ -85,9 +90,9 @@ src/ tools/screenshots.sh store screenshot capture (app self-capture; Xvfb fallback) ``` -The UI-agnostic plumbing — session pump, FFmpeg decode, PipeWire audio, SDL3 gamepads + -keymap, trust store, mDNS discovery, library client, Wake-on-LAN — lives in -`crates/pf-client-core`, shared with the Vulkan session binary. +The UI-agnostic plumbing — session pump, the native decode ladder (Vulkan Video · VAAPI · +OpenH264/rav1d), PipeWire audio, SDL3 gamepads + keymap, trust store, mDNS discovery, library +client, Wake-on-LAN — lives in `crates/pf-client-core`, shared with the Vulkan session binary. ## Related diff --git a/clients/linux/THIRD-PARTY-NOTICES.txt b/clients/linux/THIRD-PARTY-NOTICES.txt new file mode 100644 index 00000000..280c1365 --- /dev/null +++ b/clients/linux/THIRD-PARTY-NOTICES.txt @@ -0,0 +1,12331 @@ +THIRD-PARTY SOFTWARE NOTICES +============================================================================ + +punktfunk (https://git.unom.io/unom/punktfunk) is licensed under MIT OR Apache-2.0. +The binaries it ships statically/dynamically link the third-party Rust crates listed +below. Each is distributed under its own permissive license; the full license texts +follow the manifest. This file is generated by scripts/gen-third-party-notices.py +(or `cargo about`, see about.toml) — do not edit by hand. + +Scope: the Rust crates linked by punktfunk-client-linux,punktfunk-client-session,punktfunk-cli,pf-update — not the whole punktfunk workspace. + +Total third-party crates: 449 + +---------------------------------------------------------------------------- +VENDORED THIRD-PARTY SOURCE (inside first-party crates) +---------------------------------------------------------------------------- + pyrowave (vendored, crates/pyrowave-sys) — https://github.com/Themaister/pyrowave + Granite subset (vendored, crates/pyrowave-sys) — https://github.com/Themaister/Granite + volk (vendored, crates/pyrowave-sys) — https://github.com/zeux/volk + Vulkan-Headers (vendored, crates/pyrowave-sys) — https://github.com/KhronosGroup/Vulkan-Headers + Font Awesome Free brand icons (vendored, assets/os-icons) — https://fontawesome.com + Simple Icons (vendored, assets/os-icons) — https://simpleicons.org + Bazzite logo (vendored, assets/os-icons) — https://github.com/ublue-os/bazzite + +---------------------------------------------------------------------------- +MANIFEST (crate version — SPDX license — source) +---------------------------------------------------------------------------- + adler2 2.0.1 — 0BSD OR MIT OR Apache-2.0 — https://github.com/oyvindln/adler2 + aead 0.5.2 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits + aes 0.8.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/block-ciphers + aes-gcm 0.10.3 — Apache-2.0 OR MIT — https://github.com/RustCrypto/AEADs + aho-corasick 1.1.4 — Unlicense OR MIT — https://github.com/BurntSushi/aho-corasick + anes 0.1.6 — MIT OR Apache-2.0 — https://github.com/zrzka/anes-rs + annotate-snippets 0.11.5 — MIT OR Apache-2.0 — https://github.com/rust-lang/annotate-snippets-rs + anstream 1.0.0 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git + anstyle 1.0.14 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git + anstyle-parse 1.0.0 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git + anstyle-query 1.1.5 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git + anstyle-wincon 3.0.11 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git + anyhow 1.0.103 — MIT OR Apache-2.0 — https://github.com/dtolnay/anyhow + ash 0.38.0+1.3.281 — MIT OR Apache-2.0 — https://github.com/ash-rs/ash + assert_matches 1.5.0 — MIT/Apache-2.0 — https://github.com/murarth/assert_matches + async-channel 2.5.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-channel + atomig 0.4.3 — MIT/Apache-2.0 — https://github.com/LukasKalbertodt/atomig/ + atomig-macro 0.4.0 — MIT/Apache-2.0 — https://github.com/LukasKalbertodt/atomig/ + audiopus_sys 0.2.2 — ISC — https://github.com/lakelezz/audiopus_sys.git + autocfg 1.5.1 — Apache-2.0 OR MIT — https://github.com/cuviper/autocfg + base64 0.22.1 — MIT OR Apache-2.0 — https://github.com/marshallpierce/rust-base64 + bindgen 0.72.1 — BSD-3-Clause — https://github.com/rust-lang/rust-bindgen + bit-set 0.8.0 — Apache-2.0 OR MIT — https://github.com/contain-rs/bit-set + bit-vec 0.8.0 — Apache-2.0 OR MIT — https://github.com/contain-rs/bit-vec + bitflags 1.3.2 — MIT/Apache-2.0 — https://github.com/bitflags/bitflags + bitflags 2.13.0 — MIT OR Apache-2.0 — https://github.com/bitflags/bitflags + block-buffer 0.10.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils + block-padding 0.3.3 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils + bumpalo 3.20.3 — MIT OR Apache-2.0 — https://github.com/fitzgen/bumpalo + bytemuck 1.25.0 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/bytemuck + bytemuck_derive 1.10.2 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/bytemuck + byteorder 1.5.0 — Unlicense OR MIT — https://github.com/BurntSushi/byteorder + bytes 1.12.0 — MIT — https://github.com/tokio-rs/bytes + cairo-rs 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core + cairo-sys-rs 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core + cast 0.3.0 — MIT OR Apache-2.0 — https://github.com/japaric/cast.rs + cbindgen 0.29.4 — MPL-2.0 — https://github.com/mozilla/cbindgen + cc 1.2.65 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs + cesu8 1.1.0 — Apache-2.0/MIT — https://github.com/emk/cesu8-rs + cexpr 0.6.0 — Apache-2.0/MIT — https://github.com/jethrogb/rust-cexpr + cfg-expr 0.20.8 — MIT OR Apache-2.0 — https://github.com/EmbarkStudios/cfg-expr + cfg-if 1.0.4 — MIT OR Apache-2.0 — https://github.com/rust-lang/cfg-if + cfg_aliases 0.2.1 — MIT — https://github.com/katharostech/cfg_aliases + chacha20 0.9.1 — Apache-2.0 OR MIT — https://github.com/RustCrypto/stream-ciphers + chacha20poly1305 0.10.1 — Apache-2.0 OR MIT — https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305 + ciborium 0.2.2 — Apache-2.0 — https://github.com/enarx/ciborium + ciborium-io 0.2.2 — Apache-2.0 — https://github.com/enarx/ciborium + ciborium-ll 0.2.2 — Apache-2.0 — https://github.com/enarx/ciborium + cipher 0.4.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits + clang-sys 1.8.1 — Apache-2.0 — https://github.com/KyleMayes/clang-sys + clap 4.6.1 — MIT OR Apache-2.0 — https://github.com/clap-rs/clap + clap_builder 4.6.0 — MIT OR Apache-2.0 — https://github.com/clap-rs/clap + clap_lex 1.1.0 — MIT OR Apache-2.0 — https://github.com/clap-rs/clap + cmake 0.1.58 — MIT OR Apache-2.0 — https://github.com/rust-lang/cmake-rs + colorchoice 1.0.5 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git + combine 4.6.7 — MIT — https://github.com/Marwes/combine + concurrent-queue 2.5.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/concurrent-queue + const-oid 0.9.6 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/const-oid + convert_case 0.8.0 — MIT — https://github.com/rutrum/convert-case + cookie-factory 0.3.3 — MIT — https://github.com/rust-bakery/cookie-factory + core-foundation 0.10.1 — MIT OR Apache-2.0 — https://github.com/servo/core-foundation-rs + core-foundation-sys 0.8.7 — MIT OR Apache-2.0 — https://github.com/servo/core-foundation-rs + cpufeatures 0.2.17 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils + crc32fast 1.5.0 — MIT OR Apache-2.0 — https://github.com/srijs/rust-crc32fast + criterion 0.5.1 — Apache-2.0 OR MIT — https://github.com/bheisler/criterion.rs + criterion-plot 0.5.0 — MIT/Apache-2.0 — https://github.com/bheisler/criterion.rs + crossbeam-deque 0.8.6 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam + crossbeam-epoch 0.9.20 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam + crossbeam-utils 0.8.21 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam + crunchy 0.2.4 — MIT — https://github.com/eira-fransham/crunchy + crypto-common 0.1.7 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits + ctr 0.9.2 — MIT OR Apache-2.0 — https://github.com/RustCrypto/block-modes + curve25519-dalek 4.1.3 — BSD-3-Clause — https://github.com/dalek-cryptography/curve25519-dalek/tree/main/curve25519-dalek + curve25519-dalek-derive 0.1.1 — MIT/Apache-2.0 — https://github.com/dalek-cryptography/curve25519-dalek + defmt 1.1.1 — MIT OR Apache-2.0 — https://github.com/knurling-rs/defmt + defmt-macros 1.1.1 — MIT OR Apache-2.0 — https://github.com/knurling-rs/defmt + defmt-parser 1.0.0 — MIT OR Apache-2.0 — https://github.com/knurling-rs/defmt + deranged 0.5.8 — MIT OR Apache-2.0 — https://github.com/jhpratt/deranged + digest 0.10.7 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits + displaydoc 0.2.6 — MIT OR Apache-2.0 — https://github.com/yaahc/displaydoc + either 1.16.0 — MIT OR Apache-2.0 — https://github.com/rayon-rs/either + env_filter 2.0.0 — MIT OR Apache-2.0 — https://github.com/rust-cli/env_logger + env_logger 0.11.11 — MIT OR Apache-2.0 — https://github.com/rust-cli/env_logger + equivalent 1.0.2 — Apache-2.0 OR MIT — https://github.com/indexmap-rs/equivalent + errno 0.3.14 — MIT OR Apache-2.0 — https://github.com/lambda-fairy/rust-errno + event-listener 5.4.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/event-listener + event-listener-strategy 0.5.4 — Apache-2.0 OR MIT — https://github.com/smol-rs/event-listener-strategy + fastbloom 0.14.1 — MIT OR Apache-2.0 — https://github.com/tomtomwombat/fastbloom/ + fastrand 2.4.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/fastrand + fiat-crypto 0.2.9 — MIT OR Apache-2.0 OR BSD-1-Clause — https://github.com/mit-plv/fiat-crypto + field-offset 0.3.6 — MIT OR Apache-2.0 — https://github.com/Diggsey/rust-field-offset + filetime 0.2.29 — MIT/Apache-2.0 — https://github.com/alexcrichton/filetime + find-msvc-tools 0.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs + fixedbitset 0.5.7 — MIT OR Apache-2.0 — https://github.com/petgraph/fixedbitset + flate2 1.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/flate2-rs + flume 0.12.0 — Apache-2.0/MIT — https://github.com/zesterer/flume + fnv 1.0.7 — Apache-2.0 / MIT — https://github.com/servo/rust-fnv + foldhash 0.2.0 — Zlib — https://github.com/orlp/foldhash + form_urlencoded 1.2.2 — MIT OR Apache-2.0 — https://github.com/servo/rust-url + fragile 2.1.0 — Apache-2.0 — https://github.com/mitsuhiko/fragile + futures 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + futures-channel 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + futures-core 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + futures-executor 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + futures-io 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + futures-macro 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + futures-sink 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + futures-task 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + futures-util 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + gdk-pixbuf 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core + gdk-pixbuf-sys 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core + gdk4 0.11.2 — MIT — https://github.com/gtk-rs/gtk4-rs + gdk4-sys 0.11.2 — MIT — https://github.com/gtk-rs/gtk4-rs + generic-array 0.14.7 — MIT — https://github.com/fizyk20/generic-array.git + getrandom 0.2.17 — MIT OR Apache-2.0 — https://github.com/rust-random/getrandom + getrandom 0.3.4 — MIT OR Apache-2.0 — https://github.com/rust-random/getrandom + getrandom 0.4.3 — MIT OR Apache-2.0 — https://github.com/rust-random/getrandom + ghash 0.5.1 — Apache-2.0 OR MIT — https://github.com/RustCrypto/universal-hashes + gio 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core + gio-sys 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core + glib 0.22.7 — MIT — https://github.com/gtk-rs/gtk-rs-core + glib-build-tools 0.22.8 — MIT — https://github.com/gtk-rs/gtk-rs-core + glib-macros 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core + glib-sys 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core + glob 0.3.3 — MIT OR Apache-2.0 — https://github.com/rust-lang/glob + gobject-sys 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core + graphene-rs 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core + graphene-sys 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core + gsk4 0.11.1 — MIT — https://github.com/gtk-rs/gtk4-rs + gsk4-sys 0.11.1 — MIT — https://github.com/gtk-rs/gtk4-rs + gtk4 0.11.3 — MIT — https://github.com/gtk-rs/gtk4-rs + gtk4-macros 0.11.0 — MIT — https://github.com/gtk-rs/gtk4-rs + gtk4-sys 0.11.3 — MIT — https://github.com/gtk-rs/gtk4-rs + half 2.7.1 — MIT OR Apache-2.0 — https://github.com/VoidStarKat/half-rs + hashbrown 0.17.1 — MIT OR Apache-2.0 — https://github.com/rust-lang/hashbrown + heck 0.5.0 — MIT OR Apache-2.0 — https://github.com/withoutboats/heck + hermit-abi 0.5.2 — MIT OR Apache-2.0 — https://github.com/hermit-os/hermit-rs + hkdf 0.12.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/KDFs/ + hmac 0.12.1 — MIT OR Apache-2.0 — https://github.com/RustCrypto/MACs + icu_collections 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x + icu_locale_core 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x + icu_normalizer 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x + icu_normalizer_data 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x + icu_properties 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x + icu_properties_data 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x + icu_provider 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x + idna 1.1.0 — MIT OR Apache-2.0 — https://github.com/servo/rust-url/ + idna_adapter 1.2.2 — Apache-2.0 OR MIT — https://github.com/hsivonen/idna_adapter + if-addrs 0.13.4 — MIT OR BSD-3-Clause — https://github.com/messense/if-addrs + if-addrs 0.15.0 — MIT OR BSD-3-Clause — https://github.com/messense/if-addrs + indexmap 2.14.0 — Apache-2.0 OR MIT — https://github.com/indexmap-rs/indexmap + inout 0.1.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils + is-terminal 0.4.17 — MIT — https://github.com/sunfishcode/is-terminal + is_terminal_polyfill 1.70.2 — MIT OR Apache-2.0 — https://github.com/polyfill-rs/is_terminal_polyfill + itertools 0.10.5 — MIT/Apache-2.0 — https://github.com/rust-itertools/itertools + itertools 0.13.0 — MIT OR Apache-2.0 — https://github.com/rust-itertools/itertools + itoa 1.0.18 — MIT OR Apache-2.0 — https://github.com/dtolnay/itoa + jiff 0.2.35 — Unlicense OR MIT — https://github.com/BurntSushi/jiff + jiff-core 0.1.0 — Unlicense OR MIT — https://github.com/BurntSushi/jiff + jiff-static 0.2.35 — Unlicense OR MIT — https://github.com/BurntSushi/jiff + jni 0.21.1 — MIT/Apache-2.0 — https://github.com/jni-rs/jni-rs + jni-sys 0.3.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys + jni-sys 0.4.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys + jni-sys-macros 0.4.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys + jobserver 0.1.34 — MIT OR Apache-2.0 — https://github.com/rust-lang/jobserver-rs + js-sys 0.3.103 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/js-sys + lazy_static 1.5.0 — MIT OR Apache-2.0 — https://github.com/rust-lang-nursery/lazy-static.rs + libadwaita 0.9.1 — MIT — https://gitlab.gnome.org/World/Rust/libadwaita-rs + libadwaita-sys 0.9.1 — MIT — https://gitlab.gnome.org/World/Rust/libadwaita-rs + libc 0.2.186 — MIT OR Apache-2.0 — https://github.com/rust-lang/libc + libloading 0.8.9 — ISC — https://github.com/nagisa/rust_libloading/ + libm 0.2.16 — MIT — https://github.com/rust-lang/compiler-builtins + libspa 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs + libspa-sys 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs + linux-raw-sys 0.12.1 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/sunfishcode/linux-raw-sys + litemap 0.8.2 — Unicode-3.0 — https://github.com/unicode-org/icu4x + lock_api 0.4.14 — MIT OR Apache-2.0 — https://github.com/Amanieu/parking_lot + log 0.4.33 — MIT OR Apache-2.0 — https://github.com/rust-lang/log + lru-slab 0.1.2 — MIT OR Apache-2.0 OR Zlib — https://github.com/Ralith/lru-slab + matchers 0.2.0 — MIT — https://github.com/hawkw/matchers + mdns-sd 0.20.1 — Apache-2.0 OR MIT — https://github.com/keepsimple1/mdns-sd + memchr 2.8.2 — Unlicense OR MIT — https://github.com/BurntSushi/memchr + memoffset 0.9.1 — MIT — https://github.com/Gilnaa/memoffset + minimal-lexical 0.2.1 — MIT/Apache-2.0 — https://github.com/Alexhuszagh/minimal-lexical + miniz_oxide 0.8.9 — MIT OR Zlib OR Apache-2.0 — https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide + mio 1.2.1 — MIT — https://github.com/tokio-rs/mio + nasm-rs 0.3.2 — MIT OR Apache-2.0 — https://github.com/medek/nasm-rs + nix 0.30.1 — MIT — https://github.com/nix-rust/nix + nom 7.1.3 — MIT — https://github.com/Geal/nom + nom 8.0.0 — MIT — https://github.com/rust-bakery/nom + nu-ansi-term 0.50.3 — MIT — https://github.com/nushell/nu-ansi-term + num-conv 0.2.2 — MIT OR Apache-2.0 — https://github.com/jhpratt/num-conv + num-integer 0.1.46 — MIT OR Apache-2.0 — https://github.com/rust-num/num-integer + num-traits 0.2.19 — MIT OR Apache-2.0 — https://github.com/rust-num/num-traits + once_cell 1.21.4 — MIT OR Apache-2.0 — https://github.com/matklad/once_cell + once_cell_polyfill 1.70.2 — MIT OR Apache-2.0 — https://github.com/polyfill-rs/once_cell_polyfill + oorandom 11.1.5 — MIT — https://hg.sr.ht/~icefox/oorandom + opaque-debug 0.3.1 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils + openh264 0.9.3 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs + openh264-sys2 0.9.6 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs + openssl-probe 0.2.1 — MIT OR Apache-2.0 — https://github.com/rustls/openssl-probe + opus 0.3.1 — MIT/Apache-2.0 — https://github.com/SpaceManiac/opus-rs + pango 0.22.6 — MIT — https://github.com/gtk-rs/gtk-rs-core + pango-sys 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core + parking 2.2.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/parking + parking_lot 0.12.5 — MIT OR Apache-2.0 — https://github.com/Amanieu/parking_lot + parking_lot_core 0.9.12 — MIT OR Apache-2.0 — https://github.com/Amanieu/parking_lot + paste 1.0.15 — MIT OR Apache-2.0 — https://github.com/dtolnay/paste + pem 3.0.6 — MIT — https://github.com/jcreekmore/pem-rs.git + percent-encoding 2.3.2 — MIT OR Apache-2.0 — https://github.com/servo/rust-url/ + pin-project-lite 0.2.17 — Apache-2.0 OR MIT — https://github.com/taiki-e/pin-project-lite + pipewire 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs + pipewire-sys 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs + pkg-config 0.3.33 — MIT OR Apache-2.0 — https://github.com/rust-lang/pkg-config-rs + poly1305 0.8.0 — Apache-2.0 OR MIT — https://github.com/RustCrypto/universal-hashes + polyval 0.6.2 — Apache-2.0 OR MIT — https://github.com/RustCrypto/universal-hashes + portable-atomic 1.14.0 — Apache-2.0 OR MIT — https://github.com/taiki-e/portable-atomic + portable-atomic-util 0.2.7 — Apache-2.0 OR MIT — https://github.com/taiki-e/portable-atomic-util + potential_utf 0.1.5 — Unicode-3.0 — https://github.com/unicode-org/icu4x + powerfmt 0.2.0 — MIT OR Apache-2.0 — https://github.com/jhpratt/powerfmt + ppv-lite86 0.2.21 — MIT OR Apache-2.0 — https://github.com/cryptocorrosion/cryptocorrosion + prettyplease 0.2.37 — MIT OR Apache-2.0 — https://github.com/dtolnay/prettyplease + proc-macro-crate 3.5.0 — MIT OR Apache-2.0 — https://github.com/bkchr/proc-macro-crate + proc-macro2 1.0.106 — MIT OR Apache-2.0 — https://github.com/dtolnay/proc-macro2 + proptest 1.11.0 — MIT OR Apache-2.0 — https://github.com/proptest-rs/proptest + quick-error 1.2.3 — MIT/Apache-2.0 — http://github.com/tailhook/quick-error + quinn 0.11.11 — MIT OR Apache-2.0 — https://github.com/quinn-rs/quinn + quinn-proto 0.11.15 — MIT OR Apache-2.0 — https://github.com/quinn-rs/quinn + quinn-udp 0.5.14 — MIT OR Apache-2.0 — https://github.com/quinn-rs/quinn + quote 1.0.46 — MIT OR Apache-2.0 — https://github.com/dtolnay/quote + r-efi 5.3.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi + r-efi 6.0.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi + rand 0.9.4 — MIT OR Apache-2.0 — https://github.com/rust-random/rand + rand_chacha 0.9.0 — MIT OR Apache-2.0 — https://github.com/rust-random/rand + rand_core 0.6.4 — MIT OR Apache-2.0 — https://github.com/rust-random/rand + rand_core 0.9.5 — MIT OR Apache-2.0 — https://github.com/rust-random/rand + rand_xorshift 0.4.0 — MIT OR Apache-2.0 — https://github.com/rust-random/rngs + rav1d 1.1.0 — BSD-2-Clause — https://github.com/memorysafety/rav1d + raw-cpuid 11.6.0 — MIT — https://github.com/gz/rust-cpuid + rayon 1.12.0 — MIT OR Apache-2.0 — https://github.com/rayon-rs/rayon + rayon-core 1.13.0 — MIT OR Apache-2.0 — https://github.com/rayon-rs/rayon + rcgen 0.13.2 — MIT OR Apache-2.0 — https://github.com/rustls/rcgen + readme-rustdocifier 0.1.1 — MIT — https://github.com/malaire/readme-rustdocifier + redox_syscall 0.5.18 — MIT — https://gitlab.redox-os.org/redox-os/syscall + reed-solomon-simd 3.1.0 — MIT AND BSD-3-Clause — https://github.com/AndersTrier/reed-solomon-simd + regex 1.12.4 — MIT OR Apache-2.0 — https://github.com/rust-lang/regex + regex-automata 0.4.14 — MIT OR Apache-2.0 — https://github.com/rust-lang/regex + regex-syntax 0.8.11 — MIT OR Apache-2.0 — https://github.com/rust-lang/regex + relm4 0.11.0 — Apache-2.0 OR MIT — https://github.com/Relm4/Relm4 + relm4-css 0.11.0 — Apache-2.0 OR MIT — https://github.com/Relm4/Relm4 + relm4-macros 0.11.0 — Apache-2.0 OR MIT — https://github.com/Relm4/Relm4 + ring 0.17.14 — Apache-2.0 AND ISC — https://github.com/briansmith/ring + rpkg-config 0.1.2 — Zlib OR MIT OR Apache-2.0 — https://github.com/maia-s/rpkg-config-rs + rustc-hash 2.1.2 — Apache-2.0 OR MIT — https://github.com/rust-lang/rustc-hash + rustc_version 0.4.1 — MIT OR Apache-2.0 — https://github.com/djc/rustc-version-rs + rustix 1.1.4 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/rustix + rustls 0.23.41 — Apache-2.0 OR ISC OR MIT — https://github.com/rustls/rustls + rustls-native-certs 0.8.4 — Apache-2.0 OR ISC OR MIT — https://github.com/rustls/rustls-native-certs + rustls-pki-types 1.14.1 — MIT OR Apache-2.0 — https://github.com/rustls/pki-types + rustls-platform-verifier 0.6.2 — MIT OR Apache-2.0 — https://github.com/rustls/rustls-platform-verifier + rustls-platform-verifier-android 0.1.1 — MIT OR Apache-2.0 — https://github.com/rustls/rustls-platform-verifier + rustls-webpki 0.103.13 — ISC — https://github.com/rustls/webpki + rustversion 1.0.22 — MIT OR Apache-2.0 — https://github.com/dtolnay/rustversion + rusty-fork 0.3.1 — MIT/Apache-2.0 — https://github.com/altsysrq/rusty-fork + safe_arch 0.7.4 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/safe_arch + same-file 1.0.6 — Unlicense/MIT — https://github.com/BurntSushi/same-file + schannel 0.1.29 — MIT — https://github.com/steffengy/schannel-rs + scopeguard 1.2.0 — MIT OR Apache-2.0 — https://github.com/bluss/scopeguard + sdl3 0.18.4 — MIT — https://github.com/vhspace/sdl3-rs + sdl3-image-src 3.4.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-image-sys 0.6.4+SDL-image-3.4.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-mixer-src 3.2.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-mixer-sys 0.6.3+SDL-mixer-3.2.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-src 3.4.10 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-sys 0.6.6+SDL-3.4.10 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-ttf-src 3.2.2 — Zlib — https://github.com/maia-s/sdl3-sys-rs + sdl3-ttf-sys 0.6.1+SDL-ttf-3.2.2 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + security-framework 3.7.0 — MIT OR Apache-2.0 — https://github.com/kornelski/rust-security-framework + security-framework-sys 2.17.0 — MIT OR Apache-2.0 — https://github.com/kornelski/rust-security-framework + semver 1.0.28 — MIT OR Apache-2.0 — https://github.com/dtolnay/semver + serde 1.0.228 — MIT OR Apache-2.0 — https://github.com/serde-rs/serde + serde_core 1.0.228 — MIT OR Apache-2.0 — https://github.com/serde-rs/serde + serde_derive 1.0.228 — MIT OR Apache-2.0 — https://github.com/serde-rs/serde + serde_json 1.0.150 — MIT OR Apache-2.0 — https://github.com/serde-rs/json + serde_spanned 0.6.9 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + serde_spanned 1.1.1 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + sha2 0.10.9 — MIT OR Apache-2.0 — https://github.com/RustCrypto/hashes + sharded-slab 0.1.7 — MIT — https://github.com/hawkw/sharded-slab + shlex 1.3.0 — MIT OR Apache-2.0 — https://github.com/comex/rust-shlex + shlex 2.0.1 — MIT OR Apache-2.0 — https://github.com/comex/rust-shlex + signal-hook-registry 1.4.8 — MIT OR Apache-2.0 — https://github.com/vorner/signal-hook + simd-adler32 0.3.9 — MIT — https://github.com/mcountryman/simd-adler32 + siphasher 1.0.3 — MIT/Apache-2.0 — https://github.com/jedisct1/rust-siphash + skia-bindings 0.87.0 — MIT — https://github.com/rust-skia/rust-skia + skia-safe 0.87.0 — MIT — https://github.com/rust-skia/rust-skia + slab 0.4.12 — MIT — https://github.com/tokio-rs/slab + smallvec 1.15.2 — MIT OR Apache-2.0 — https://github.com/servo/rust-smallvec + socket-pktinfo 0.4.0 — MIT — https://github.com/pixsper/socket-pktinfo + socket2 0.6.4 — MIT OR Apache-2.0 — https://github.com/rust-lang/socket2 + spake2 0.4.0 — MIT OR Apache-2.0 — https://github.com/RustCrypto/PAKEs/tree/master/spake2 + spin 0.9.8 — MIT — https://github.com/mvdnes/spin-rs.git + stable_deref_trait 1.2.1 — MIT OR Apache-2.0 — https://github.com/storyyeller/stable_deref_trait + strsim 0.11.1 — MIT — https://github.com/rapidfuzz/strsim-rs + strum 0.26.3 — MIT — https://github.com/Peternator7/strum + strum_macros 0.26.4 — MIT — https://github.com/Peternator7/strum + subtle 2.6.1 — BSD-3-Clause — https://github.com/dalek-cryptography/subtle + syn 2.0.118 — MIT OR Apache-2.0 — https://github.com/dtolnay/syn + synstructure 0.13.2 — MIT — https://github.com/mystor/synstructure + system-deps 7.0.8 — MIT OR Apache-2.0 — https://github.com/gdesmott/system-deps + tar 0.4.46 — MIT OR Apache-2.0 — https://github.com/composefs/tar-rs + target-lexicon 0.13.5 — Apache-2.0 WITH LLVM-exception — https://github.com/bytecodealliance/target-lexicon + tempfile 3.27.0 — MIT OR Apache-2.0 — https://github.com/Stebalien/tempfile + thiserror 1.0.69 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror + thiserror 2.0.18 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror + thiserror-impl 1.0.69 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror + thiserror-impl 2.0.18 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror + thread_local 1.1.9 — MIT OR Apache-2.0 — https://github.com/Amanieu/thread_local-rs + time 0.3.51 — MIT OR Apache-2.0 — https://github.com/time-rs/time + time-core 0.1.9 — MIT OR Apache-2.0 — https://github.com/time-rs/time + time-macros 0.2.30 — MIT OR Apache-2.0 — https://github.com/time-rs/time + tinystr 0.8.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x + tinytemplate 1.2.1 — Apache-2.0 OR MIT — https://github.com/bheisler/TinyTemplate + tinyvec 1.11.0 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/tinyvec + tinyvec_macros 0.1.1 — MIT OR Apache-2.0 OR Zlib — https://github.com/Soveu/tinyvec_macros + to_method 1.1.0 — CC0-1.0 — https://github.com/whentze/to_method + tokio 1.52.3 — MIT — https://github.com/tokio-rs/tokio + tokio-macros 2.7.0 — MIT — https://github.com/tokio-rs/tokio + toml 0.8.23 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml 0.9.12+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml 1.1.2+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_datetime 0.6.11 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_datetime 0.7.5+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_datetime 1.1.1+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_edit 0.22.27 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_edit 0.25.12+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_parser 1.1.2+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_write 0.1.2 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_writer 1.1.1+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + tracing 0.1.44 — MIT — https://github.com/tokio-rs/tracing + tracing-attributes 0.1.31 — MIT — https://github.com/tokio-rs/tracing + tracing-core 0.1.36 — MIT — https://github.com/tokio-rs/tracing + tracing-log 0.2.0 — MIT — https://github.com/tokio-rs/tracing + tracing-subscriber 0.3.23 — MIT — https://github.com/tokio-rs/tracing + typenum 1.20.1 — MIT OR Apache-2.0 — https://github.com/paholg/typenum + unarray 0.1.4 — MIT OR Apache-2.0 — https://github.com/cameron1024/unarray + unicode-ident 1.0.24 — (MIT OR Apache-2.0) AND Unicode-3.0 — https://github.com/dtolnay/unicode-ident + unicode-segmentation 1.13.3 — MIT OR Apache-2.0 — https://github.com/unicode-rs/unicode-segmentation + unicode-width 0.2.2 — MIT OR Apache-2.0 — https://github.com/unicode-rs/unicode-width + universal-hash 0.5.1 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits + untrusted 0.9.0 — ISC — https://github.com/briansmith/untrusted + ureq 2.12.1 — MIT OR Apache-2.0 — https://github.com/algesten/ureq + url 2.5.8 — MIT OR Apache-2.0 — https://github.com/servo/rust-url + utf8_iter 1.0.4 — Apache-2.0 OR MIT — https://github.com/hsivonen/utf8_iter + utf8parse 0.2.2 — Apache-2.0 OR MIT — https://github.com/alacritty/vte + valuable 0.1.1 — MIT — https://github.com/tokio-rs/valuable + vcpkg 0.2.15 — MIT/Apache-2.0 — https://github.com/mcgoo/vcpkg-rs + version-compare 0.2.1 — MIT — https://gitlab.com/timvisee/version-compare + version_check 0.9.5 — MIT/Apache-2.0 — https://github.com/SergioBenitez/version_check + wait-timeout 0.2.1 — MIT/Apache-2.0 — https://github.com/alexcrichton/wait-timeout + walkdir 2.5.0 — Unlicense/MIT — https://github.com/BurntSushi/walkdir + wasapi 0.23.0 — MIT — https://github.com/HEnquist/wasapi-rs + wasi 0.11.1+wasi-snapshot-preview1 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/wasi + wasip2 1.0.4+wasi-0.2.12 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/wasi-rs + wasm-bindgen 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen + wasm-bindgen-macro 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/macro + wasm-bindgen-macro-support 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/macro-support + wasm-bindgen-shared 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/shared + web-time 1.1.0 — MIT OR Apache-2.0 — https://github.com/daxpedda/web-time + webpki-root-certs 1.0.8 — CDLA-Permissive-2.0 — https://github.com/rustls/webpki-roots + webpki-roots 0.26.11 — CDLA-Permissive-2.0 — https://github.com/rustls/webpki-roots + webpki-roots 1.0.8 — CDLA-Permissive-2.0 — https://github.com/rustls/webpki-roots + wide 0.7.33 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/wide + winapi-util 0.1.11 — Unlicense OR MIT — https://github.com/BurntSushi/winapi-util + windows 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-collections 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-collections 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-core 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-core 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-future 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-future 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-implement 0.60.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-implement 0.60.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-interface 0.59.3 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-interface 0.59.3 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-link 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-link 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-numerics 0.3.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-numerics 0.3.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-reference 0.1.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-result 0.4.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-result 0.4.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-strings 0.5.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-strings 0.5.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-sys 0.45.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-sys 0.52.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-sys 0.59.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-sys 0.60.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-sys 0.61.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-targets 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-targets 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-targets 0.53.5 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-threading 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-threading 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-time 0.1.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_aarch64_gnullvm 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_aarch64_gnullvm 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_aarch64_gnullvm 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_aarch64_msvc 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_aarch64_msvc 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_aarch64_msvc 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_gnu 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_gnu 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_gnu 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_gnullvm 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_gnullvm 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_msvc 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_msvc 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_msvc 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_gnu 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_gnu 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_gnu 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_gnullvm 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_gnullvm 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_gnullvm 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_msvc 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_msvc 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_msvc 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + winnow 0.7.15 — MIT — https://github.com/winnow-rs/winnow + winnow 1.0.3 — MIT — https://github.com/winnow-rs/winnow + winreg 0.56.0 — MIT — https://github.com/gentoo90/winreg-rs + winresource 0.1.31 — MIT — https://github.com/BenjaminRi/winresource + wit-bindgen 0.57.1 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/wit-bindgen + writeable 0.6.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x + xattr 1.6.1 — MIT OR Apache-2.0 — https://github.com/Stebalien/xattr + yasna 0.5.2 — MIT OR Apache-2.0 — https://github.com/qnighy/yasna.rs + yoke 0.8.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x + yoke-derive 0.8.2 — Unicode-3.0 — https://github.com/unicode-org/icu4x + zerocopy 0.7.35 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy + zerocopy 0.8.52 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy + zerocopy-derive 0.7.35 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy + zerocopy-derive 0.8.52 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy + zerofrom 0.1.8 — Unicode-3.0 — https://github.com/unicode-org/icu4x + zerofrom-derive 0.1.7 — Unicode-3.0 — https://github.com/unicode-org/icu4x + zeroize 1.9.0 — Apache-2.0 OR MIT — https://github.com/RustCrypto/utils + zerotrie 0.2.4 — Unicode-3.0 — https://github.com/unicode-org/icu4x + zerovec 0.11.6 — Unicode-3.0 — https://github.com/unicode-org/icu4x + zerovec-derive 0.11.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x + zmij 1.0.21 — MIT — https://github.com/dtolnay/zmij + +---------------------------------------------------------------------------- +Crates whose package did not embed a license file (SPDX + source only) +---------------------------------------------------------------------------- + anes 0.1.6 — MIT OR Apache-2.0 — https://github.com/zrzka/anes-rs + atomig-macro 0.4.0 — MIT/Apache-2.0 — https://github.com/LukasKalbertodt/atomig/ + cookie-factory 0.3.3 — MIT — https://github.com/rust-bakery/cookie-factory + defmt-parser 1.0.0 — MIT OR Apache-2.0 — https://github.com/knurling-rs/defmt + jni-sys-macros 0.4.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys + openh264 0.9.3 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs + openh264-sys2 0.9.6 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs + r-efi 5.3.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi + r-efi 6.0.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi + rustls-platform-verifier-android 0.1.1 — MIT OR Apache-2.0 — https://github.com/rustls/rustls-platform-verifier + sdl3-image-src 3.4.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-mixer-src 3.2.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-ttf-src 3.2.2 — Zlib — https://github.com/maia-s/sdl3-sys-rs + skia-bindings 0.87.0 — MIT — https://github.com/rust-skia/rust-skia + skia-safe 0.87.0 — MIT — https://github.com/rust-skia/rust-skia + valuable 0.1.1 — MIT — https://github.com/tokio-rs/valuable + yasna 0.5.2 — MIT OR Apache-2.0 — https://github.com/qnighy/yasna.rs + +============================================================================ +FULL LICENSE TEXTS (deduplicated) +============================================================================ + +---------------------------------------------------------------------------- +The following license (LICENSE-0BSD) applies to: adler2 2.0.1 +---------------------------------------------------------------------------- +Copyright (C) Jonas Schievink + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: adler2 2.0.1, proc-macro-crate 3.5.0 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/LICENSE-2.0 + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: adler2 2.0.1, anyhow 1.0.103, async-channel 2.5.0, concurrent-queue 2.5.0, curve25519-dalek-derive 0.1.1, displaydoc 0.2.6, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, flume 0.12.0, hermit-abi 0.5.2, is-terminal 0.4.17, itoa 1.0.18, linux-raw-sys 0.12.1, minimal-lexical 0.2.1, once_cell 1.21.4, parking 2.2.1, paste 1.0.15, pin-project-lite 0.2.17, portable-atomic 1.14.0, portable-atomic-util 0.2.7, prettyplease 0.2.37, proc-macro-crate 3.5.0, proc-macro2 1.0.106, quote 1.0.46, relm4 0.11.0, relm4-css 0.11.0, relm4-macros 0.11.0, rustc-hash 2.1.2, rustix 1.1.4, rustversion 1.0.22, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, syn 2.0.118, system-deps 7.0.8, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1, zmij 1.0.21 +---------------------------------------------------------------------------- +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 following license (LICENSE-APACHE) applies to: aead 0.5.2, aes 0.8.4, aes-gcm 0.10.3, block-buffer 0.10.4, block-padding 0.3.3, chacha20 0.9.1, chacha20poly1305 0.10.1, cipher 0.4.4, const-oid 0.9.6, cpufeatures 0.2.17, crypto-common 0.1.7, ctr 0.9.2, digest 0.10.7, ghash 0.5.1, hkdf 0.12.4, hmac 0.12.1, inout 0.1.4, opaque-debug 0.3.1, poly1305 0.8.0, polyval 0.6.2, sha2 0.10.9, spake2 0.4.0, universal-hash 0.5.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: aead 0.5.2 +---------------------------------------------------------------------------- +Copyright (c) 2019 The RustCrypto Project Developers +Copyright (c) 2019 MobileCoin, LLC + +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 following license (LICENSE-MIT) applies to: aes 0.8.4 +---------------------------------------------------------------------------- +Copyright (c) 2018 Artyom Pavlov + +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 following license (LICENSE-MIT) applies to: aes-gcm 0.10.3, chacha20poly1305 0.10.1 +---------------------------------------------------------------------------- +Copyright (c) 2019 The RustCrypto Project Developers + +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 following license (COPYING) applies to: aho-corasick 1.1.4, byteorder 1.5.0, jiff 0.2.35, jiff-core 0.1.0, jiff-static 0.2.35, memchr 2.8.2, same-file 1.0.6, walkdir 2.5.0, winapi-util 0.1.11 +---------------------------------------------------------------------------- +This project is dual-licensed under the Unlicense and MIT licenses. + +You may use this code under the terms of either license. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: aho-corasick 1.1.4, byteorder 1.5.0, jiff 0.2.35, jiff-core 0.1.0, jiff-static 0.2.35, memchr 2.8.2, walkdir 2.5.0 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Andrew Gallant + +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 following license (UNLICENSE) applies to: aho-corasick 1.1.4, byteorder 1.5.0, jiff 0.2.35, jiff-core 0.1.0, jiff-static 0.2.35, memchr 2.8.2, same-file 1.0.6, walkdir 2.5.0, winapi-util 0.1.11 +---------------------------------------------------------------------------- +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to + + +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: annotate-snippets 0.11.5, anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, crc32fast 1.5.0, env_filter 2.0.0, env_logger 0.11.11, is_terminal_polyfill 1.70.2, jni-sys 0.3.1, jni-sys 0.4.1, once_cell_polyfill 1.70.2, quick-error 1.2.3, serde_spanned 0.6.9, serde_spanned 1.1.1, toml 0.8.23, toml 0.9.12+spec-1.1.0, toml 1.1.2+spec-1.1.0, toml_datetime 0.6.11, toml_datetime 0.7.5+spec-1.1.0, toml_datetime 1.1.1+spec-1.1.0, toml_edit 0.22.27, toml_edit 0.25.12+spec-1.1.0, toml_parser 1.1.2+spec-1.1.0, toml_write 0.1.2, toml_writer 1.1.1+spec-1.1.0 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: annotate-snippets 0.11.5, anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, env_filter 2.0.0, env_logger 0.11.11, is_terminal_polyfill 1.70.2, once_cell_polyfill 1.70.2, serde_spanned 0.6.9, serde_spanned 1.1.1, toml 0.8.23, toml 0.9.12+spec-1.1.0, toml 1.1.2+spec-1.1.0, toml_datetime 0.6.11, toml_datetime 0.7.5+spec-1.1.0, toml_datetime 1.1.1+spec-1.1.0, toml_edit 0.22.27, toml_edit 0.25.12+spec-1.1.0, toml_parser 1.1.2+spec-1.1.0, toml_write 0.1.2, toml_writer 1.1.1+spec-1.1.0 +---------------------------------------------------------------------------- +Copyright (c) Individual 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 following license (LICENSE-APACHE) applies to: anyhow 1.0.103, fastbloom 0.14.1, itoa 1.0.18, libc 0.2.186, paste 1.0.15, prettyplease 0.2.37, proc-macro2 1.0.106, quote 1.0.46, relm4 0.11.0, relm4-css 0.11.0, relm4-macros 0.11.0, rustc-hash 2.1.2, rustversion 1.0.22, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, syn 2.0.118, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, utf8parse 0.2.2 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + + +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: ash 0.38.0+1.3.281 +---------------------------------------------------------------------------- +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +Copyright 2016 Maik Klein + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: ash 0.38.0+1.3.281 +---------------------------------------------------------------------------- +Copyright (c) 2016 ASH + +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 following license (LICENSE-APACHE) applies to: assert_matches 1.5.0, async-channel 2.5.0, autocfg 1.5.1, base64 0.22.1, bitflags 1.3.2, bitflags 2.13.0, bumpalo 3.20.3, cast 0.3.0, cc 1.2.65, cexpr 0.6.0, cfg-if 1.0.4, cmake 0.1.58, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, displaydoc 0.2.6, either 1.16.0, equivalent 1.0.2, errno 0.3.14, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, filetime 0.2.29, find-msvc-tools 0.1.9, fixedbitset 0.5.7, flate2 1.1.9, fnv 1.0.7, form_urlencoded 1.2.2, glob 0.3.3, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.0, itertools 0.10.5, itertools 0.13.0, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, lazy_static 1.5.0, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, nasm-rs 0.3.2, num-integer 0.1.46, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, pkg-config 0.3.33, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, rustc_version 0.4.1, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, stable_deref_trait 1.2.1, system-deps 7.0.8, tar 0.4.46, tempfile 3.27.0, thread_local 1.1.9, tinytemplate 1.2.1, unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, vcpkg 0.2.15, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, wit-bindgen 0.57.1, xattr 1.6.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: assert_matches 1.5.0 +---------------------------------------------------------------------------- +Copyright (c) 2016 Murarth + +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 following license (LICENSE-APACHE) applies to: atomig 0.4.3, bit-set 0.8.0, bit-vec 0.8.0, cfg-expr 0.20.8, defmt 1.1.1, defmt-macros 1.1.1, minimal-lexical 0.2.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: atomig 0.4.3 +---------------------------------------------------------------------------- +Copyright (c) 2016 Project Developers + +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 following license (LICENSE.md) applies to: audiopus_sys 0.2.2 +---------------------------------------------------------------------------- +ISC License + +Copyright (c) 2019, Lakelezz + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: autocfg 1.5.1 +---------------------------------------------------------------------------- +Copyright (c) 2018 Josh Stone + +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 following license (LICENSE-MIT) applies to: base64 0.22.1 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Alice Maz + +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 following license (bazzite.txt) applies to: Bazzite logo (vendored, assets/os-icons) +---------------------------------------------------------------------------- +Bazzite — the `bazzite` mark in assets/os-icons/ is derived from the Bazzite logo in +the Bazzite source repository (repo_content/Bazzite.svg). + +Copyright (c) Universal Blue (https://github.com/ublue-os/bazzite) + +Licensed under the Apache License, Version 2.0, +https://www.apache.org/licenses/LICENSE-2.0. + +Modifications: the logo's "b" letterform was lifted out of the surrounding badge, the +gradient and decorative overlays were dropped, and the path was translated and scaled +into a 24x24 box with a monochrome fill (fill="currentColor"). + +Brand icons are trademarks of their respective owners and are used for identification +purposes only; their use does not imply endorsement. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: bindgen 0.72.1 +---------------------------------------------------------------------------- +BSD 3-Clause License + +Copyright (c) 2013, Jyun-Yan You +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: bit-set 0.8.0, bit-vec 0.8.0 +---------------------------------------------------------------------------- +Copyright (c) 2023 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: bitflags 1.3.2, bitflags 2.13.0, glob 0.3.3, log 0.4.33, num-integer 0.1.46, num-traits 0.2.19, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11 +---------------------------------------------------------------------------- +Copyright (c) 2014 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: block-buffer 0.10.4, block-padding 0.3.3 +---------------------------------------------------------------------------- +Copyright (c) 2018-2019 The RustCrypto Project Developers + +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 following license (LICENSE-MIT) applies to: bumpalo 3.20.3 +---------------------------------------------------------------------------- +Copyright (c) 2019 Nick Fitzgerald + +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 following license (LICENSE-APACHE) applies to: bytemuck 1.25.0, bytemuck_derive 1.10.2, safe_arch 0.7.4 +---------------------------------------------------------------------------- +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + + "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: bytemuck 1.25.0, bytemuck_derive 1.10.2 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2019 Daniel "Lokathor" Gee. + +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 (including the next paragraph) 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 following license (LICENSE-ZLIB) applies to: bytemuck 1.25.0, bytemuck_derive 1.10.2, tinyvec 1.11.0 +---------------------------------------------------------------------------- +Copyright (c) 2019 Daniel "Lokathor" Gee. + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: bytes 1.12.0 +---------------------------------------------------------------------------- +Copyright (c) 2018 Carl Lerche + +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 following license (COPYRIGHT) applies to: cairo-rs 0.22.0, gdk-pixbuf 0.22.0, gdk4 0.11.2, gio 0.22.6, glib 0.22.7, glib-build-tools 0.22.8, glib-macros 0.22.6, graphene-rs 0.22.0, gsk4 0.11.1, gtk4 0.11.3, gtk4-macros 0.11.0, pango 0.22.6 +---------------------------------------------------------------------------- +The gtk-rs Project is licensed under the MIT license, see the LICENSE file or +. + +Copyrights in the gtk-rs Project project are retained by their contributors. +No copyright assignment is required to contribute to the gtk-rs Project +project. + +For full authorship information, see the version control history. + +This project provides interoperability with various GNOME libraries but +doesn't distribute any parts of them. Distributing compiled libraries and +executables that link to those libraries may be subject to terms of the GNU +LGPL or other licenses. For more information check the license of each GNOME +library. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: cairo-rs 0.22.0, cairo-sys-rs 0.22.0, gdk-pixbuf 0.22.0, gdk-pixbuf-sys 0.22.0, gdk4 0.11.2, gdk4-sys 0.11.2, gio 0.22.6, gio-sys 0.22.0, glib 0.22.7, glib-build-tools 0.22.8, glib-macros 0.22.6, glib-sys 0.22.6, gobject-sys 0.22.6, graphene-rs 0.22.0, graphene-sys 0.22.0, gsk4 0.11.1, gsk4-sys 0.11.1, gtk4 0.11.3, gtk4-macros 0.11.0, gtk4-sys 0.11.3, libadwaita 0.9.1, libadwaita-sys 0.9.1, pango 0.22.6, pango-sys 0.22.0 +---------------------------------------------------------------------------- +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 following license (LICENSE-MIT) applies to: cast 0.3.0 +---------------------------------------------------------------------------- +Copyright (c) 2014-2017 Jorge Aparicio + +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 following license (LICENSE) applies to: cbindgen 0.29.4 +---------------------------------------------------------------------------- +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, filetime 0.2.29, find-msvc-tools 0.1.9, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 +---------------------------------------------------------------------------- +Copyright (c) 2014 Alex Crichton + +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 following license (COPYRIGHT-RUST.txt) applies to: cesu8 1.1.0 +---------------------------------------------------------------------------- +Short version for non-lawyers: + +The Rust Project is dual-licensed under Apache 2.0 and MIT +terms. + + +Longer version: + +The Rust Project is copyright 2014, The Rust Project +Developers (given in the file AUTHORS.txt). + +Licensed under the Apache License, Version 2.0 + or the MIT +license , +at your option. All files in the project carrying such +notice may not be copied, modified, or distributed except +according to those terms. + + +The Rust Project includes packages written by third parties. +The following third party packages are included, and carry +their own copyright notices and license terms: + +* Two header files that are part of the Valgrind + package. These files are found at src/rt/vg/valgrind.h and + src/rt/vg/memcheck.h, within this distribution. These files + are redistributed under the following terms, as noted in + them: + + for src/rt/vg/valgrind.h: + + This file is part of Valgrind, a dynamic binary + instrumentation framework. + + Copyright (C) 2000-2010 Julian Seward. All rights + reserved. + + Redistribution and use in source and binary forms, with + or without modification, are permitted provided that the + following conditions are met: + + 1. Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + + 2. The origin of this software must not be + misrepresented; you must not claim that you wrote the + original software. If you use this software in a + product, an acknowledgment in the product + documentation would be appreciated but is not + required. + + 3. Altered source versions must be plainly marked as + such, and must not be misrepresented as being the + original software. + + 4. The name of the author may not be used to endorse or + promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + OF SUCH DAMAGE. + + for src/rt/vg/memcheck.h: + + This file is part of MemCheck, a heavyweight Valgrind + tool for detecting memory errors. + + Copyright (C) 2000-2010 Julian Seward. All rights + reserved. + + Redistribution and use in source and binary forms, with + or without modification, are permitted provided that the + following conditions are met: + + 1. Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + + 2. The origin of this software must not be + misrepresented; you must not claim that you wrote the + original software. If you use this software in a + product, an acknowledgment in the product + documentation would be appreciated but is not + required. + + 3. Altered source versions must be plainly marked as + such, and must not be misrepresented as being the + original software. + + 4. The name of the author may not be used to endorse or + promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + OF SUCH DAMAGE. + +* The auxiliary file src/etc/pkg/modpath.iss contains a + library routine compiled, by Inno Setup, into the Windows + installer binary. This file is licensed under the LGPL, + version 3, but, in our legal interpretation, this does not + affect the aggregate "collected work" license of the Rust + distribution (MIT/ASL2) nor any other components of it. We + believe that the terms governing distribution of the + binary Windows installer built from modpath.iss are + therefore LGPL, but not the terms governing distribution + of any of the files installed by such an installer (such + as the Rust compiler or runtime libraries themselves). + +* The src/rt/miniz.c file, carrying an implementation of + RFC1950/RFC1951 DEFLATE, by Rich Geldreich + . All uses of this file are + permitted by the embedded "unlicense" notice + (effectively: public domain with warranty disclaimer). + +* LLVM. Code for this package is found in src/llvm. + + Copyright (c) 2003-2013 University of Illinois at + Urbana-Champaign. All rights reserved. + + Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + + Permission is hereby granted, free of charge, to any + person obtaining a copy of this software and associated + documentation files (the "Software"), to deal with 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: + + * Redistributions of source code must retain the + above copyright notice, this list of conditions + and the following disclaimers. + + * Redistributions in binary form must reproduce the + above copyright notice, this list of conditions + and the following disclaimers in the documentation + and/or other materials provided with the + distribution. + + * Neither the names of the LLVM Team, University of + Illinois at Urbana-Champaign, nor the names of its + contributors may be used to endorse or promote + products derived from this Software without + specific prior written permission. + + 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 CONTRIBUTORS 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 WITH THE SOFTWARE. + +* Additional libraries included in LLVM carry separate + BSD-compatible licenses. See src/llvm/LICENSE.txt for + details. + +* compiler-rt, in src/compiler-rt is dual licensed under + LLVM's license and MIT: + + Copyright (c) 2009-2014 by the contributors listed in + CREDITS.TXT + + All rights reserved. + + Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + + Permission is hereby granted, free of charge, to any + person obtaining a copy of this software and associated + documentation files (the "Software"), to deal with 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: + + * Redistributions of source code must retain the + above copyright notice, this list of conditions + and the following disclaimers. + + * Redistributions in binary form must reproduce the + above copyright notice, this list of conditions + and the following disclaimers in the documentation + and/or other materials provided with the + distribution. + + * Neither the names of the LLVM Team, University of + Illinois at Urbana-Champaign, nor the names of its + contributors may be used to endorse or promote + products derived from this Software without + specific prior written permission. + + 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 CONTRIBUTORS 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 WITH THE SOFTWARE. + + ======================================================== + + Copyright (c) 2009-2014 by the contributors listed in + CREDITS.TXT + + 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. + +* Portions of the FFI code for interacting with the native ABI + is derived from the Clay programming language, which carries + the following license. + + Copyright (C) 2008-2010 Tachyon Technologies. + All rights reserved. + + Redistribution and use in source and binary forms, with + or without modification, are permitted provided that the + following conditions are met: + + 1. Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + + 2. Redistributions in binary form must reproduce the + above copyright notice, this list of conditions and + the following disclaimer in the documentation and/or + other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + DEVELOPERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + OF SUCH DAMAGE. + +* Hoedown, the markdown parser, under src/rt/hoedown, is + licensed as follows. + + Copyright (c) 2008, Natacha Porté + Copyright (c) 2011, Vicent Martí + Copyright (c) 2013, Devin Torres and the Hoedown authors + + Permission to use, copy, modify, and distribute this + software for any purpose with or without fee is hereby + granted, provided that the above copyright notice and + this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR + DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE + INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR + ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA + OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +* libbacktrace, under src/libbacktrace: + + Copyright (C) 2012-2014 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + + Redistribution and use in source and binary forms, with + or without modification, are permitted provided that the + following conditions are met: + + (1) Redistributions of source code must retain the + above copyright notice, this list of conditions and + the following disclaimer. + + (2) Redistributions in binary form must reproduce + the above copyright notice, this list of conditions + and the following disclaimer in the documentation + and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + OF SUCH DAMAGE. */ + +* jemalloc, under src/jemalloc: + + Copyright (C) 2002-2014 Jason Evans + . All rights reserved. + Copyright (C) 2007-2012 Mozilla Foundation. + All rights reserved. + Copyright (C) 2009-2014 Facebook, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright notice(s), + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice(s), + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) + BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + OF SUCH DAMAGE. + +* Additional copyright may be retained by contributors other + than Mozilla, the Rust Project Developers, or the parties + enumerated in this file. Such copyright can be determined + on a case-by-case basis by examining the author of each + portion of a file in the revision-control commit records + of the project, or by consulting representative comments + claiming copyright ownership for a file. + + For example, the text: + + "Copyright (c) 2011 Google Inc." + + appears in some files, and these files thereby denote + that their author and copyright-holder is Google Inc. + + In all such cases, the absence of explicit licensing text + indicates that the contributor chose to license their work + for distribution under identical terms to those Mozilla + has chosen for the collective work, enumerated at the top + of this file. The only difference is the retention of + copyright itself, held by the contributor. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: cexpr 0.6.0 +---------------------------------------------------------------------------- +(C) Copyright 2016 Jethro G. Beekman + +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 following license (LICENSE-MIT) applies to: cfg-expr 0.20.8 +---------------------------------------------------------------------------- +Copyright (c) 2019 Embark Studios + +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 following license (LICENSE) applies to: cfg_aliases 0.2.1 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2020 Katharos Technology + +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 following license (NOTICES.md) applies to: cfg_aliases 0.2.1 +---------------------------------------------------------------------------- +# 3rd Party Notices + +The `cfg_aliases!` macro uses a lot of the code from [`tectonic_cfg_support::target_cfg!`] macro which is under the following license: + +[`tectonic_cfg_support::target_cfg!`]: https://github.com/tectonic-typesetting/tectonic/blob/f2439b936470ad27bdf92882064bc4702ee01899/cfg_support/src/lib.rs#L166 + + tectonic_cfg_support is licensed under the MIT License. + + 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 following license (LICENSE-MIT) applies to: chacha20 0.9.1 +---------------------------------------------------------------------------- +Copyright (c) 2019-2023 The RustCrypto Project Developers + +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 following license (LICENSE) applies to: ciborium 0.2.2, ciborium-io 0.2.2, ciborium-ll 0.2.2, clang-sys 1.8.1, flume 0.12.0, fragile 2.1.0, lru-slab 0.1.2, quinn 0.11.11, quinn-proto 0.11.15, quinn-udp 0.5.14, rpkg-config 0.1.2, rustls-platform-verifier 0.6.2, tinyvec 1.11.0, unarray 0.1.4, ureq 2.12.1, utf8_iter 1.0.4, zeroize 1.9.0 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: cipher 0.4.4 +---------------------------------------------------------------------------- +Copyright (c) 2016-2020 RustCrypto Developers + +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 following license (LICENSE) applies to: combine 4.6.7 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Markus Westerlind + +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 following license (LICENSE-MIT) applies to: const-oid 0.9.6 +---------------------------------------------------------------------------- +Copyright (c) 2020-2022 The RustCrypto Project Developers + +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 following license (LICENSE) applies to: convert_case 0.8.0 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2025 rutrum + +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 following license (LICENSE-MIT) applies to: core-foundation 0.10.1, core-foundation-sys 0.8.7 +---------------------------------------------------------------------------- +Copyright (c) 2012-2013 Mozilla Foundation + +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 following license (LICENSE-MIT) applies to: cpufeatures 0.2.17 +---------------------------------------------------------------------------- +Copyright (c) 2020-2025 The RustCrypto Project Developers + +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 following license (LICENSE-MIT) applies to: crc32fast 1.5.0 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2018 Sam Rijs, Alex Crichton and 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 following license (LICENSE-MIT) applies to: criterion 0.5.1, criterion-plot 0.5.0 +---------------------------------------------------------------------------- +Copyright (c) 2014 Jorge Aparicio + +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 following license (LICENSE-MIT) applies to: crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2019 The Crossbeam Project Developers + +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 following license (LICENSE) applies to: crunchy 0.2.4 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright 2017-2023 Eira Fransham. + +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 following license (LICENSE-MIT) applies to: crypto-common 0.1.7 +---------------------------------------------------------------------------- +Copyright (c) 2021 RustCrypto Developers + +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 following license (LICENSE-MIT) applies to: ctr 0.9.2 +---------------------------------------------------------------------------- +Copyright (c) 2018-2022 RustCrypto Developers +Copyright (c) 2018 Artyom Pavlov + +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 following license (LICENSE) applies to: curve25519-dalek 4.1.3 +---------------------------------------------------------------------------- +Copyright (c) 2016-2021 isis agora lovecruft. All rights reserved. +Copyright (c) 2016-2021 Henry de Valence. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== + +Portions of curve25519-dalek were originally derived from Adam Langley's +Go ed25519 implementation, found at , +under the following licence: + +======================================================================== + +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: defmt 1.1.1, defmt-macros 1.1.1 +---------------------------------------------------------------------------- +Copyright (c) Ferrous Systems + +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 following license (LICENSE-Apache) applies to: deranged 0.5.8 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Jacob Pratt et al. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: deranged 0.5.8 +---------------------------------------------------------------------------- +Copyright (c) 2024 Jacob Pratt et al. + +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 following license (LICENSE-MIT) applies to: digest 0.10.7, hmac 0.12.1 +---------------------------------------------------------------------------- +Copyright (c) 2017 Artyom Pavlov + +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 following license (LICENSE-MIT) applies to: either 1.16.0, itertools 0.10.5, itertools 0.13.0 +---------------------------------------------------------------------------- +Copyright (c) 2015 + +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 following license (LICENSE-MIT) applies to: equivalent 1.0.2 +---------------------------------------------------------------------------- +Copyright (c) 2016--2023 + +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 following license (LICENSE-MIT) applies to: errno 0.3.14 +---------------------------------------------------------------------------- +Copyright (c) 2014 Chris Wong + +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 following license (LICENSE-MIT) applies to: fastbloom 0.14.1 +---------------------------------------------------------------------------- +Copyright (c) 2023 Thomas Pendock + +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 following license (COPYRIGHT) applies to: fiat-crypto 0.2.9 +---------------------------------------------------------------------------- +SPDX-License-Identifier: MIT OR Apache-2.0 OR BSD-1-Clause + +Fiat Cryptography is licensed under the MIT License or +, the Apache License, Version 2.0 + or , or +the BSD 1-Clause License or +, at your option. + + +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: fiat-crypto 0.2.9 +---------------------------------------------------------------------------- +The Apache License, Version 2.0 (Apache-2.0) + +Copyright 2015-2020 the fiat-crypto authors (see the AUTHORS file) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-BSD-1) applies to: fiat-crypto 0.2.9 +---------------------------------------------------------------------------- +The BSD 1-Clause License (BSD-1-Clause) + +Copyright (c) 2015-2020 the fiat-crypto authors (see the AUTHORS file) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +THIS SOFTWARE IS PROVIDED BY the fiat-crypto authors "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Berkeley Software Design, +Inc. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: fiat-crypto 0.2.9 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015-2020 the fiat-crypto authors (see the AUTHORS file). + +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 following license (LICENSE-APACHE) applies to: field-offset 0.3.6, half 2.7.1, miniz_oxide 0.8.9, num-conv 0.2.2, pin-project-lite 0.2.17, portable-atomic 1.14.0, portable-atomic-util 0.2.7, time 0.3.51, time-core 0.1.9, time-macros 0.2.30 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: field-offset 0.3.6 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2016-2021 Diggory Blake, and other 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 following license (LICENSE-MIT) applies to: fixedbitset 0.5.7 +---------------------------------------------------------------------------- +Copyright (c) 2015-2017 + +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 following license (LICENSE-MIT) applies to: flate2 1.1.9 +---------------------------------------------------------------------------- +Copyright (c) 2014-2026 Alex Crichton + +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 following license (LICENSE-MIT) applies to: fnv 1.0.7 +---------------------------------------------------------------------------- +Copyright (c) 2017 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 following license (LICENSE) applies to: foldhash 0.2.0 +---------------------------------------------------------------------------- +Copyright (c) 2024 Orson Peters + +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a product, + an acknowledgment in the product documentation would be appreciated but is + not required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (font-awesome-brands.txt) applies to: Font Awesome Free brand icons (vendored, assets/os-icons) +---------------------------------------------------------------------------- +Font Awesome Free — brand icons (apple, linux, steam, ubuntu, fedora, opensuse in +assets/os-icons/) are from Font Awesome Free. + +Copyright (c) Fonticons, Inc. (https://fontawesome.com) + +Font Awesome Free icons are licensed under the Creative Commons Attribution 4.0 +International license (CC BY 4.0), https://creativecommons.org/licenses/by/4.0/. +The icons are redistributed here as monochrome SVG path data with no +modifications beyond color normalization (fill="currentColor"). + +Per the Font Awesome Free license (https://fontawesome.com/license/free): +"Font Awesome Free is free, open source, and GPL friendly. You can use it for +commercial projects, open source projects, or really almost whatever you want. +Attribution is required by MIT, SIL OFL, and CC BY licenses." + +Brand icons are trademarks of their respective owners and are used for +identification purposes only; their use does not imply endorsement. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: form_urlencoded 1.2.2 +---------------------------------------------------------------------------- +Copyright (c) 2013-2016 The rust-url developers + +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 following license (LICENSE-APACHE) applies to: futures 0.3.32, futures-channel 0.3.32, futures-core 0.3.32, futures-executor 0.3.32, futures-io 0.3.32, futures-macro 0.3.32, futures-sink 0.3.32, futures-task 0.3.32, futures-util 0.3.32 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: futures 0.3.32, futures-channel 0.3.32, futures-core 0.3.32, futures-executor 0.3.32, futures-io 0.3.32, futures-macro 0.3.32, futures-sink 0.3.32, futures-task 0.3.32, futures-util 0.3.32 +---------------------------------------------------------------------------- +Copyright (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +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 following license (LICENSE) applies to: generic-array 0.14.7 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Bartłomiej Kamiński + +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 following license (LICENSE-APACHE) applies to: getrandom 0.2.17, getrandom 0.3.4, getrandom 0.4.3 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: getrandom 0.2.17 +---------------------------------------------------------------------------- +Copyright (c) 2018-2024 The rust-random Project Developers +Copyright (c) 2014 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: getrandom 0.3.4 +---------------------------------------------------------------------------- +Copyright (c) 2018-2025 The rust-random Project Developers +Copyright (c) 2014 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: getrandom 0.4.3 +---------------------------------------------------------------------------- +Copyright (c) 2018-2026 The rust-random Project Developers +Copyright (c) 2014 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: ghash 0.5.1 +---------------------------------------------------------------------------- +Copyright (c) 2019 RustCrypto Developers + +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 following license (LICENSE) applies to: Granite subset (vendored, crates/pyrowave-sys) +---------------------------------------------------------------------------- +Copyright (c) 2017-2026 Hans-Kristian Arntzen + +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 following license (LICENSE-MIT) applies to: half 2.7.1 +---------------------------------------------------------------------------- +MIT License + +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 following license (LICENSE-MIT) applies to: hashbrown 0.17.1 +---------------------------------------------------------------------------- +Copyright (c) 2016 Amanieu d'Antras + +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 following license (LICENSE-MIT) applies to: heck 0.5.0, unicode-segmentation 1.13.3, unicode-width 0.2.2 +---------------------------------------------------------------------------- +Copyright (c) 2015 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: hkdf 0.12.4 +---------------------------------------------------------------------------- +Copyright (c) 2015-2018 Vlad Filippov +Copyright (c) 2018-2021 RustCrypto Developers + +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 following license (LICENSE) applies to: icu_collections 2.2.0, icu_locale_core 2.2.0, icu_normalizer 2.2.0, icu_normalizer_data 2.2.0, icu_properties 2.2.0, icu_properties_data 2.2.0, icu_provider 2.2.0, litemap 0.8.2, potential_utf 0.1.5, tinystr 0.8.3, writeable 0.6.3, yoke 0.8.3, yoke-derive 0.8.2, zerofrom 0.1.8, zerofrom-derive 0.1.7, zerotrie 0.2.4, zerovec 0.11.6, zerovec-derive 0.11.3 +---------------------------------------------------------------------------- +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: idna 1.1.0, percent-encoding 2.3.2, url 2.5.8 +---------------------------------------------------------------------------- +Copyright (c) 2013-2025 The rust-url developers + +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 following license (LICENSE-MIT) applies to: idna_adapter 1.2.2 +---------------------------------------------------------------------------- +Copyright (c) The rust-url developers + +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 following license (LICENSE-BSD) applies to: if-addrs 0.13.4, if-addrs 0.15.0 +---------------------------------------------------------------------------- +Copyright 2018 MaidSafe.net limited. +Copyright 2020 messense + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: if-addrs 0.13.4, if-addrs 0.15.0 +---------------------------------------------------------------------------- +Copyright 2018 MaidSafe.net limited. +Copyright 2020 messense + +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 following license (LICENSE-MIT) applies to: indexmap 2.14.0 +---------------------------------------------------------------------------- +Copyright (c) 2016--2017 + +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 following license (LICENSE-MIT) applies to: inout 0.1.4 +---------------------------------------------------------------------------- +Copyright (c) 2022 The RustCrypto Project Developers +Copyright (c) 2022 Artyom Pavlov + +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 following license (LICENSE-MIT-atty) applies to: is-terminal 0.4.17 +---------------------------------------------------------------------------- +Portions of this project are derived from atty, which bears the following +copyright notice and permission notice: + +Copyright (c) 2015-2019 Doug Tangren + +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 following license (LICENSE-MIT) applies to: jni 0.21.1 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2016 Prevoty, Inc. and jni-rs 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 following license (LICENSE-MIT) applies to: jni-sys 0.3.1, jni-sys 0.4.1 +---------------------------------------------------------------------------- +Copyright (c) 2015 The rust-jni-sys Developers + +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 following license (LICENSE-MIT) applies to: lazy_static 1.5.0, rayon 1.12.0, rayon-core 1.13.0 +---------------------------------------------------------------------------- +Copyright (c) 2010 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: libc 0.2.186 +---------------------------------------------------------------------------- +Copyright (c) The Rust Project Developers + +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 following license (LICENSE) applies to: libloading 0.8.9 +---------------------------------------------------------------------------- +Copyright © 2015, Simonas Kazlauskas + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without +fee is hereby granted, provided that the above copyright notice and this permission notice appear +in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +---------------------------------------------------------------------------- +The following license (LICENSE.txt) applies to: libm 0.2.16 +---------------------------------------------------------------------------- +rust-lang/libm as a whole is available for use under the MIT license: + +------------------------------------------------------------------------------ +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. +------------------------------------------------------------------------------ + +As a contributor, you agree that your code can be used under either the MIT +license or the Apache-2.0 license: + +------------------------------------------------------------------------------ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +------------------------------------------------------------------------------ + +This Rust library contains the following copyrights: + + Copyright (c) 2018 Jorge Aparicio + +Portions of this software are derived from third-party works licensed under +terms compatible with the above MIT license: + +* musl libc https://www.musl-libc.org/. This library contains the following + copyright: + + Copyright © 2005-2020 Rich Felker, et al. + +* The CORE-MATH project https://core-math.gitlabpages.inria.fr/. CORE-MATH + routines are available under the MIT license on a per-file basis. + +The musl libc COPYRIGHT file also includes the following notice relevant to +math portions of the library: + +------------------------------------------------------------------------------ +Much of the math library code (src/math/* and src/complex/*) is +Copyright © 1993,2004 Sun Microsystems or +Copyright © 2003-2011 David Schultz or +Copyright © 2003-2009 Steven G. Kargl or +Copyright © 2003-2009 Bruce D. Evans or +Copyright © 2008 Stephen L. Moshier or +Copyright © 2017-2018 Arm Limited +and labelled as such in comments in the individual source files. All +have been licensed under extremely permissive terms. +------------------------------------------------------------------------------ + +Copyright notices are retained in src/* files where relevant. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: libspa 0.9.2, libspa-sys 0.9.2, pipewire 0.9.2, pipewire-sys 0.9.2 +---------------------------------------------------------------------------- +Copyright The pipewire-rs 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 (including the next +paragraph) 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 following license (COPYRIGHT) applies to: linux-raw-sys 0.12.1 +---------------------------------------------------------------------------- +Short version for non-lawyers: + +`linux-raw-sys` is triple-licensed under Apache 2.0 with the LLVM Exception, +Apache 2.0, and MIT terms. + + +Longer version: + +Copyrights in the `linux-raw-sys` project are retained by their contributors. +No copyright assignment is required to contribute to the `linux-raw-sys` +project. + +Some files include code derived from Rust's `libstd`; see the comments in +the code for details. + +Except as otherwise noted (below and/or in individual files), `linux-raw-sys` +is licensed under: + + - the Apache License, Version 2.0, with the LLVM Exception + or + + - the Apache License, Version 2.0 + or + , + - or the MIT license + or + , + +at your option. + + +---------------------------------------------------------------------------- +The following license (LICENSE-Apache-2.0_WITH_LLVM-exception) applies to: linux-raw-sys 0.12.1, rustix 1.1.4, target-lexicon 0.13.5, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: lock_api 0.4.14, nasm-rs 0.3.2, parking_lot 0.12.5, parking_lot_core 0.9.12, rustc_version 0.4.1, thread_local 1.1.9 +---------------------------------------------------------------------------- +Copyright (c) 2016 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: lru-slab 0.1.2 +---------------------------------------------------------------------------- +Copyright (c) 2024 The lru-slab Developers + +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 following license (LICENSE-ZLIB) applies to: lru-slab 0.1.2 +---------------------------------------------------------------------------- +Copyright (c) 2024 The lru-slab Developers + +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a product, an + acknowledgment in the product documentation would be appreciated but is not + required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: matchers 0.2.0 +---------------------------------------------------------------------------- +Copyright (c) 2019 Eliza Weisman + +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 following license (LICENSE-APACHE) applies to: mdns-sd 0.20.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [2021-2022] [Han Xu, keepsimple@gmail.com] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: mdns-sd 0.20.1 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2021-2022, Han Xu, keepsimple@gmail.com + +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 following license (LICENSE) applies to: memoffset 0.9.1 +---------------------------------------------------------------------------- +Copyright (c) 2017 Gilad Naaman + +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 following license (LICENSE.md) applies to: minimal-lexical 0.2.1 +---------------------------------------------------------------------------- +Minimal-lexical is dual licensed under the Apache 2.0 license as well as the MIT +license. See the LICENCE-MIT and the LICENCE-APACHE files for the licenses. + +--- + +`src/bellerophon.rs` is loosely based off the Golang implementation, +found [here](https://github.com/golang/go/blob/b10849fbb97a2244c086991b4623ae9f32c212d0/src/strconv/extfloat.go). +That code (used if the `compact` feature is enabled) is subject to a +[3-clause BSD license](https://github.com/golang/go/blob/b10849fbb97a2244c086991b4623ae9f32c212d0/LICENSE): + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: miniz_oxide 0.8.9 +---------------------------------------------------------------------------- +MIT License + +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2017 Frommi +Copyright (c) 2017-2024 oyvindln + + +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 following license (LICENSE-MIT.md) applies to: miniz_oxide 0.8.9 +---------------------------------------------------------------------------- +MIT License + +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2017 Frommi +Copyright (c) 2017-2024 oyvindln + +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 following license (LICENSE-ZLIB.md) applies to: miniz_oxide 0.8.9 +---------------------------------------------------------------------------- +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2020 Frommi +Copyright (c) 2017-2024 oyvindln + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: mio 1.2.1 +---------------------------------------------------------------------------- +Copyright (c) 2014 Carl Lerche and other MIO 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 following license (LICENSE) applies to: nix 0.30.1 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Carl Lerche + nix-rust Authors + +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 following license (LICENSE) applies to: nom 7.1.3, nom 8.0.0 +---------------------------------------------------------------------------- +Copyright (c) 2014-2019 Geoffroy Couprie + +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 following license (LICENSE) applies to: nu-ansi-term 0.50.3 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2014 Benjamin Sago +Copyright (c) 2021-2022 The Nushell Project Developers + +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 following license (LICENSE-MIT) applies to: num-conv 0.2.2 +---------------------------------------------------------------------------- +Copyright (c) Jacob Pratt + +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 following license (LICENSE-MIT) applies to: oorandom 11.1.5 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2019 Simon Heath + +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 following license (LICENSE-MIT) applies to: opaque-debug 0.3.1 +---------------------------------------------------------------------------- +Copyright (c) 2018-2024 The RustCrypto Project Developers + +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 following license (LICENSE-MIT) applies to: opus 0.3.1 +---------------------------------------------------------------------------- +Copyright (c) 2016 Tad Hardesty + +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 following license (LICENSE-THIRD-PARTY) applies to: parking 2.2.1 +---------------------------------------------------------------------------- +=============================================================================== + +Copyright 2014-2020 The Rust Project Developers + +Licensed under the Apache License, Version 2.0 or the MIT license +, at your +option. All files in the project carrying such notice may not be +copied, modified, or distributed except according to those terms. + + +---------------------------------------------------------------------------- +The following license (LICENSE.md) applies to: pem 3.0.6 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2016 Jonathan Creekmore + +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 following license (LICENSE-MIT) applies to: poly1305 0.8.0 +---------------------------------------------------------------------------- +Copyright (c) 2015-2019 RustCrypto Developers + +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 following license (LICENSE-MIT) applies to: polyval 0.6.2 +---------------------------------------------------------------------------- +Copyright (c) 2019-2023 RustCrypto Developers + +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 following license (LICENSE-Apache) applies to: powerfmt 0.2.0 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Jacob Pratt et al. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: powerfmt 0.2.0 +---------------------------------------------------------------------------- +Copyright (c) 2023 Jacob Pratt et al. + +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 following license (LICENSE-APACHE) applies to: ppv-lite86 0.2.21 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2019 The CryptoCorrosion Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: ppv-lite86 0.2.21 +---------------------------------------------------------------------------- +Copyright (c) 2019 The CryptoCorrosion 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 following license (LICENSE-MIT) applies to: proptest 1.11.0, rusty-fork 0.3.1 +---------------------------------------------------------------------------- +Copyright (c) 2016 FullContact, Inc + +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 following license (LICENSE) applies to: pyrowave (vendored, crates/pyrowave-sys) +---------------------------------------------------------------------------- +Copyright (c) 2025 Hans-Kristian Arntzen + +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 following license (LICENSE-MIT) applies to: quick-error 1.2.3 +---------------------------------------------------------------------------- +Copyright (c) 2015 The quick-error Developers + +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 following license (LICENSE-MIT) applies to: quinn 0.11.11, quinn-proto 0.11.15, quinn-udp 0.5.14 +---------------------------------------------------------------------------- +Copyright (c) 2018 The quinn Developers + +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 following license (COPYRIGHT) applies to: rand 0.9.4, rand_chacha 0.9.0, rand_core 0.6.4, rand_core 0.9.5, rand_xorshift 0.4.0 +---------------------------------------------------------------------------- +Copyrights in the Rand project are retained by their contributors. No +copyright assignment is required to contribute to the Rand project. + +For full authorship information, see the version control history. + +Except as otherwise noted (below and/or in individual files), Rand is +licensed under the Apache License, Version 2.0 or + or the MIT license + or , at your option. + +The Rand project includes code from the Rust project +published under these same licenses. + + +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: rand 0.9.4, rand_chacha 0.9.0, rand_xorshift 0.4.0 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: rand 0.9.4, rand_chacha 0.9.0, rand_core 0.6.4, rand_core 0.9.5, rand_xorshift 0.4.0 +---------------------------------------------------------------------------- +Copyright 2018 Developers of the Rand project +Copyright (c) 2014 The Rust Project Developers + +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 following license (LICENSE-APACHE) applies to: rand_core 0.6.4, rand_core 0.9.5 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + +---------------------------------------------------------------------------- +The following license (COPYING) applies to: rav1d 1.1.0 +---------------------------------------------------------------------------- +Copyright © 2018-2019, VideoLAN and dav1d authors +Copyright © 2023-2024, VideoLAN, dav1d authors, and Internet Security Research Group +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE.md) applies to: raw-cpuid 11.6.0 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Gerd Zellweger + +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 following license (LICENSE) applies to: rcgen 0.13.2 +---------------------------------------------------------------------------- +Copyright (c) 2019-2022 est31 and contributors + +Licensed under MIT or Apache License 2.0, +at your option. + +The full list of contributors can be obtained by looking +at the VCS log (originally, this crate was git versioned, +there you can do "git shortlog -sn" for this task). + +MIT License +----------- + +The MIT License (MIT) + +Copyright (c) 2019-2022 est31 and 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. + + + +Apache License, version 2.0 +--------------------------- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: readme-rustdocifier 0.1.1 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2022 Markus Laire + +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 following license (LICENSE) applies to: redox_syscall 0.5.18 +---------------------------------------------------------------------------- +Copyright (c) 2017 Redox OS Developers + +MIT License + +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 following license (LICENSE) applies to: reed-solomon-simd 3.1.0 +---------------------------------------------------------------------------- +All code from Anders Trier Olesen is under the MIT License (1st license below). +All code from Markus Laire is under MIT License (2nd license below). + +This crate is based on [1] which uses BSD-3-Clause License (3rd license below). + +[1] https://github.com/catid/leopard + +----- + +Copyright (c) 2023 Anders Trier Olesen + +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. + +----- + +Copyright (c) 2022 Markus Laire + +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. + +----- + +Copyright (c) 2017 Christopher A. Taylor. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of Leopard-RS nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: ring 0.17.14 +---------------------------------------------------------------------------- +*ring* uses an "ISC" license, like BoringSSL used to use, for new code +files. See LICENSE-other-bits for the text of that license. + +See LICENSE-BoringSSL for code that was sourced from BoringSSL under the +Apache 2.0 license. Some code that was sourced from BoringSSL under the ISC +license. In each case, the license info is at the top of the file. + +See src/polyfill/once_cell/LICENSE-APACHE and src/polyfill/once_cell/LICENSE-MIT +for the license to code that was sourced from the once_cell project. + + +---------------------------------------------------------------------------- +The following license (LICENSE-BoringSSL) applies to: ring 0.17.14 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +Licenses for support code +------------------------- + +Parts of the TLS test suite are under the Go license. This code is not included +in BoringSSL (i.e. libcrypto and libssl) when compiled, however, so +distributing code linked against BoringSSL does not trigger this license: + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +BoringSSL uses the Chromium test infrastructure to run a continuous build, +trybots etc. The scripts which manage this, and the script for generating build +metadata, are under the Chromium license. Distributing code linked against +BoringSSL does not trigger this license. + +Copyright 2015 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-other-bits) applies to: ring 0.17.14 +---------------------------------------------------------------------------- +Copyright 2015-2025 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT.md) applies to: rpkg-config 0.1.2 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2024 Maia S. R. + +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 following license (LICENSE-ZLIB.md) applies to: rpkg-config 0.1.2, sdl3-src 3.4.10 +---------------------------------------------------------------------------- +zlib License + +(C) 2024 Maia S. R. + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (COPYRIGHT) applies to: rustix 1.1.4 +---------------------------------------------------------------------------- +Short version for non-lawyers: + +`rustix` is triple-licensed under Apache 2.0 with the LLVM Exception, +Apache 2.0, and MIT terms. + + +Longer version: + +Copyrights in the `rustix` project are retained by their contributors. +No copyright assignment is required to contribute to the `rustix` +project. + +Some files include code derived from Rust's `libstd`; see the comments in +the code for details. + +Except as otherwise noted (below and/or in individual files), `rustix` +is licensed under: + + - the Apache License, Version 2.0, with the LLVM Exception + or + + - the Apache License, Version 2.0 + or + , + - or the MIT license + or + , + +at your option. + + +---------------------------------------------------------------------------- +The following license (LICENSE-ISC) applies to: rustls 0.23.41, rustls-native-certs 0.8.4 +---------------------------------------------------------------------------- +ISC License (ISC) +Copyright (c) 2016, Joseph Birr-Pixton + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: rustls 0.23.41, rustls-native-certs 0.8.4 +---------------------------------------------------------------------------- +Copyright (c) 2016 Joseph Birr-Pixton + +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 following license (LICENSE) applies to: rustls-native-certs 0.8.4 +---------------------------------------------------------------------------- +Rustls is distributed under the following three licenses: + +- Apache License version 2.0. +- MIT license. +- ISC license. + +These are included as LICENSE-APACHE, LICENSE-MIT and LICENSE-ISC +respectively. You may use this software under the terms of any +of these licenses, at your option. + + +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: rustls-pki-types 1.14.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2023 Dirkjan Ochtman + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: rustls-pki-types 1.14.1 +---------------------------------------------------------------------------- +Copyright (c) 2023 Dirkjan Ochtman + +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 following license (LICENSE-MIT) applies to: rustls-platform-verifier 0.6.2 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2022 1Password + +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 following license (LICENSE) applies to: rustls-webpki 0.103.13 +---------------------------------------------------------------------------- +Except as otherwise noted, this project is licensed under the following +(ISC-style) terms: + +Copyright 2015 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +The files under third-party/chromium are licensed as described in +third-party/chromium/LICENSE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT.md) applies to: safe_arch 0.7.4 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2023 Daniel "Lokathor" Gee. + +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 (including the next paragraph) 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 following license (LICENSE-ZLIB.md) applies to: safe_arch 0.7.4, wide 0.7.33 +---------------------------------------------------------------------------- +Copyright (c) 2020 Daniel "Lokathor" Gee. + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: same-file 1.0.6, winapi-util 0.1.11 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2017 Andrew Gallant + +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 following license (LICENSE.md) applies to: schannel 0.1.29 +---------------------------------------------------------------------------- +Copyright (c) 2015 steffengy + +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 following license (LICENSE-MIT) applies to: scopeguard 1.2.0 +---------------------------------------------------------------------------- +Copyright (c) 2016-2019 Ulrik Sverdrup "bluss" and scopeguard developers + +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 following license (LICENSE) applies to: sdl3 0.18.4 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2013 Mozilla Foundation + +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 following license (LICENSE.md) applies to: sdl3-image-sys 0.6.4+SDL-image-3.4.4, sdl3-mixer-sys 0.6.3+SDL-mixer-3.2.4, sdl3-ttf-sys 0.6.1+SDL-ttf-3.2.2 +---------------------------------------------------------------------------- +zlib License + +(C) 2025 Maia S Ravn + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (LICENSE.md) applies to: sdl3-sys 0.6.6+SDL-3.4.10 +---------------------------------------------------------------------------- +zlib License + +(C) 2024-2025 Maia S Ravn + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: security-framework 3.7.0, security-framework-sys 2.17.0 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Steven Fackler + +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 following license (LICENSE-MIT) applies to: sha2 0.10.9 +---------------------------------------------------------------------------- +Copyright (c) 2006-2009 Graydon Hoare +Copyright (c) 2009-2013 Mozilla Foundation +Copyright (c) 2016 Artyom Pavlov + +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 following license (LICENSE) applies to: sharded-slab 0.1.7 +---------------------------------------------------------------------------- +Copyright (c) 2019 Eliza Weisman + +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 following license (LICENSE-APACHE) applies to: shlex 1.3.0, shlex 2.0.1 +---------------------------------------------------------------------------- +Copyright 2015 Nicholas Allegra (comex). + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: shlex 1.3.0, shlex 2.0.1 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Nicholas Allegra (comex). + +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 following license (LICENSE-MIT) applies to: signal-hook-registry 1.4.8 +---------------------------------------------------------------------------- +Copyright (c) 2017 tokio-jsonrpc developers + +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 following license (LICENSE.md) applies to: simd-adler32 0.3.9 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) [2021] [Marvin Countryman] + +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 following license (simple-icons.txt) applies to: Simple Icons (vendored, assets/os-icons) +---------------------------------------------------------------------------- +Simple Icons — brand icons (arch, nixos, debian, cachyos, nobara in assets/os-icons/) +are from Simple Icons +(https://simpleicons.org, https://github.com/simple-icons/simple-icons). + +The Simple Icons SVG path data is released under CC0 1.0 Universal (public domain +dedication), https://creativecommons.org/publicdomain/zero/1.0/ — no attribution +required; this notice is provided for provenance. + +Brand icons are trademarks of their respective owners and are used for +identification purposes only; their use does not imply endorsement. See +https://github.com/simple-icons/simple-icons/blob/develop/DISCLAIMER.md. + + +---------------------------------------------------------------------------- +The following license (COPYING) applies to: siphasher 1.0.3 +---------------------------------------------------------------------------- +Copyright 2012-2016 The Rust Project Developers. +Copyright 2016-2026 Frank Denis. + +Licensed under the Apache License, Version 2.0 or the MIT license +, at your +option. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: slab 0.4.12 +---------------------------------------------------------------------------- +Copyright (c) 2019 Carl Lerche + +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 following license (LICENSE-MIT) applies to: smallvec 1.15.2 +---------------------------------------------------------------------------- +Copyright (c) 2018 The Servo Project Developers + +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 following license (LICENSE) applies to: socket-pktinfo 0.4.0 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2025 Pixsper + +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 following license (LICENSE-MIT) applies to: spake2 0.4.0 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2017-2023 Brian Warner + +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 following license (LICENSE) applies to: spin 0.9.8 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2014 Mathijs van de Nes + +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 following license (LICENSE-MIT) applies to: stable_deref_trait 1.2.1 +---------------------------------------------------------------------------- +Copyright (c) 2017 Robert Grosse + +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 following license (LICENSE) applies to: strsim 0.11.1 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Danny Guo +Copyright (c) 2016 Titus Wormer +Copyright (c) 2018 Akash Kurdekar + +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 following license (LICENSE) applies to: strum 0.26.3, strum_macros 0.26.4 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2019 Peter Glotfelty + +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 following license (LICENSE) applies to: subtle 2.6.1 +---------------------------------------------------------------------------- +Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. +Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: synstructure 0.13.2 +---------------------------------------------------------------------------- +Copyright 2016 Nika Layzell + +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 following license (LICENSE-MIT) applies to: tar 0.4.46 +---------------------------------------------------------------------------- +Copyright (c) The tar-rs Project 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 following license (LICENSE-MIT) applies to: tempfile 3.27.0, xattr 1.6.1 +---------------------------------------------------------------------------- +Copyright (c) 2015 Steven Allen + +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 following license (LICENSE-MIT) applies to: time 0.3.51, time-core 0.1.9, time-macros 0.2.30 +---------------------------------------------------------------------------- +Copyright (c) Jacob Pratt et al. + +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 following license (LICENSE-MIT) applies to: tinytemplate 1.2.1 +---------------------------------------------------------------------------- +Copyright (c) 2019 Brook Heisler + +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 following license (LICENSE-MIT.md) applies to: tinyvec 1.11.0 +---------------------------------------------------------------------------- +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 following license (LICENSE-APACHE.md) applies to: tinyvec_macros 0.1.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Tomasz "Soveu" Marx + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT.md) applies to: tinyvec_macros 0.1.1 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2020 Soveu + +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 following license (LICENSE-ZLIB.md) applies to: tinyvec_macros 0.1.1 +---------------------------------------------------------------------------- +zlib License + +(C) 2020 Tomasz "Soveu" Marx + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: to_method 1.1.0 +---------------------------------------------------------------------------- +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: tokio 1.52.3 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) Tokio 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 following license (LICENSE) applies to: tokio-macros 2.7.0 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2019 Yoshua Wuyts +Copyright (c) Tokio 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 following license (LICENSE) applies to: tracing 0.1.44, tracing-attributes 0.1.31, tracing-core 0.1.36, tracing-log 0.2.0, tracing-subscriber 0.3.23 +---------------------------------------------------------------------------- +Copyright (c) 2019 Tokio 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 following license (LICENSE) applies to: typenum 1.20.1 +---------------------------------------------------------------------------- +MIT OR Apache-2.0 + + +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: typenum 1.20.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2014 Paho Lurie-Gregg + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: typenum 1.20.1 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2014 Paho Lurie-Gregg + +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 following license (LICENSE-MIT) applies to: unarray 0.1.4 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) [year] [fullname] + +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 following license (LICENSE-UNICODE) applies to: unicode-ident 1.0.24 +---------------------------------------------------------------------------- +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 1991-2023 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + + +---------------------------------------------------------------------------- +The following license (COPYRIGHT) applies to: unicode-segmentation 1.13.3, unicode-width 0.2.2 +---------------------------------------------------------------------------- +Licensed under the Apache License, Version 2.0 + or the MIT +license , +at your option. All files in the project carrying such +notice may not be copied, modified, or distributed except +according to those terms. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: universal-hash 0.5.1 +---------------------------------------------------------------------------- +Copyright (c) 2019-2020 RustCrypto Developers + +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 following license (LICENSE.txt) applies to: untrusted 0.9.0 +---------------------------------------------------------------------------- +// Copyright 2015-2016 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: ureq 2.12.1 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2019 Martin Algesten + +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 following license (COPYRIGHT) applies to: utf8_iter 1.0.4 +---------------------------------------------------------------------------- +Copyright Mozilla Foundation + +Licensed under the Apache License (Version 2.0), or the MIT license, +(the "Licenses") at your option. You may not use this file except in +compliance with one of the Licenses. You may obtain copies of the +Licenses at: + + https://www.apache.org/licenses/LICENSE-2.0 + https://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software +distributed under the Licenses is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Licenses for the specific language governing permissions and +limitations under the Licenses. + +-- + +Test code is dedicated to the Public Domain when so designated (see +the individual files for PD/CC0-dedicated sections). + +-- + +The implementation for Utf8CharIndices was adapted from the +CharIndices implementation of the Rust standard library at revision +ab32548539ec38a939c1b58599249f3b54130026 +(https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/library/core/src/str/iter.rs). + +Excerpt from https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/COPYRIGHT , +which refers to +https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/LICENSE-APACHE +and +https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/LICENSE-MIT +: + +For full authorship information, see the version control history or +https://thanks.rust-lang.org + +Except as otherwise noted (below and/or in individual files), Rust is +licensed under the Apache License, Version 2.0 or + or the MIT license + or , at your option. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: utf8_iter 1.0.4 +---------------------------------------------------------------------------- +Copyright Mozilla Foundation + +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 following license (LICENSE-MIT) applies to: utf8parse 0.2.2 +---------------------------------------------------------------------------- +Copyright (c) 2016 Joe Wilm + +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 following license (LICENSE-MIT) applies to: vcpkg 0.2.15 +---------------------------------------------------------------------------- +Copyright (c) 2017 Jim McGrath + +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 following license (LICENSE) applies to: version-compare 0.2.1 +---------------------------------------------------------------------------- +Copyright (c) 2017 Tim Visée + +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 following license (LICENSE-MIT) applies to: version_check 0.9.5 +---------------------------------------------------------------------------- +The MIT License (MIT) +Copyright (c) 2017-2018 Sergio Benitez + +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 following license (LICENSE.md) applies to: volk (vendored, crates/pyrowave-sys) +---------------------------------------------------------------------------- +Copyright (c) 2018-2026 Arseny Kapoulkine + +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 following license (LICENSE.md) applies to: Vulkan-Headers (vendored, crates/pyrowave-sys) +---------------------------------------------------------------------------- +Copyright 2015-2023 The Khronos Group Inc. + +Files in this repository fall under one of these licenses: + +- `Apache-2.0` +- `MIT` + +Note: With the exception of `parse_dependency.py` the files using `MIT` license +also fall under `Apache-2.0`. Example: + +``` +SPDX-License-Identifier: Apache-2.0 OR MIT +``` + +Full license text of these licenses is available at: + + * Apache-2.0: https://opensource.org/licenses/Apache-2.0 + * MIT: https://opensource.org/licenses/MIT + + +---------------------------------------------------------------------------- +The following license (LICENSE.txt) applies to: wasapi 0.23.0 +---------------------------------------------------------------------------- +Copyright (c) 2020 Henrik Enquist + +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 following license (LICENSE-APACHE) applies to: web-time 1.1.0 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 dAxpeDDa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: web-time 1.1.0 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2023 dAxpeDDa + +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 following license (LICENSE) applies to: webpki-root-certs 1.0.8, webpki-roots 0.26.11, webpki-roots 1.0.8 +---------------------------------------------------------------------------- +# Community Data License Agreement - Permissive - Version 2.0 + +This is the Community Data License Agreement - Permissive, Version +2.0 (the "agreement"). Data Provider(s) and Data Recipient(s) agree +as follows: + +## 1. Provision of the Data + +1.1. A Data Recipient may use, modify, and share the Data made +available by Data Provider(s) under this agreement if that Data +Recipient follows the terms of this agreement. + +1.2. This agreement does not impose any restriction on a Data +Recipient's use, modification, or sharing of any portions of the +Data that are in the public domain or that may be used, modified, +or shared under any other legal exception or limitation. + +## 2. Conditions for Sharing Data + +2.1. A Data Recipient may share Data, with or without modifications, so +long as the Data Recipient makes available the text of this agreement +with the shared Data. + +## 3. No Restrictions on Results + +3.1. This agreement does not impose any restriction or obligations +with respect to the use, modification, or sharing of Results. + +## 4. No Warranty; Limitation of Liability + +4.1. All Data Recipients receive the Data subject to the following +terms: + +THE DATA IS PROVIDED ON AN "AS IS" BASIS, WITHOUT REPRESENTATIONS, +WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED +INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, +NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +NO DATA PROVIDER SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING +WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE DATA OR RESULTS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +## 5. Definitions + +5.1. "Data" means the material received by a Data Recipient under +this agreement. + +5.2. "Data Provider" means any person who is the source of Data +provided under this agreement and in reliance on a Data Recipient's +agreement to its terms. + +5.3. "Data Recipient" means any person who receives Data directly +or indirectly from a Data Provider and agrees to the terms of this +agreement. + +5.4. "Results" means any outcome obtained by computational analysis +of Data, including for example machine learning models and models' +insights. + + +---------------------------------------------------------------------------- +The following license (license-apache-2.0) applies to: windows 0.62.2, windows-collections 0.3.2, windows-core 0.62.2, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-numerics 0.3.1, windows-reference 0.1.0, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.45.0, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.60.2, windows-sys 0.61.2, windows-targets 0.42.2, windows-targets 0.52.6, windows-targets 0.53.5, windows-threading 0.2.1, windows-time 0.1.0, windows_aarch64_gnullvm 0.42.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_gnullvm 0.53.1, windows_aarch64_msvc 0.42.2, windows_aarch64_msvc 0.52.6, windows_aarch64_msvc 0.53.1, windows_i686_gnu 0.42.2, windows_i686_gnu 0.52.6, windows_i686_gnu 0.53.1, windows_i686_gnullvm 0.52.6, windows_i686_gnullvm 0.53.1, windows_i686_msvc 0.42.2, windows_i686_msvc 0.52.6, windows_i686_msvc 0.53.1, windows_x86_64_gnu 0.42.2, windows_x86_64_gnu 0.52.6, windows_x86_64_gnu 0.53.1, windows_x86_64_gnullvm 0.42.2, windows_x86_64_gnullvm 0.52.6, windows_x86_64_gnullvm 0.53.1, windows_x86_64_msvc 0.42.2, windows_x86_64_msvc 0.52.6, windows_x86_64_msvc 0.53.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (license-mit) applies to: windows 0.62.2, windows-collections 0.3.2, windows-core 0.62.2, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-numerics 0.3.1, windows-reference 0.1.0, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.45.0, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.60.2, windows-sys 0.61.2, windows-targets 0.42.2, windows-targets 0.52.6, windows-targets 0.53.5, windows-threading 0.2.1, windows-time 0.1.0, windows_aarch64_gnullvm 0.42.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_gnullvm 0.53.1, windows_aarch64_msvc 0.42.2, windows_aarch64_msvc 0.52.6, windows_aarch64_msvc 0.53.1, windows_i686_gnu 0.42.2, windows_i686_gnu 0.52.6, windows_i686_gnu 0.53.1, windows_i686_gnullvm 0.52.6, windows_i686_gnullvm 0.53.1, windows_i686_msvc 0.42.2, windows_i686_msvc 0.52.6, windows_i686_msvc 0.53.1, windows_x86_64_gnu 0.42.2, windows_x86_64_gnu 0.52.6, windows_x86_64_gnu 0.53.1, windows_x86_64_gnullvm 0.42.2, windows_x86_64_gnullvm 0.52.6, windows_x86_64_gnullvm 0.53.1, windows_x86_64_msvc 0.42.2, windows_x86_64_msvc 0.52.6, windows_x86_64_msvc 0.53.1 +---------------------------------------------------------------------------- +MIT License + + Copyright (c) Microsoft Corporation. + + 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 following license (LICENSE-MIT) applies to: winnow 0.7.15, winnow 1.0.3 +---------------------------------------------------------------------------- +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 following license (LICENSE) applies to: winreg 0.56.0 +---------------------------------------------------------------------------- +Copyright (c) 2015 Igor Shaula + +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 following license (LICENSE) applies to: winresource 0.1.31 +---------------------------------------------------------------------------- +Copyright 2016 Max Resch + +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 following license (LICENSE-APACHE) applies to: zerocopy 0.7.35, zerocopy 0.8.52, zerocopy-derive 0.7.35, zerocopy-derive 0.8.52 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 The Fuchsia Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-BSD) applies to: zerocopy 0.7.35, zerocopy 0.8.52, zerocopy-derive 0.7.35, zerocopy-derive 0.8.52 +---------------------------------------------------------------------------- +Copyright 2019 The Fuchsia Authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: zerocopy 0.7.35, zerocopy 0.8.52, zerocopy-derive 0.7.35, zerocopy-derive 0.8.52 +---------------------------------------------------------------------------- +Copyright 2023 The Fuchsia Authors + +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 following license (LICENSE-MIT) applies to: zeroize 1.9.0 +---------------------------------------------------------------------------- +Copyright (c) 2018-2026 The RustCrypto Project Developers + +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. + diff --git a/clients/linux/src/ui_settings.rs b/clients/linux/src/ui_settings.rs index 260362b7..ed35fd69 100644 --- a/clients/linux/src/ui_settings.rs +++ b/clients/linux/src/ui_settings.rs @@ -728,7 +728,11 @@ const CODEC_LABELS: &[&str] = &[ "AV1", "PyroWave (wired LAN)", ]; -const DECODERS: &[&str] = &["auto", "vulkan", "vaapi", "software"]; +// Stored decoder-preference values. `native-*` since M10 — the bare "vulkan"/"vaapi" +// named libavcodec's rungs, which are deleted; a store still holding them is migrated on +// read (`pf_client_core::video::migrate_decoder_pref`) and simply matches no entry here +// until the user re-picks. The labels below are unchanged and still true. +const DECODERS: &[&str] = &["auto", "native-vulkan", "native-vaapi", "software"]; /// Touch-input model values (persisted) paired with their display labels below — the /// cross-client set (Android/Apple). Only meaningful on a touchscreen (Deck/tablet). const TOUCH_MODES: &[&str] = &["trackpad", "pointer", "touch"]; @@ -773,16 +777,26 @@ const APP_LICENSE: &str = concat!( "\n\n=============================== Apache-2.0 ===============================\n\n", include_str!("../../../LICENSE-APACHE"), ); -/// Third-party software notices for the linked Rust crates (generated by -/// scripts/gen-third-party-notices.sh; shown as a Legal section in the About dialog). -const THIRD_PARTY_NOTICES: &str = include_str!("../../../THIRD-PARTY-NOTICES.txt"); +/// Third-party software notices for the Rust crates THIS CLIENT links — the shell, the +/// session streamer, the headless CLI and the update helper (generated by +/// scripts/gen-third-party-notices.sh; shown as a Legal section in the About dialog, and +/// shipped as /usr/share/doc/punktfunk-client/THIRD-PARTY-NOTICES.txt by the packages). +/// +/// Deliberately the client-scoped file and not the workspace-wide one at the repo root: +/// that root file is the HOST's, it still carries `ffmpeg-next` and the full FFmpeg licence +/// text — and after M10 this app links no FFmpeg at all, which is exactly what the section +/// below it claims. +const THIRD_PARTY_NOTICES: &str = include_str!("../THIRD-PARTY-NOTICES.txt"); /// The dynamically linked system libraries — not in the crate notices, since they aren't /// crates. Their full texts ship with each project rather than being vendored here. const SYSTEM_LIBRARY_NOTICES: &str = "This application dynamically links system libraries under their own licenses, including \ - FFmpeg (LGPL v2.1+), GTK 4 and libadwaita (LGPL v2.1+), PipeWire (MIT), and SDL 3 (Zlib). \ - Their full license texts are available from each project."; + GTK 4 and libadwaita (LGPL v2.1+), PipeWire (MIT), and SDL 3 (Zlib). \ + Their full license texts are available from each project. Video decoding uses the \ + system's own Vulkan Video and VAAPI drivers (loaded at runtime, never linked), with \ + OpenH264 and rav1d — both BSD-2-Clause, and both in the Rust crate notices — as the \ + CPU fallback; no FFmpeg is linked or bundled."; /// Show the About dialog (app license + the third-party-software Legal section) — reached /// from the primary menu (app.rs `win.about`). @@ -806,7 +820,7 @@ pub fn show_about(parent: &impl IsA) { .license_type(gtk::License::Custom) .license(license.as_str()) .build(); - // The native (FFmpeg/GTK/PipeWire/SDL3) components are dynamically linked under their own + // The native (GTK/PipeWire/SDL3) components are dynamically linked under their own // (LGPL/Zlib/MIT) licenses; the Rust crate notices are the substantive attribution set. about.add_legal_section( "Third-party software (Rust crates)", @@ -1628,7 +1642,11 @@ pub fn show_scoped( mouse_row.set_selected(mouse_i); set_row_subtitle(mouse_row.widget(), MOUSE_MODE_CAPTIONS[mouse_i as usize]); compositor_row.set_selected(index::compositor(s)); - let dec_i = DECODERS.iter().position(|&d| d == s.decoder).unwrap_or(0); + // Migrated for the LOOKUP only (the store is left alone): a pre-M10 settings file + // holds `vulkan`/`vaapi`, which match no entry — the combo would show Automatic and + // a save would silently rewrite the user's hardware preference to `auto`. + let dec_stored = pf_client_core::video::migrate_decoder_pref(&s.decoder); + let dec_i = DECODERS.iter().position(|&d| d == dec_stored).unwrap_or(0); decoder_row.set_selected(dec_i as u32); stats_row.set_selected(index::stats(s)); fullscreen_row.set_active(s.fullscreen_on_stream); diff --git a/clients/session/Cargo.toml b/clients/session/Cargo.toml index 2b2a39d3..f3b7acab 100644 --- a/clients/session/Cargo.toml +++ b/clients/session/Cargo.toml @@ -24,7 +24,6 @@ pyrowave = ["pf-client-core/pyrowave", "pf-presenter/pyrowave"] # (`--no-default-features`) is the ~15 MB-smaller power-user build: same streaming, # stats on stdout only. ui = ["dep:pf-console-ui", "dep:serde_json"] - # Same Linux+Windows gating as the rest of the client stack; elsewhere this is a stub # binary. [target.'cfg(any(target_os = "linux", windows))'.dependencies] diff --git a/clients/session/README.md b/clients/session/README.md index 37ca6a78..3e7555ca 100644 --- a/clients/session/README.md +++ b/clients/session/README.md @@ -49,19 +49,67 @@ path + per-stage latency equation); any tier but Off also emits the stdout mirro `--no-default-features` is the ~5 MB power-user build — same streaming, stats on stdout only, no Skia anywhere in the dependency tree. -Decode follows the Settings preference (auto: Vulkan Video → VAAPI → software on Linux, -Vulkan Video → D3D11VA → software on Windows): FFmpeg's Vulkan Video decoder runs on the -presenter's own device where the stack supports it (every vendor, zero copy); VAAPI -dmabufs import per-plane elsewhere (D3D11VA textures on Windows); software is the -universal fallback. 10-bit Main10 and HDR10 are advertised -(`VIDEO_CAP_10BIT|HDR`): P010 decodes through all three paths, and PQ streams present +Decode follows the Settings preference (auto is vendor-ordered: Vulkan Video → VAAPI → +software on Linux, Vulkan Video → D3D11VA → software on Windows, with VAAPI/D3D11VA first +on Intel — every rung native since M10; see "Decode rungs" below): the Vulkan decoder runs +on the presenter's own device where the stack supports it (every vendor, zero copy); VAAPI +dmabufs import per-plane elsewhere (D3D11VA textures on Windows); software is the universal +fallback. 10-bit Main10 and HDR10 are advertised (`VIDEO_CAP_10BIT|HDR`): P010 decodes +through the Vulkan and VAAPI/D3D11VA paths (the CPU rung is 8-bit by contract and refuses +10-bit rather than mis-scaling it), and PQ streams present on an HDR10/ST.2084 swapchain when the desktop offers one (KDE HDR, gamescope) or tone-map in-shader to SDR when it doesn't (`PUNKTFUNK_TONEMAP_PEAK` tunes the rolloff, default ≈1000 nits). The host still gates the upgrade behind its `PUNKTFUNK_10BIT` policy. -Debug/bisect knobs: `PUNKTFUNK_DECODER=vulkan|vaapi|d3d11va|software`, `PUNKTFUNK_PRESENT_MODE= +## Decode rungs (M10: native only) + +**This binary contains no FFmpeg.** `auto` walks native rungs — pf-vkdecode over Vulkan +Video, then the platform's own (pf-dxvadec on Windows, pf-vaadec on Linux), then the CPU +rung (openh264/rav1d). The libavcodec rungs that used to sit under each of them are +deleted, along with `pf-ffvk` and the `ffmpeg-next` dependency. + +Two of the native rungs have never decoded a frame on real hardware (native VAAPI at all; +native D3D11VA's AV1 leg). They run anyway — with the libavcodec twins gone, the only +thing below them is the CPU, so barring them would cost the session hardware decode +outright rather than move it one rung down. What replaces the safety net is the log: every +session names the rung it landed on with its evidence state, + + decode rung active rung=native-vulkan codec=HEVC hardware_verified=true evidence=... + +…and that line is a **WARNING** when nothing has ever decoded a frame through the +rung/codec pair the session chose. `pf-client-core`'s `video.rs` module docs carry the full +table; read any field report about M10 against it. + +Debug/bisect knobs: `PUNKTFUNK_DECODER=native-vulkan|native-vaapi|native-d3d11va|software` +(a pin skips the vendor order, which is how a lab run reaches a rung `auto` will not pick +on this device; a pinned rung that cannot open still falls through to the standard ladder, +loudly; `native-vaapi` also takes `PUNKTFUNK_VAAPI_DEVICE=/dev/dri/renderDNNN` to choose +the GPU). The pre-M10 spellings `vulkan`/`vaapi`/`d3d11va` named the libavcodec rungs +specifically; they are MIGRATED onto the native rung for the same hardware family, with a +`warn` line saying so — every desktop Settings UI offered those values, so refusing them +would end a session over a dropdown someone picked long ago. +`PUNKTFUNK_PRESENT_MODE= mailbox|fifo|immediate|fifo_relaxed` (default MAILBOX, FIFO where the surface offers no MAILBOX — AMD on Windows), `PUNKTFUNK_VK_DEVICE=` (multi-GPU), and `PUNKTFUNK_HW_FAULT=import` (fault every VAAPI dmabuf import — proves the three-strike demotion to software on healthy hardware). + +`PUNKTFUNK_AU_FAULT=drop|truncate|flip[:period]` deliberately corrupts decoder input on the +native Vulkan lane (default period 60 — one AU a second at 60 fps; inert everywhere else, and +inert entirely if the value doesn't parse). `drop` swallows the AU, so the next one references a +picture that was never decoded — the bitstream planner catches it immediately. `truncate` delivers +a picture whose slice data stops mid-frame and `flip` alters one byte deep in the payload: both +parse perfectly, so only the driver's per-frame decode-status query can see them, and neither is +visible at all on a driver without `queryResultStatusSupport`. Watch the +result on the Detailed stats line's `integrity:` term (`damaged` = concealment the planner caught, +`refused` = AUs the decoder rejected outright, `driver-failed` = the hardware's own verdict, `run` += consecutive frames with no picture, `worst run` = the longest such stretch of the session — the +once-a-second `run` sample misses the bad moment almost every time — and `no driver status` = this +device cannot answer the driver question at all). A session that lands on any other lane says so +in the log rather than faulting silently. + +Note that `PUNKTFUNK_AU_DUMP` records the AU as it arrived from the HOST, while the fault injector +runs later, at the native decoder's own entry. On a faulted run the dump is therefore the clean +bitstream — reconstruct the damaged bytes from the spec if you need them (the injector is pure and +deterministic). diff --git a/clients/session/src/console.rs b/clients/session/src/console.rs index 7bd78a70..24b7298e 100644 --- a/clients/session/src/console.rs +++ b/clients/session/src/console.rs @@ -266,6 +266,11 @@ pub fn run(target: Option<&str>) -> u8 { ActionOutcome::Start(Box::new(params)) } OverlayAction::CancelConnect => ActionOutcome::Handled, // run-loop-side + // Also run-loop-side: the clipboard belongs to SDL, which this callback + // has no handle on. Unreachable in practice — listed so adding an action + // to the enum keeps failing loudly here instead of falling into a + // wildcard that silently drops it. + OverlayAction::CopyText(_) => ActionOutcome::Handled, OverlayAction::Quit => ActionOutcome::Quit, } }); @@ -286,6 +291,21 @@ pub fn run(target: Option<&str>) -> u8 { } } +/// A console row key → its index in the known-hosts store. The key is the pinned +/// fingerprint when there is one, else `addr:port` (see the row builder), and a pinned +/// CARD's key carries the profile id past a NUL — the console strips that before it +/// sends a command, so nothing here has to. +fn index_for_key(known: &trust::KnownHosts, key: &str) -> Option { + known + .hosts + .iter() + .position(|h| !h.fp_hex.is_empty() && h.fp_hex == key) + .or_else(|| { + let (addr, port) = key.rsplit_once(':')?; + known.index_by_addr(addr, port.parse().ok()?) + }) +} + fn host_display_name(name: &str, addr: &str) -> String { if name.trim().is_empty() { addr.to_string() @@ -483,6 +503,48 @@ impl ServiceState { } self.last_probe = Instant::now() - Duration::from_secs(60); // probe it now } + ConsoleCmd::UpdateHost { + key, + name, + addr, + port, + } => { + let mut known = trust::KnownHosts::load(); + let Some(h) = index_for_key(&known, &key).and_then(|i| known.hosts.get_mut(i)) + else { + tracing::warn!(%key, "edit for an unknown host — ignoring"); + return; + }; + // Edited IN PLACE rather than removed and re-added: the fingerprint, the + // learned MAC, the pinned cards and the profile binding all hang off this + // entry, and re-adding would silently unpair a host the user only renamed. + h.name = if name.trim().is_empty() { + addr.clone() + } else { + name + }; + h.addr = addr; + h.port = port; + if let Err(e) = known.save() { + tracing::warn!(error = %format!("{e:#}"), "saving known hosts"); + } + self.last_probe = Instant::now() - Duration::from_secs(60); // the address moved + } + ConsoleCmd::ForgetHost { key } => { + let mut known = trust::KnownHosts::load(); + let Some(i) = index_for_key(&known, &key) else { + tracing::warn!(%key, "forget for an unknown host — ignoring"); + return; + }; + let gone = known.hosts.remove(i); + if let Err(e) = known.save() { + tracing::warn!(error = %format!("{e:#}"), "saving known hosts"); + } + tracing::info!(name = %gone.name, addr = %gone.addr, "host forgotten"); + // It may still be advertising, in which case it comes straight back as a + // DISCOVERED row — unsaved and unpaired, which is the honest state. + self.last_probe = Instant::now() - Duration::from_secs(60); + } ConsoleCmd::Wake { key, then_connect } => { if let Some(c) = self.wake_cancel.take() { c.store(true, Ordering::SeqCst); @@ -534,14 +596,7 @@ impl ServiceState { // end; never touches `profile_id` (the default binding). Idempotent, so // a repeated press inside one refresh window can't double-pin. let mut known = trust::KnownHosts::load(); - let idx = known - .hosts - .iter() - .position(|h| !h.fp_hex.is_empty() && h.fp_hex == key) - .or_else(|| { - let (addr, port) = key.rsplit_once(':')?; - known.index_by_addr(addr, port.parse().ok()?) - }); + let idx = index_for_key(&known, &key); let Some(h) = idx.and_then(|i| known.hosts.get_mut(i)) else { tracing::warn!(%key, "pin toggle for an unknown host — ignoring"); return; diff --git a/clients/session/src/main.rs b/clients/session/src/main.rs index 5abfa882..96272edc 100644 --- a/clients/session/src/main.rs +++ b/clients/session/src/main.rs @@ -346,9 +346,13 @@ mod session_main { bitrate_kbps: settings.bitrate_kbps, audio_channels: settings.audio_channels, preferred_codec: settings.preferred_codec(), + // Nothing excluded on a fresh dial. Only the run loop's codec-fallback retry + // sets this, and it does so on a CLONE of these params — a Settings-level + // "never use HEVC" would be `preferred_codec`, not this. + exclude_codecs: 0, // HDR off = don't advertise 10-bit/HDR at all; the host then never upgrades. // MULTI_SLICE is decoder truth for THIS embedder: every desktop decode stack - // (FFmpeg software, VAAPI, D3D11VA, Vulkan Video) handles AUs carrying several + // (Vulkan Video, D3D11VA, VAAPI, openh264/rav1d) handles AUs carrying several // slice NALs, so the host may keep its multi-slice low-latency default (§7 LN1). // The mobile/TV embedders must NOT copy this blindly — Amlogic MediaCodec wedges // on multi-slice AUs (see `VIDEO_CAP_MULTI_SLICE`), so they advertise per-decoder. @@ -357,12 +361,14 @@ mod session_main { // HEVC, and a real GPU 4:4:4 encode probe, and answers the resolved chroma in the // Welcome BEFORE we build a decoder. Advertised whenever the user asks because // every path can DISPLAY it: the Vulkan presenter samples the 2-plane 4:4:4 pool - // formats (hardware RExt decode where the driver offers it — NVIDIA today) and - // swscale converts anything else for the software rung, with the decoder ladder - // demoting on its own. No capability probe gates the bit — software decode is the - // guaranteed floor — but the cost is VISIBLE, not silent: the Detailed stats - // overlay prints the resolved chroma ("4:4:4→4:2:0" when the host declined) and - // the decode path frames actually took. + // formats (hardware RExt decode where the driver offers it — NVIDIA today), + // with the decoder ladder demoting on its own. No capability probe gates the + // bit — but note (M8) that the software rung below it is 4:2:0 8-bit ONLY and + // refuses anything else rather than mis-scaling it, so on a box whose hardware + // 4:4:4 decode fails the floor is a codec fallback, not a converted picture. + // The cost stays VISIBLE, not silent: the Detailed stats overlay prints the + // resolved chroma ("4:4:4→4:2:0" when the host declined) and the decode path + // frames actually took. video_caps: punktfunk_core::quic::VIDEO_CAP_MULTI_SLICE | if settings.hdr_enabled { punktfunk_core::quic::VIDEO_CAP_10BIT | punktfunk_core::quic::VIDEO_CAP_HDR @@ -488,7 +494,8 @@ mod session_main { /// /// RADV-only knob: ANV/NVIDIA/other drivers ignore `RADV_PERFTEST`, and a box where video /// decode is already the default just no-ops. Append rather than clobber so a user's own - /// `RADV_PERFTEST` survives; `PUNKTFUNK_DECODER=vaapi` still overrides the decoder choice. + /// `RADV_PERFTEST` survives; `PUNKTFUNK_DECODER=native-vaapi` still overrides the decoder + /// choice (the pre-M10 `vaapi` spelling reaches the same rung — it migrates, loudly). #[cfg(target_os = "linux")] fn enable_radv_video_decode() { const TOKEN: &str = "video_decode"; @@ -503,6 +510,65 @@ mod session_main { ); } + /// The driver's own answers about video images, printed with nothing in front of + /// them (`--probe-decode`). + /// + /// Passing the five conjuncts above only says Vulkan Video EXISTS on a device; this + /// says whether the zero-copy pipeline can be BUILT on it — a different question + /// with, on at least one shipping driver, a different answer. Verbatim on purpose: + /// the Intel Arc refusal was twice diagnosed from punktfunk's own error text and + /// twice the diagnosis was wrong, and what broke it open both times was reading what + /// the driver actually said. + fn print_video_formats(a: &pf_presenter::vk::AdapterDecode) { + use pf_presenter::vk::probe::{describe_create_flags, describe_usage}; + for p in &a.formats { + println!(" {} (wants {:?}):", p.profile, p.wanted); + for u in &p.usages { + let answer = match &u.formats { + Err(e) => format!("query failed: {e:?}"), + Ok(entries) if entries.is_empty() => "no formats offered".to_string(), + Ok(entries) => entries + .iter() + .map(|f| { + format!( + "{:?} usage={} create={} {:?} {:?}", + f.format, + describe_usage(f.image_usage), + describe_create_flags(f.image_create_flags), + f.image_type, + f.image_tiling, + ) + }) + .collect::>() + .join("; "), + }; + println!(" {:<24} {answer}", u.label); + // The second opinion, printed only where it differs from the video + // format query. Worded as "also asked" rather than "disagrees" on + // purpose: measured on both vendors this call answers "creatable" for + // combinations the video query rejects (NVIDIA included, for SAMPLED + // alone), so it does not honour the profile list and a difference here + // is NOT the driver contradicting itself. Printed anyway because the + // question gets re-asked by everyone who reads a refusal. + let listed = u + .wanted_entry(p.wanted) + .is_some_and(|f| f.image_usage.contains(u.usage)); + if listed != u.image_format_support.is_ok() { + let second = match &u.image_format_support { + Ok(()) => "creatable".to_string(), + Err(e) => format!("{e:?}"), + }; + println!( + " {:<24} (also asked: \ + vkGetPhysicalDeviceImageFormatProperties2 says {second} — that \ + call does not honour the profile list; not authority)", + "" + ); + } + } + } + } + pub fn run() -> u8 { // Logs to STDERR — stdout is the machine interface (ready/stats/error lines). tracing_subscriber::fmt() @@ -530,6 +596,128 @@ mod session_main { }; } + // `--probe-decode`: per-adapter Vulkan Video decode capability, then exit. Human + // output on purpose — this is a triage tool, not a picker source, which is also + // why it is a separate flag: `--list-adapters` is parsed line-by-line by the + // desktop shells' GPU picker and must keep printing bare names. + if arg_flag("--probe-decode") { + return match pf_presenter::vk::probe_decode() { + Ok(adapters) => { + if adapters.is_empty() { + println!("no Vulkan physical devices"); + } + for (i, a) in adapters.iter().enumerate() { + // The bracketed number is the PUNKTFUNK_VK_DEVICE value, and the + // FIRST listed entry is what + // a default run presents on — the decoder shares that device, so + // on a hybrid box this line is usually the answer. + let kind = if a.discrete { "discrete" } else { "integrated" }; + // `a.index`, NOT the loop position. This list is sorted + // discrete-first for reading, but PUNKTFUNK_VK_DEVICE indexes the + // raw enumeration, which puts the iGPU first on some hybrids — + // printing the loop position would name the other GPU on exactly + // the machines this flag is for. The `i == 0` marker is still the + // loop position, because sorted-first IS what pick_device lands on + // when nothing overrides it. + println!( + "[{}] {} ({kind}){}", + a.index, + a.name, + if i == 0 { " <- default presenter" } else { "" } + ); + println!( + " vulkan video decode: {}", + if a.usable { "YES" } else { "no" } + ); + // Name every bit, and ACCOUNT for the ones we cannot name. The + // 5070 Ti reports 0xF — four bits — while punktfunk decodes three + // codecs, so the first version of this line printed three names + // beside a four-bit mask and looked complete. VP9 (bit 3) is a + // real decode operation this client has no rung for; a codec the + // tool cannot name must not silently vanish from a mask it prints, + // or the reader is left to trust that the words cover the number. + const OPS: [(u32, &str); 4] = [ + (0x1, "H.264"), + (0x2, "H.265"), + (0x4, "AV1"), + (0x8, "VP9 (no punktfunk rung)"), + ]; + let mut codecs: Vec = OPS + .iter() + .filter(|(bit, _)| a.codec_ops & bit != 0) + .map(|(_, n)| (*n).to_string()) + .collect(); + let named: u32 = OPS.iter().map(|(b, _)| b).sum(); + let unknown = a.codec_ops & !named; + if unknown != 0 { + codecs.push(format!("unrecognised bits 0x{unknown:X}")); + } + println!( + " driver decode ops: {}", + if codecs.is_empty() { + format!("none (0x{:X})", a.codec_ops) + } else { + format!("{} (0x{:X})", codecs.join(", "), a.codec_ops) + } + ); + if !a.usable { + // Say which conjunct failed. "no" with no reason is the thing + // this whole flag exists to stop. + let mut why: Vec = Vec::new(); + if !a.api_1_3 { + why.push("device is not Vulkan 1.3".into()); + } + if !a.features_ok { + why.push( + "missing samplerYcbcrConversion / timelineSemaphore / \ + synchronization2" + .into(), + ); + } + if a.decode_family.is_none() { + why.push("no queue family advertises VIDEO_DECODE".into()); + } + if !a.base_missing.is_empty() { + why.push(format!("missing {}", a.base_missing.join(", "))); + } + if a.codec_exts.is_empty() { + why.push("no VK_KHR_video_decode_{h264,h265,av1} extension".into()); + } + println!(" why not: {}", why.join("; ")); + } else { + println!(" extensions: {}", a.codec_exts.join(", ")); + } + print_video_formats(a); + } + if adapters.len() > 1 { + // The single most common misreading of this output: seeing a + // capable GPU listed and concluding the decoder will use it. + // Vulkan Video decodes on the PRESENTER's device, and the decoder + // preference does not move the presenter. + println!(); + println!( + "Vulkan Video decodes on the presenter's device. PUNKTFUNK_DECODER \ + picks the rung," + ); + println!( + "not the GPU — move the presenter with PUNKTFUNK_VK_DEVICE= or" + ); + println!( + "PUNKTFUNK_VK_ADAPTER=, which is the safer knob \ + where two" + ); + println!("adapters share a name."); + } + 0 + } + Err(e) => { + eprintln!("probe-decode: {e:#}"); + EXIT_PRESENTER_FAILED + } + }; + } + // `--list-audio`: the PipeWire endpoints the settings pickers offer, as // `sink|sourcenode.namedescription` lines — a debug window into the // same enumeration the GTK shell probes. diff --git a/clients/windows/Cargo.toml b/clients/windows/Cargo.toml index fbafe1d3..a3d1ad77 100644 --- a/clients/windows/Cargo.toml +++ b/clients/windows/Cargo.toml @@ -78,11 +78,6 @@ windows = { git = "https://github.com/microsoft/windows-rs", rev = "acb5a1a74410 "winuser", ] } -# FFmpeg — used only to enumerate which codecs this client can decode (probe::decodable_codecs), -# advertised to the host on the speed-test connect. Same pin as the host/Linux client. (Real -# decode + present live in the spawned punktfunk-session binary.) -ffmpeg-next = "8" - # Gamepad enumeration + pin persistence for Settings runs on pf-client-core's shared SDL service # (see the `gamepad` field in app/); the spawned punktfunk-session does the actual forwarding. SDL3 # itself (built from source via the bundled CMake on Windows) is pulled transitively by diff --git a/clients/windows/README.md b/clients/windows/README.md index 5a56d11b..7b897ebe 100644 --- a/clients/windows/README.md +++ b/clients/windows/README.md @@ -2,7 +2,7 @@ The native **Windows** app for streaming a punktfunk host to your PC. A modern WinUI 3 app that discovers hosts on your network, pairs with a PIN, and streams at your display's own resolution and -refresh rate — with a hardware-accelerated D3D11 video path and HDR. +refresh rate — with hardware-accelerated video decode and HDR. It's **pure Rust**: the UI is WinUI 3 driven through [windows-reactor](https://github.com/microsoft/windows-rs) (a declarative, React-like framework), and it links the shared **`punktfunk-core`** directly to speak @@ -10,9 +10,11 @@ the fast **`punktfunk/1`** protocol. ## Features -- **Hardware decode, GPU present** — FFmpeg HEVC with a **D3D11VA zero-copy path** (decoder and - presenter share one D3D11 device; NV12/P010 textures sampled straight into a `SwapChainPanel` - composition swapchain), with a robust software-decode fallback. +- **Hardware decode, GPU present** — Punktfunk's own decoders, no FFmpeg anywhere in the client: + **Vulkan Video** (`pf-vkdecode`) leads on NVIDIA and AMD, **D3D11VA** (`pf-dxvadec` driving + `ID3D11VideoDecoder`) leads on Intel, whichever isn't first is the fallback, and an + OpenH264/rav1d CPU rung is last. Either hardware rung hands its surface to the Vulkan presenter + without a CPU copy. - **HDR10** — advertise 10-bit/HDR, detect PQ in-band, and flip the swapchain to `R10G10B10A2` + ST.2084 with HDR10 metadata. - **Your display's native mode** — the host builds a virtual display at exactly your WxH@Hz. @@ -42,9 +44,9 @@ A stock [Moonlight](https://moonlight-stream.org/) client also works over GameSt ## Build from source Windows-only (the crate builds as a stub on other platforms so the workspace stays green). You need -the MSVC toolchain, an `FFMPEG_DIR` FFmpeg tree, and CMake (SDL3 builds from source). The Windows -App SDK runtime bootstrap is staged next to the exe by `windows-reactor-setup` from this crate's -own `build.rs` — no extra environment needed. +the MSVC toolchain and CMake (SDL3 builds from source) — nothing else: decode is native since M10, +so there is no `FFMPEG_DIR` to point anywhere, and the Windows App SDK runtime bootstrap is staged +next to the exe by `windows-reactor-setup` from this crate's own `build.rs`. ```sh cargo build -p punktfunk-client-windows --target x86_64-pc-windows-msvc diff --git a/clients/windows/THIRD-PARTY-NOTICES.txt b/clients/windows/THIRD-PARTY-NOTICES.txt new file mode 100644 index 00000000..3f7c7a5e --- /dev/null +++ b/clients/windows/THIRD-PARTY-NOTICES.txt @@ -0,0 +1,12213 @@ +THIRD-PARTY SOFTWARE NOTICES +============================================================================ + +punktfunk (https://git.unom.io/unom/punktfunk) is licensed under MIT OR Apache-2.0. +The binaries it ships statically/dynamically link the third-party Rust crates listed +below. Each is distributed under its own permissive license; the full license texts +follow the manifest. This file is generated by scripts/gen-third-party-notices.py +(or `cargo about`, see about.toml) — do not edit by hand. + +Scope: the Rust crates linked by punktfunk-client-windows,punktfunk-client-session,punktfunk-cli — not the whole punktfunk workspace. + +Total third-party crates: 421 + +---------------------------------------------------------------------------- +VENDORED THIRD-PARTY SOURCE (inside first-party crates) +---------------------------------------------------------------------------- + pyrowave (vendored, crates/pyrowave-sys) — https://github.com/Themaister/pyrowave + Granite subset (vendored, crates/pyrowave-sys) — https://github.com/Themaister/Granite + volk (vendored, crates/pyrowave-sys) — https://github.com/zeux/volk + Vulkan-Headers (vendored, crates/pyrowave-sys) — https://github.com/KhronosGroup/Vulkan-Headers + Font Awesome Free brand icons (vendored, assets/os-icons) — https://fontawesome.com + Simple Icons (vendored, assets/os-icons) — https://simpleicons.org + Bazzite logo (vendored, assets/os-icons) — https://github.com/ublue-os/bazzite + +---------------------------------------------------------------------------- +MANIFEST (crate version — SPDX license — source) +---------------------------------------------------------------------------- + adler2 2.0.1 — 0BSD OR MIT OR Apache-2.0 — https://github.com/oyvindln/adler2 + aead 0.5.2 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits + aes 0.8.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/block-ciphers + aes-gcm 0.10.3 — Apache-2.0 OR MIT — https://github.com/RustCrypto/AEADs + aho-corasick 1.1.4 — Unlicense OR MIT — https://github.com/BurntSushi/aho-corasick + anes 0.1.6 — MIT OR Apache-2.0 — https://github.com/zrzka/anes-rs + annotate-snippets 0.11.5 — MIT OR Apache-2.0 — https://github.com/rust-lang/annotate-snippets-rs + anstream 1.0.0 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git + anstyle 1.0.14 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git + anstyle-parse 1.0.0 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git + anstyle-query 1.1.5 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git + anstyle-wincon 3.0.11 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git + anyhow 1.0.103 — MIT OR Apache-2.0 — https://github.com/dtolnay/anyhow + ash 0.38.0+1.3.281 — MIT OR Apache-2.0 — https://github.com/ash-rs/ash + assert_matches 1.5.0 — MIT/Apache-2.0 — https://github.com/murarth/assert_matches + async-channel 2.5.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/async-channel + atomig 0.4.3 — MIT/Apache-2.0 — https://github.com/LukasKalbertodt/atomig/ + atomig-macro 0.4.0 — MIT/Apache-2.0 — https://github.com/LukasKalbertodt/atomig/ + audiopus_sys 0.2.2 — ISC — https://github.com/lakelezz/audiopus_sys.git + autocfg 1.5.1 — Apache-2.0 OR MIT — https://github.com/cuviper/autocfg + base64 0.22.1 — MIT OR Apache-2.0 — https://github.com/marshallpierce/rust-base64 + bindgen 0.72.1 — BSD-3-Clause — https://github.com/rust-lang/rust-bindgen + bit-set 0.8.0 — Apache-2.0 OR MIT — https://github.com/contain-rs/bit-set + bit-vec 0.8.0 — Apache-2.0 OR MIT — https://github.com/contain-rs/bit-vec + bitflags 1.3.2 — MIT/Apache-2.0 — https://github.com/bitflags/bitflags + bitflags 2.13.0 — MIT OR Apache-2.0 — https://github.com/bitflags/bitflags + block-buffer 0.10.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils + block-padding 0.3.3 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils + bumpalo 3.20.3 — MIT OR Apache-2.0 — https://github.com/fitzgen/bumpalo + bytemuck 1.25.0 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/bytemuck + bytemuck_derive 1.10.2 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/bytemuck + byteorder 1.5.0 — Unlicense OR MIT — https://github.com/BurntSushi/byteorder + bytes 1.12.0 — MIT — https://github.com/tokio-rs/bytes + cast 0.3.0 — MIT OR Apache-2.0 — https://github.com/japaric/cast.rs + cbindgen 0.29.4 — MPL-2.0 — https://github.com/mozilla/cbindgen + cc 1.2.65 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs + cesu8 1.1.0 — Apache-2.0/MIT — https://github.com/emk/cesu8-rs + cexpr 0.6.0 — Apache-2.0/MIT — https://github.com/jethrogb/rust-cexpr + cfg-expr 0.20.8 — MIT OR Apache-2.0 — https://github.com/EmbarkStudios/cfg-expr + cfg-if 1.0.4 — MIT OR Apache-2.0 — https://github.com/rust-lang/cfg-if + cfg_aliases 0.2.1 — MIT — https://github.com/katharostech/cfg_aliases + chacha20 0.9.1 — Apache-2.0 OR MIT — https://github.com/RustCrypto/stream-ciphers + chacha20poly1305 0.10.1 — Apache-2.0 OR MIT — https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305 + ciborium 0.2.2 — Apache-2.0 — https://github.com/enarx/ciborium + ciborium-io 0.2.2 — Apache-2.0 — https://github.com/enarx/ciborium + ciborium-ll 0.2.2 — Apache-2.0 — https://github.com/enarx/ciborium + cipher 0.4.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits + clang-sys 1.8.1 — Apache-2.0 — https://github.com/KyleMayes/clang-sys + clap 4.6.1 — MIT OR Apache-2.0 — https://github.com/clap-rs/clap + clap_builder 4.6.0 — MIT OR Apache-2.0 — https://github.com/clap-rs/clap + clap_lex 1.1.0 — MIT OR Apache-2.0 — https://github.com/clap-rs/clap + cmake 0.1.58 — MIT OR Apache-2.0 — https://github.com/rust-lang/cmake-rs + colorchoice 1.0.5 — MIT OR Apache-2.0 — https://github.com/rust-cli/anstyle.git + combine 4.6.7 — MIT — https://github.com/Marwes/combine + concurrent-queue 2.5.0 — Apache-2.0 OR MIT — https://github.com/smol-rs/concurrent-queue + const-oid 0.9.6 — Apache-2.0 OR MIT — https://github.com/RustCrypto/formats/tree/master/const-oid + convert_case 0.8.0 — MIT — https://github.com/rutrum/convert-case + cookie-factory 0.3.3 — MIT — https://github.com/rust-bakery/cookie-factory + core-foundation 0.10.1 — MIT OR Apache-2.0 — https://github.com/servo/core-foundation-rs + core-foundation-sys 0.8.7 — MIT OR Apache-2.0 — https://github.com/servo/core-foundation-rs + cpufeatures 0.2.17 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils + crc32fast 1.5.0 — MIT OR Apache-2.0 — https://github.com/srijs/rust-crc32fast + criterion 0.5.1 — Apache-2.0 OR MIT — https://github.com/bheisler/criterion.rs + criterion-plot 0.5.0 — MIT/Apache-2.0 — https://github.com/bheisler/criterion.rs + crossbeam-deque 0.8.6 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam + crossbeam-epoch 0.9.20 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam + crossbeam-utils 0.8.21 — MIT OR Apache-2.0 — https://github.com/crossbeam-rs/crossbeam + crunchy 0.2.4 — MIT — https://github.com/eira-fransham/crunchy + crypto-common 0.1.7 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits + ctr 0.9.2 — MIT OR Apache-2.0 — https://github.com/RustCrypto/block-modes + curve25519-dalek 4.1.3 — BSD-3-Clause — https://github.com/dalek-cryptography/curve25519-dalek/tree/main/curve25519-dalek + curve25519-dalek-derive 0.1.1 — MIT/Apache-2.0 — https://github.com/dalek-cryptography/curve25519-dalek + defmt 1.1.1 — MIT OR Apache-2.0 — https://github.com/knurling-rs/defmt + defmt-macros 1.1.1 — MIT OR Apache-2.0 — https://github.com/knurling-rs/defmt + defmt-parser 1.0.0 — MIT OR Apache-2.0 — https://github.com/knurling-rs/defmt + deranged 0.5.8 — MIT OR Apache-2.0 — https://github.com/jhpratt/deranged + digest 0.10.7 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits + displaydoc 0.2.6 — MIT OR Apache-2.0 — https://github.com/yaahc/displaydoc + either 1.16.0 — MIT OR Apache-2.0 — https://github.com/rayon-rs/either + env_filter 2.0.0 — MIT OR Apache-2.0 — https://github.com/rust-cli/env_logger + env_logger 0.11.11 — MIT OR Apache-2.0 — https://github.com/rust-cli/env_logger + equivalent 1.0.2 — Apache-2.0 OR MIT — https://github.com/indexmap-rs/equivalent + errno 0.3.14 — MIT OR Apache-2.0 — https://github.com/lambda-fairy/rust-errno + event-listener 5.4.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/event-listener + event-listener-strategy 0.5.4 — Apache-2.0 OR MIT — https://github.com/smol-rs/event-listener-strategy + fastbloom 0.14.1 — MIT OR Apache-2.0 — https://github.com/tomtomwombat/fastbloom/ + fastrand 2.4.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/fastrand + fiat-crypto 0.2.9 — MIT OR Apache-2.0 OR BSD-1-Clause — https://github.com/mit-plv/fiat-crypto + filetime 0.2.29 — MIT/Apache-2.0 — https://github.com/alexcrichton/filetime + find-msvc-tools 0.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs + fixedbitset 0.5.7 — MIT OR Apache-2.0 — https://github.com/petgraph/fixedbitset + flate2 1.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/flate2-rs + flume 0.12.0 — Apache-2.0/MIT — https://github.com/zesterer/flume + fnv 1.0.7 — Apache-2.0 / MIT — https://github.com/servo/rust-fnv + foldhash 0.2.0 — Zlib — https://github.com/orlp/foldhash + form_urlencoded 1.2.2 — MIT OR Apache-2.0 — https://github.com/servo/rust-url + futures-channel 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + futures-core 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + futures-io 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + futures-macro 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + futures-sink 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + futures-task 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + futures-util 0.3.32 — MIT OR Apache-2.0 — https://github.com/rust-lang/futures-rs + generic-array 0.14.7 — MIT — https://github.com/fizyk20/generic-array.git + getrandom 0.2.17 — MIT OR Apache-2.0 — https://github.com/rust-random/getrandom + getrandom 0.3.4 — MIT OR Apache-2.0 — https://github.com/rust-random/getrandom + getrandom 0.4.3 — MIT OR Apache-2.0 — https://github.com/rust-random/getrandom + ghash 0.5.1 — Apache-2.0 OR MIT — https://github.com/RustCrypto/universal-hashes + glob 0.3.3 — MIT OR Apache-2.0 — https://github.com/rust-lang/glob + half 2.7.1 — MIT OR Apache-2.0 — https://github.com/VoidStarKat/half-rs + hashbrown 0.17.1 — MIT OR Apache-2.0 — https://github.com/rust-lang/hashbrown + heck 0.5.0 — MIT OR Apache-2.0 — https://github.com/withoutboats/heck + hermit-abi 0.5.2 — MIT OR Apache-2.0 — https://github.com/hermit-os/hermit-rs + hkdf 0.12.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/KDFs/ + hmac 0.12.1 — MIT OR Apache-2.0 — https://github.com/RustCrypto/MACs + icu_collections 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x + icu_locale_core 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x + icu_normalizer 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x + icu_normalizer_data 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x + icu_properties 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x + icu_properties_data 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x + icu_provider 2.2.0 — Unicode-3.0 — https://github.com/unicode-org/icu4x + idna 1.1.0 — MIT OR Apache-2.0 — https://github.com/servo/rust-url/ + idna_adapter 1.2.2 — Apache-2.0 OR MIT — https://github.com/hsivonen/idna_adapter + if-addrs 0.13.4 — MIT OR BSD-3-Clause — https://github.com/messense/if-addrs + if-addrs 0.15.0 — MIT OR BSD-3-Clause — https://github.com/messense/if-addrs + indexmap 2.14.0 — Apache-2.0 OR MIT — https://github.com/indexmap-rs/indexmap + inout 0.1.4 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils + is-terminal 0.4.17 — MIT — https://github.com/sunfishcode/is-terminal + is_terminal_polyfill 1.70.2 — MIT OR Apache-2.0 — https://github.com/polyfill-rs/is_terminal_polyfill + itertools 0.10.5 — MIT/Apache-2.0 — https://github.com/rust-itertools/itertools + itertools 0.13.0 — MIT OR Apache-2.0 — https://github.com/rust-itertools/itertools + itoa 1.0.18 — MIT OR Apache-2.0 — https://github.com/dtolnay/itoa + jiff 0.2.35 — Unlicense OR MIT — https://github.com/BurntSushi/jiff + jiff-core 0.1.0 — Unlicense OR MIT — https://github.com/BurntSushi/jiff + jiff-static 0.2.35 — Unlicense OR MIT — https://github.com/BurntSushi/jiff + jni 0.21.1 — MIT/Apache-2.0 — https://github.com/jni-rs/jni-rs + jni-sys 0.3.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys + jni-sys 0.4.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys + jni-sys-macros 0.4.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys + jobserver 0.1.34 — MIT OR Apache-2.0 — https://github.com/rust-lang/jobserver-rs + js-sys 0.3.103 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/js-sys + lazy_static 1.5.0 — MIT OR Apache-2.0 — https://github.com/rust-lang-nursery/lazy-static.rs + libc 0.2.186 — MIT OR Apache-2.0 — https://github.com/rust-lang/libc + libloading 0.8.9 — ISC — https://github.com/nagisa/rust_libloading/ + libm 0.2.16 — MIT — https://github.com/rust-lang/compiler-builtins + libspa 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs + libspa-sys 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs + linux-raw-sys 0.12.1 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/sunfishcode/linux-raw-sys + litemap 0.8.2 — Unicode-3.0 — https://github.com/unicode-org/icu4x + lock_api 0.4.14 — MIT OR Apache-2.0 — https://github.com/Amanieu/parking_lot + log 0.4.33 — MIT OR Apache-2.0 — https://github.com/rust-lang/log + lru-slab 0.1.2 — MIT OR Apache-2.0 OR Zlib — https://github.com/Ralith/lru-slab + matchers 0.2.0 — MIT — https://github.com/hawkw/matchers + mdns-sd 0.20.1 — Apache-2.0 OR MIT — https://github.com/keepsimple1/mdns-sd + memchr 2.8.2 — Unlicense OR MIT — https://github.com/BurntSushi/memchr + minimal-lexical 0.2.1 — MIT/Apache-2.0 — https://github.com/Alexhuszagh/minimal-lexical + miniz_oxide 0.8.9 — MIT OR Zlib OR Apache-2.0 — https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide + mio 1.2.1 — MIT — https://github.com/tokio-rs/mio + nasm-rs 0.3.2 — MIT OR Apache-2.0 — https://github.com/medek/nasm-rs + nix 0.30.1 — MIT — https://github.com/nix-rust/nix + nom 7.1.3 — MIT — https://github.com/Geal/nom + nom 8.0.0 — MIT — https://github.com/rust-bakery/nom + nu-ansi-term 0.50.3 — MIT — https://github.com/nushell/nu-ansi-term + num-conv 0.2.2 — MIT OR Apache-2.0 — https://github.com/jhpratt/num-conv + num-integer 0.1.46 — MIT OR Apache-2.0 — https://github.com/rust-num/num-integer + num-traits 0.2.19 — MIT OR Apache-2.0 — https://github.com/rust-num/num-traits + once_cell 1.21.4 — MIT OR Apache-2.0 — https://github.com/matklad/once_cell + once_cell_polyfill 1.70.2 — MIT OR Apache-2.0 — https://github.com/polyfill-rs/once_cell_polyfill + oorandom 11.1.5 — MIT — https://hg.sr.ht/~icefox/oorandom + opaque-debug 0.3.1 — MIT OR Apache-2.0 — https://github.com/RustCrypto/utils + openh264 0.9.3 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs + openh264-sys2 0.9.6 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs + openssl-probe 0.2.1 — MIT OR Apache-2.0 — https://github.com/rustls/openssl-probe + opus 0.3.1 — MIT/Apache-2.0 — https://github.com/SpaceManiac/opus-rs + parking 2.2.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/parking + parking_lot 0.12.5 — MIT OR Apache-2.0 — https://github.com/Amanieu/parking_lot + parking_lot_core 0.9.12 — MIT OR Apache-2.0 — https://github.com/Amanieu/parking_lot + paste 1.0.15 — MIT OR Apache-2.0 — https://github.com/dtolnay/paste + pem 3.0.6 — MIT — https://github.com/jcreekmore/pem-rs.git + percent-encoding 2.3.2 — MIT OR Apache-2.0 — https://github.com/servo/rust-url/ + pin-project-lite 0.2.17 — Apache-2.0 OR MIT — https://github.com/taiki-e/pin-project-lite + pipewire 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs + pipewire-sys 0.9.2 — MIT — https://gitlab.freedesktop.org/pipewire/pipewire-rs + pkg-config 0.3.33 — MIT OR Apache-2.0 — https://github.com/rust-lang/pkg-config-rs + poly1305 0.8.0 — Apache-2.0 OR MIT — https://github.com/RustCrypto/universal-hashes + polyval 0.6.2 — Apache-2.0 OR MIT — https://github.com/RustCrypto/universal-hashes + portable-atomic 1.14.0 — Apache-2.0 OR MIT — https://github.com/taiki-e/portable-atomic + portable-atomic-util 0.2.7 — Apache-2.0 OR MIT — https://github.com/taiki-e/portable-atomic-util + potential_utf 0.1.5 — Unicode-3.0 — https://github.com/unicode-org/icu4x + powerfmt 0.2.0 — MIT OR Apache-2.0 — https://github.com/jhpratt/powerfmt + ppv-lite86 0.2.21 — MIT OR Apache-2.0 — https://github.com/cryptocorrosion/cryptocorrosion + prettyplease 0.2.37 — MIT OR Apache-2.0 — https://github.com/dtolnay/prettyplease + proc-macro2 1.0.106 — MIT OR Apache-2.0 — https://github.com/dtolnay/proc-macro2 + proptest 1.11.0 — MIT OR Apache-2.0 — https://github.com/proptest-rs/proptest + quick-error 1.2.3 — MIT/Apache-2.0 — http://github.com/tailhook/quick-error + quinn 0.11.11 — MIT OR Apache-2.0 — https://github.com/quinn-rs/quinn + quinn-proto 0.11.15 — MIT OR Apache-2.0 — https://github.com/quinn-rs/quinn + quinn-udp 0.5.14 — MIT OR Apache-2.0 — https://github.com/quinn-rs/quinn + quote 1.0.46 — MIT OR Apache-2.0 — https://github.com/dtolnay/quote + r-efi 5.3.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi + r-efi 6.0.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi + rand 0.9.4 — MIT OR Apache-2.0 — https://github.com/rust-random/rand + rand_chacha 0.9.0 — MIT OR Apache-2.0 — https://github.com/rust-random/rand + rand_core 0.6.4 — MIT OR Apache-2.0 — https://github.com/rust-random/rand + rand_core 0.9.5 — MIT OR Apache-2.0 — https://github.com/rust-random/rand + rand_xorshift 0.4.0 — MIT OR Apache-2.0 — https://github.com/rust-random/rngs + rav1d 1.1.0 — BSD-2-Clause — https://github.com/memorysafety/rav1d + raw-cpuid 11.6.0 — MIT — https://github.com/gz/rust-cpuid + rayon 1.12.0 — MIT OR Apache-2.0 — https://github.com/rayon-rs/rayon + rayon-core 1.13.0 — MIT OR Apache-2.0 — https://github.com/rayon-rs/rayon + rcgen 0.13.2 — MIT OR Apache-2.0 — https://github.com/rustls/rcgen + readme-rustdocifier 0.1.1 — MIT — https://github.com/malaire/readme-rustdocifier + redox_syscall 0.5.18 — MIT — https://gitlab.redox-os.org/redox-os/syscall + reed-solomon-simd 3.1.0 — MIT AND BSD-3-Clause — https://github.com/AndersTrier/reed-solomon-simd + regex 1.12.4 — MIT OR Apache-2.0 — https://github.com/rust-lang/regex + regex-automata 0.4.14 — MIT OR Apache-2.0 — https://github.com/rust-lang/regex + regex-syntax 0.8.11 — MIT OR Apache-2.0 — https://github.com/rust-lang/regex + ring 0.17.14 — Apache-2.0 AND ISC — https://github.com/briansmith/ring + rpkg-config 0.1.2 — Zlib OR MIT OR Apache-2.0 — https://github.com/maia-s/rpkg-config-rs + rustc-hash 2.1.2 — Apache-2.0 OR MIT — https://github.com/rust-lang/rustc-hash + rustc_version 0.4.1 — MIT OR Apache-2.0 — https://github.com/djc/rustc-version-rs + rustix 1.1.4 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/rustix + rustls 0.23.41 — Apache-2.0 OR ISC OR MIT — https://github.com/rustls/rustls + rustls-native-certs 0.8.4 — Apache-2.0 OR ISC OR MIT — https://github.com/rustls/rustls-native-certs + rustls-pki-types 1.14.1 — MIT OR Apache-2.0 — https://github.com/rustls/pki-types + rustls-platform-verifier 0.6.2 — MIT OR Apache-2.0 — https://github.com/rustls/rustls-platform-verifier + rustls-platform-verifier-android 0.1.1 — MIT OR Apache-2.0 — https://github.com/rustls/rustls-platform-verifier + rustls-webpki 0.103.13 — ISC — https://github.com/rustls/webpki + rustversion 1.0.22 — MIT OR Apache-2.0 — https://github.com/dtolnay/rustversion + rusty-fork 0.3.1 — MIT/Apache-2.0 — https://github.com/altsysrq/rusty-fork + safe_arch 0.7.4 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/safe_arch + same-file 1.0.6 — Unlicense/MIT — https://github.com/BurntSushi/same-file + schannel 0.1.29 — MIT — https://github.com/steffengy/schannel-rs + scopeguard 1.2.0 — MIT OR Apache-2.0 — https://github.com/bluss/scopeguard + sdl3 0.18.4 — MIT — https://github.com/vhspace/sdl3-rs + sdl3-image-src 3.4.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-image-sys 0.6.4+SDL-image-3.4.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-mixer-src 3.2.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-mixer-sys 0.6.3+SDL-mixer-3.2.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-src 3.4.10 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-sys 0.6.6+SDL-3.4.10 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-ttf-src 3.2.2 — Zlib — https://github.com/maia-s/sdl3-sys-rs + sdl3-ttf-sys 0.6.1+SDL-ttf-3.2.2 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + security-framework 3.7.0 — MIT OR Apache-2.0 — https://github.com/kornelski/rust-security-framework + security-framework-sys 2.17.0 — MIT OR Apache-2.0 — https://github.com/kornelski/rust-security-framework + semver 1.0.28 — MIT OR Apache-2.0 — https://github.com/dtolnay/semver + serde 1.0.228 — MIT OR Apache-2.0 — https://github.com/serde-rs/serde + serde_core 1.0.228 — MIT OR Apache-2.0 — https://github.com/serde-rs/serde + serde_derive 1.0.228 — MIT OR Apache-2.0 — https://github.com/serde-rs/serde + serde_json 1.0.150 — MIT OR Apache-2.0 — https://github.com/serde-rs/json + serde_spanned 0.6.9 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + serde_spanned 1.1.1 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + sha2 0.10.9 — MIT OR Apache-2.0 — https://github.com/RustCrypto/hashes + sharded-slab 0.1.7 — MIT — https://github.com/hawkw/sharded-slab + shlex 1.3.0 — MIT OR Apache-2.0 — https://github.com/comex/rust-shlex + shlex 2.0.1 — MIT OR Apache-2.0 — https://github.com/comex/rust-shlex + signal-hook-registry 1.4.8 — MIT OR Apache-2.0 — https://github.com/vorner/signal-hook + simd-adler32 0.3.9 — MIT — https://github.com/mcountryman/simd-adler32 + siphasher 1.0.3 — MIT/Apache-2.0 — https://github.com/jedisct1/rust-siphash + skia-bindings 0.87.0 — MIT — https://github.com/rust-skia/rust-skia + skia-safe 0.87.0 — MIT — https://github.com/rust-skia/rust-skia + slab 0.4.12 — MIT — https://github.com/tokio-rs/slab + smallvec 1.15.2 — MIT OR Apache-2.0 — https://github.com/servo/rust-smallvec + socket-pktinfo 0.4.0 — MIT — https://github.com/pixsper/socket-pktinfo + socket2 0.6.4 — MIT OR Apache-2.0 — https://github.com/rust-lang/socket2 + spake2 0.4.0 — MIT OR Apache-2.0 — https://github.com/RustCrypto/PAKEs/tree/master/spake2 + spin 0.9.8 — MIT — https://github.com/mvdnes/spin-rs.git + stable_deref_trait 1.2.1 — MIT OR Apache-2.0 — https://github.com/storyyeller/stable_deref_trait + strsim 0.11.1 — MIT — https://github.com/rapidfuzz/strsim-rs + strum 0.26.3 — MIT — https://github.com/Peternator7/strum + strum_macros 0.26.4 — MIT — https://github.com/Peternator7/strum + subtle 2.6.1 — BSD-3-Clause — https://github.com/dalek-cryptography/subtle + syn 2.0.118 — MIT OR Apache-2.0 — https://github.com/dtolnay/syn + synstructure 0.13.2 — MIT — https://github.com/mystor/synstructure + system-deps 7.0.8 — MIT OR Apache-2.0 — https://github.com/gdesmott/system-deps + tar 0.4.46 — MIT OR Apache-2.0 — https://github.com/composefs/tar-rs + target-lexicon 0.13.5 — Apache-2.0 WITH LLVM-exception — https://github.com/bytecodealliance/target-lexicon + tempfile 3.27.0 — MIT OR Apache-2.0 — https://github.com/Stebalien/tempfile + test_reactor 0.0.0 — UNKNOWN + thiserror 1.0.69 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror + thiserror 2.0.18 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror + thiserror-impl 1.0.69 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror + thiserror-impl 2.0.18 — MIT OR Apache-2.0 — https://github.com/dtolnay/thiserror + thread_local 1.1.9 — MIT OR Apache-2.0 — https://github.com/Amanieu/thread_local-rs + time 0.3.51 — MIT OR Apache-2.0 — https://github.com/time-rs/time + time-core 0.1.9 — MIT OR Apache-2.0 — https://github.com/time-rs/time + time-macros 0.2.30 — MIT OR Apache-2.0 — https://github.com/time-rs/time + tinystr 0.8.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x + tinytemplate 1.2.1 — Apache-2.0 OR MIT — https://github.com/bheisler/TinyTemplate + tinyvec 1.11.0 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/tinyvec + tinyvec_macros 0.1.1 — MIT OR Apache-2.0 OR Zlib — https://github.com/Soveu/tinyvec_macros + to_method 1.1.0 — CC0-1.0 — https://github.com/whentze/to_method + tokio 1.52.3 — MIT — https://github.com/tokio-rs/tokio + tokio-macros 2.7.0 — MIT — https://github.com/tokio-rs/tokio + toml 0.8.23 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml 0.9.12+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml 1.1.2+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_datetime 0.6.11 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_datetime 0.7.5+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_datetime 1.1.1+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_edit 0.22.27 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_parser 1.1.2+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_write 0.1.2 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + toml_writer 1.1.1+spec-1.1.0 — MIT OR Apache-2.0 — https://github.com/toml-rs/toml + tracing 0.1.44 — MIT — https://github.com/tokio-rs/tracing + tracing-attributes 0.1.31 — MIT — https://github.com/tokio-rs/tracing + tracing-core 0.1.36 — MIT — https://github.com/tokio-rs/tracing + tracing-log 0.2.0 — MIT — https://github.com/tokio-rs/tracing + tracing-subscriber 0.3.23 — MIT — https://github.com/tokio-rs/tracing + typenum 1.20.1 — MIT OR Apache-2.0 — https://github.com/paholg/typenum + unarray 0.1.4 — MIT OR Apache-2.0 — https://github.com/cameron1024/unarray + unicode-ident 1.0.24 — (MIT OR Apache-2.0) AND Unicode-3.0 — https://github.com/dtolnay/unicode-ident + unicode-segmentation 1.13.3 — MIT OR Apache-2.0 — https://github.com/unicode-rs/unicode-segmentation + unicode-width 0.2.2 — MIT OR Apache-2.0 — https://github.com/unicode-rs/unicode-width + universal-hash 0.5.1 — MIT OR Apache-2.0 — https://github.com/RustCrypto/traits + untrusted 0.9.0 — ISC — https://github.com/briansmith/untrusted + ureq 2.12.1 — MIT OR Apache-2.0 — https://github.com/algesten/ureq + url 2.5.8 — MIT OR Apache-2.0 — https://github.com/servo/rust-url + utf8_iter 1.0.4 — Apache-2.0 OR MIT — https://github.com/hsivonen/utf8_iter + utf8parse 0.2.2 — Apache-2.0 OR MIT — https://github.com/alacritty/vte + valuable 0.1.1 — MIT — https://github.com/tokio-rs/valuable + vcpkg 0.2.15 — MIT/Apache-2.0 — https://github.com/mcgoo/vcpkg-rs + version-compare 0.2.1 — MIT — https://gitlab.com/timvisee/version-compare + version_check 0.9.5 — MIT/Apache-2.0 — https://github.com/SergioBenitez/version_check + wait-timeout 0.2.1 — MIT/Apache-2.0 — https://github.com/alexcrichton/wait-timeout + walkdir 2.5.0 — Unlicense/MIT — https://github.com/BurntSushi/walkdir + wasapi 0.23.0 — MIT — https://github.com/HEnquist/wasapi-rs + wasi 0.11.1+wasi-snapshot-preview1 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/wasi + wasip2 1.0.4+wasi-0.2.12 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/wasi-rs + wasm-bindgen 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen + wasm-bindgen-macro 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/macro + wasm-bindgen-macro-support 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/macro-support + wasm-bindgen-shared 0.2.126 — MIT OR Apache-2.0 — https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/shared + web-time 1.1.0 — MIT OR Apache-2.0 — https://github.com/daxpedda/web-time + webpki-root-certs 1.0.8 — CDLA-Permissive-2.0 — https://github.com/rustls/webpki-roots + webpki-roots 0.26.11 — CDLA-Permissive-2.0 — https://github.com/rustls/webpki-roots + webpki-roots 1.0.8 — CDLA-Permissive-2.0 — https://github.com/rustls/webpki-roots + wide 0.7.33 — Zlib OR Apache-2.0 OR MIT — https://github.com/Lokathor/wide + winapi-util 0.1.11 — Unlicense OR MIT — https://github.com/BurntSushi/winapi-util + windows 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-canvas 0.0.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-collections 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-collections 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-composition 0.0.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-core 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-core 0.62.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-future 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-future 0.3.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-implement 0.60.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-implement 0.60.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-interface 0.59.3 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-interface 0.59.3 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-link 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-link 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-numerics 0.3.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-numerics 0.3.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-reactor 0.0.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-reactor-setup 0.0.0 — MIT OR Apache-2.0 + windows-reference 0.1.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-result 0.4.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-result 0.4.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-strings 0.5.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-strings 0.5.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-sys 0.45.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-sys 0.52.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-sys 0.59.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-sys 0.60.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-sys 0.61.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-targets 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-targets 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-targets 0.53.5 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-threading 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-threading 0.2.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-time 0.1.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows-window 0.0.0 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_aarch64_gnullvm 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_aarch64_gnullvm 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_aarch64_gnullvm 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_aarch64_msvc 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_aarch64_msvc 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_aarch64_msvc 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_gnu 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_gnu 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_gnu 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_gnullvm 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_gnullvm 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_msvc 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_msvc 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_i686_msvc 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_gnu 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_gnu 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_gnu 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_gnullvm 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_gnullvm 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_gnullvm 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_msvc 0.42.2 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_msvc 0.52.6 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + windows_x86_64_msvc 0.53.1 — MIT OR Apache-2.0 — https://github.com/microsoft/windows-rs + winnow 0.7.15 — MIT — https://github.com/winnow-rs/winnow + winnow 1.0.3 — MIT — https://github.com/winnow-rs/winnow + winreg 0.56.0 — MIT — https://github.com/gentoo90/winreg-rs + winresource 0.1.31 — MIT — https://github.com/BenjaminRi/winresource + wit-bindgen 0.57.1 — Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT — https://github.com/bytecodealliance/wit-bindgen + writeable 0.6.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x + xattr 1.6.1 — MIT OR Apache-2.0 — https://github.com/Stebalien/xattr + yasna 0.5.2 — MIT OR Apache-2.0 — https://github.com/qnighy/yasna.rs + yoke 0.8.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x + yoke-derive 0.8.2 — Unicode-3.0 — https://github.com/unicode-org/icu4x + zerocopy 0.7.35 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy + zerocopy 0.8.52 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy + zerocopy-derive 0.7.35 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy + zerocopy-derive 0.8.52 — BSD-2-Clause OR Apache-2.0 OR MIT — https://github.com/google/zerocopy + zerofrom 0.1.8 — Unicode-3.0 — https://github.com/unicode-org/icu4x + zerofrom-derive 0.1.7 — Unicode-3.0 — https://github.com/unicode-org/icu4x + zeroize 1.9.0 — Apache-2.0 OR MIT — https://github.com/RustCrypto/utils + zerotrie 0.2.4 — Unicode-3.0 — https://github.com/unicode-org/icu4x + zerovec 0.11.6 — Unicode-3.0 — https://github.com/unicode-org/icu4x + zerovec-derive 0.11.3 — Unicode-3.0 — https://github.com/unicode-org/icu4x + zmij 1.0.21 — MIT — https://github.com/dtolnay/zmij + +---------------------------------------------------------------------------- +Crates whose package did not embed a license file (SPDX + source only) +---------------------------------------------------------------------------- + anes 0.1.6 — MIT OR Apache-2.0 — https://github.com/zrzka/anes-rs + atomig-macro 0.4.0 — MIT/Apache-2.0 — https://github.com/LukasKalbertodt/atomig/ + cookie-factory 0.3.3 — MIT — https://github.com/rust-bakery/cookie-factory + defmt-parser 1.0.0 — MIT OR Apache-2.0 — https://github.com/knurling-rs/defmt + jni-sys-macros 0.4.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys + openh264 0.9.3 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs + openh264-sys2 0.9.6 — BSD-2-Clause — https://github.com/ralfbiedert/openh264-rs + r-efi 5.3.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi + r-efi 6.0.0 — MIT OR Apache-2.0 OR LGPL-2.1-or-later — https://github.com/r-efi/r-efi + rustls-platform-verifier-android 0.1.1 — MIT OR Apache-2.0 — https://github.com/rustls/rustls-platform-verifier + sdl3-image-src 3.4.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-mixer-src 3.2.4 — Zlib — https://codeberg.org/maia/sdl3-sys-rs + sdl3-ttf-src 3.2.2 — Zlib — https://github.com/maia-s/sdl3-sys-rs + skia-bindings 0.87.0 — MIT — https://github.com/rust-skia/rust-skia + skia-safe 0.87.0 — MIT — https://github.com/rust-skia/rust-skia + test_reactor 0.0.0 — UNKNOWN + valuable 0.1.1 — MIT — https://github.com/tokio-rs/valuable + yasna 0.5.2 — MIT OR Apache-2.0 — https://github.com/qnighy/yasna.rs + +============================================================================ +FULL LICENSE TEXTS (deduplicated) +============================================================================ + +---------------------------------------------------------------------------- +The following license (LICENSE-0BSD) applies to: adler2 2.0.1 +---------------------------------------------------------------------------- +Copyright (C) Jonas Schievink + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: adler2 2.0.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/LICENSE-2.0 + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: adler2 2.0.1, anyhow 1.0.103, async-channel 2.5.0, concurrent-queue 2.5.0, curve25519-dalek-derive 0.1.1, displaydoc 0.2.6, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, flume 0.12.0, hermit-abi 0.5.2, is-terminal 0.4.17, itoa 1.0.18, linux-raw-sys 0.12.1, minimal-lexical 0.2.1, once_cell 1.21.4, parking 2.2.1, paste 1.0.15, pin-project-lite 0.2.17, portable-atomic 1.14.0, portable-atomic-util 0.2.7, prettyplease 0.2.37, proc-macro2 1.0.106, quote 1.0.46, rustc-hash 2.1.2, rustix 1.1.4, rustversion 1.0.22, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, syn 2.0.118, system-deps 7.0.8, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1, zmij 1.0.21 +---------------------------------------------------------------------------- +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 following license (LICENSE-APACHE) applies to: aead 0.5.2, aes 0.8.4, aes-gcm 0.10.3, block-buffer 0.10.4, block-padding 0.3.3, chacha20 0.9.1, chacha20poly1305 0.10.1, cipher 0.4.4, const-oid 0.9.6, cpufeatures 0.2.17, crypto-common 0.1.7, ctr 0.9.2, digest 0.10.7, ghash 0.5.1, hkdf 0.12.4, hmac 0.12.1, inout 0.1.4, opaque-debug 0.3.1, poly1305 0.8.0, polyval 0.6.2, sha2 0.10.9, spake2 0.4.0, universal-hash 0.5.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: aead 0.5.2 +---------------------------------------------------------------------------- +Copyright (c) 2019 The RustCrypto Project Developers +Copyright (c) 2019 MobileCoin, LLC + +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 following license (LICENSE-MIT) applies to: aes 0.8.4 +---------------------------------------------------------------------------- +Copyright (c) 2018 Artyom Pavlov + +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 following license (LICENSE-MIT) applies to: aes-gcm 0.10.3, chacha20poly1305 0.10.1 +---------------------------------------------------------------------------- +Copyright (c) 2019 The RustCrypto Project Developers + +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 following license (COPYING) applies to: aho-corasick 1.1.4, byteorder 1.5.0, jiff 0.2.35, jiff-core 0.1.0, jiff-static 0.2.35, memchr 2.8.2, same-file 1.0.6, walkdir 2.5.0, winapi-util 0.1.11 +---------------------------------------------------------------------------- +This project is dual-licensed under the Unlicense and MIT licenses. + +You may use this code under the terms of either license. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: aho-corasick 1.1.4, byteorder 1.5.0, jiff 0.2.35, jiff-core 0.1.0, jiff-static 0.2.35, memchr 2.8.2, walkdir 2.5.0 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Andrew Gallant + +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 following license (UNLICENSE) applies to: aho-corasick 1.1.4, byteorder 1.5.0, jiff 0.2.35, jiff-core 0.1.0, jiff-static 0.2.35, memchr 2.8.2, same-file 1.0.6, walkdir 2.5.0, winapi-util 0.1.11 +---------------------------------------------------------------------------- +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to + + +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: annotate-snippets 0.11.5, anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, crc32fast 1.5.0, env_filter 2.0.0, env_logger 0.11.11, is_terminal_polyfill 1.70.2, jni-sys 0.3.1, jni-sys 0.4.1, once_cell_polyfill 1.70.2, quick-error 1.2.3, serde_spanned 0.6.9, serde_spanned 1.1.1, toml 0.8.23, toml 0.9.12+spec-1.1.0, toml 1.1.2+spec-1.1.0, toml_datetime 0.6.11, toml_datetime 0.7.5+spec-1.1.0, toml_datetime 1.1.1+spec-1.1.0, toml_edit 0.22.27, toml_parser 1.1.2+spec-1.1.0, toml_write 0.1.2, toml_writer 1.1.1+spec-1.1.0 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: annotate-snippets 0.11.5, anstream 1.0.0, anstyle 1.0.14, anstyle-parse 1.0.0, anstyle-query 1.1.5, anstyle-wincon 3.0.11, clap 4.6.1, clap_builder 4.6.0, clap_lex 1.1.0, colorchoice 1.0.5, env_filter 2.0.0, env_logger 0.11.11, is_terminal_polyfill 1.70.2, once_cell_polyfill 1.70.2, serde_spanned 0.6.9, serde_spanned 1.1.1, toml 0.8.23, toml 0.9.12+spec-1.1.0, toml 1.1.2+spec-1.1.0, toml_datetime 0.6.11, toml_datetime 0.7.5+spec-1.1.0, toml_datetime 1.1.1+spec-1.1.0, toml_edit 0.22.27, toml_parser 1.1.2+spec-1.1.0, toml_write 0.1.2, toml_writer 1.1.1+spec-1.1.0 +---------------------------------------------------------------------------- +Copyright (c) Individual 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 following license (LICENSE-APACHE) applies to: anyhow 1.0.103, fastbloom 0.14.1, itoa 1.0.18, libc 0.2.186, paste 1.0.15, prettyplease 0.2.37, proc-macro2 1.0.106, quote 1.0.46, rustc-hash 2.1.2, rustversion 1.0.22, semver 1.0.28, serde 1.0.228, serde_core 1.0.228, serde_derive 1.0.228, serde_json 1.0.150, syn 2.0.118, thiserror 1.0.69, thiserror 2.0.18, thiserror-impl 1.0.69, thiserror-impl 2.0.18, unicode-ident 1.0.24, utf8parse 0.2.2 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + + +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: ash 0.38.0+1.3.281 +---------------------------------------------------------------------------- +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +Copyright 2016 Maik Klein + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: ash 0.38.0+1.3.281 +---------------------------------------------------------------------------- +Copyright (c) 2016 ASH + +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 following license (LICENSE-APACHE) applies to: assert_matches 1.5.0, async-channel 2.5.0, autocfg 1.5.1, base64 0.22.1, bitflags 1.3.2, bitflags 2.13.0, bumpalo 3.20.3, cast 0.3.0, cc 1.2.65, cexpr 0.6.0, cfg-if 1.0.4, cmake 0.1.58, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, displaydoc 0.2.6, either 1.16.0, equivalent 1.0.2, errno 0.3.14, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, filetime 0.2.29, find-msvc-tools 0.1.9, fixedbitset 0.5.7, flate2 1.1.9, fnv 1.0.7, form_urlencoded 1.2.2, glob 0.3.3, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.0, itertools 0.10.5, itertools 0.13.0, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, lazy_static 1.5.0, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, nasm-rs 0.3.2, num-integer 0.1.46, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, pkg-config 0.3.33, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, rustc_version 0.4.1, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, stable_deref_trait 1.2.1, system-deps 7.0.8, tar 0.4.46, tempfile 3.27.0, thread_local 1.1.9, tinytemplate 1.2.1, unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, vcpkg 0.2.15, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, wit-bindgen 0.57.1, xattr 1.6.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: assert_matches 1.5.0 +---------------------------------------------------------------------------- +Copyright (c) 2016 Murarth + +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 following license (LICENSE-APACHE) applies to: atomig 0.4.3, bit-set 0.8.0, bit-vec 0.8.0, cfg-expr 0.20.8, defmt 1.1.1, defmt-macros 1.1.1, minimal-lexical 0.2.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: atomig 0.4.3 +---------------------------------------------------------------------------- +Copyright (c) 2016 Project Developers + +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 following license (LICENSE.md) applies to: audiopus_sys 0.2.2 +---------------------------------------------------------------------------- +ISC License + +Copyright (c) 2019, Lakelezz + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: autocfg 1.5.1 +---------------------------------------------------------------------------- +Copyright (c) 2018 Josh Stone + +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 following license (LICENSE-MIT) applies to: base64 0.22.1 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Alice Maz + +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 following license (bazzite.txt) applies to: Bazzite logo (vendored, assets/os-icons) +---------------------------------------------------------------------------- +Bazzite — the `bazzite` mark in assets/os-icons/ is derived from the Bazzite logo in +the Bazzite source repository (repo_content/Bazzite.svg). + +Copyright (c) Universal Blue (https://github.com/ublue-os/bazzite) + +Licensed under the Apache License, Version 2.0, +https://www.apache.org/licenses/LICENSE-2.0. + +Modifications: the logo's "b" letterform was lifted out of the surrounding badge, the +gradient and decorative overlays were dropped, and the path was translated and scaled +into a 24x24 box with a monochrome fill (fill="currentColor"). + +Brand icons are trademarks of their respective owners and are used for identification +purposes only; their use does not imply endorsement. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: bindgen 0.72.1 +---------------------------------------------------------------------------- +BSD 3-Clause License + +Copyright (c) 2013, Jyun-Yan You +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: bit-set 0.8.0, bit-vec 0.8.0 +---------------------------------------------------------------------------- +Copyright (c) 2023 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: bitflags 1.3.2, bitflags 2.13.0, glob 0.3.3, log 0.4.33, num-integer 0.1.46, num-traits 0.2.19, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11 +---------------------------------------------------------------------------- +Copyright (c) 2014 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: block-buffer 0.10.4, block-padding 0.3.3 +---------------------------------------------------------------------------- +Copyright (c) 2018-2019 The RustCrypto Project Developers + +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 following license (LICENSE-MIT) applies to: bumpalo 3.20.3 +---------------------------------------------------------------------------- +Copyright (c) 2019 Nick Fitzgerald + +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 following license (LICENSE-APACHE) applies to: bytemuck 1.25.0, bytemuck_derive 1.10.2, safe_arch 0.7.4 +---------------------------------------------------------------------------- +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + + "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: bytemuck 1.25.0, bytemuck_derive 1.10.2 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2019 Daniel "Lokathor" Gee. + +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 (including the next paragraph) 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 following license (LICENSE-ZLIB) applies to: bytemuck 1.25.0, bytemuck_derive 1.10.2, tinyvec 1.11.0 +---------------------------------------------------------------------------- +Copyright (c) 2019 Daniel "Lokathor" Gee. + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: bytes 1.12.0 +---------------------------------------------------------------------------- +Copyright (c) 2018 Carl Lerche + +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 following license (LICENSE-MIT) applies to: cast 0.3.0 +---------------------------------------------------------------------------- +Copyright (c) 2014-2017 Jorge Aparicio + +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 following license (LICENSE) applies to: cbindgen 0.29.4 +---------------------------------------------------------------------------- +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, filetime 0.2.29, find-msvc-tools 0.1.9, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 +---------------------------------------------------------------------------- +Copyright (c) 2014 Alex Crichton + +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 following license (COPYRIGHT-RUST.txt) applies to: cesu8 1.1.0 +---------------------------------------------------------------------------- +Short version for non-lawyers: + +The Rust Project is dual-licensed under Apache 2.0 and MIT +terms. + + +Longer version: + +The Rust Project is copyright 2014, The Rust Project +Developers (given in the file AUTHORS.txt). + +Licensed under the Apache License, Version 2.0 + or the MIT +license , +at your option. All files in the project carrying such +notice may not be copied, modified, or distributed except +according to those terms. + + +The Rust Project includes packages written by third parties. +The following third party packages are included, and carry +their own copyright notices and license terms: + +* Two header files that are part of the Valgrind + package. These files are found at src/rt/vg/valgrind.h and + src/rt/vg/memcheck.h, within this distribution. These files + are redistributed under the following terms, as noted in + them: + + for src/rt/vg/valgrind.h: + + This file is part of Valgrind, a dynamic binary + instrumentation framework. + + Copyright (C) 2000-2010 Julian Seward. All rights + reserved. + + Redistribution and use in source and binary forms, with + or without modification, are permitted provided that the + following conditions are met: + + 1. Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + + 2. The origin of this software must not be + misrepresented; you must not claim that you wrote the + original software. If you use this software in a + product, an acknowledgment in the product + documentation would be appreciated but is not + required. + + 3. Altered source versions must be plainly marked as + such, and must not be misrepresented as being the + original software. + + 4. The name of the author may not be used to endorse or + promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + OF SUCH DAMAGE. + + for src/rt/vg/memcheck.h: + + This file is part of MemCheck, a heavyweight Valgrind + tool for detecting memory errors. + + Copyright (C) 2000-2010 Julian Seward. All rights + reserved. + + Redistribution and use in source and binary forms, with + or without modification, are permitted provided that the + following conditions are met: + + 1. Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + + 2. The origin of this software must not be + misrepresented; you must not claim that you wrote the + original software. If you use this software in a + product, an acknowledgment in the product + documentation would be appreciated but is not + required. + + 3. Altered source versions must be plainly marked as + such, and must not be misrepresented as being the + original software. + + 4. The name of the author may not be used to endorse or + promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + OF SUCH DAMAGE. + +* The auxiliary file src/etc/pkg/modpath.iss contains a + library routine compiled, by Inno Setup, into the Windows + installer binary. This file is licensed under the LGPL, + version 3, but, in our legal interpretation, this does not + affect the aggregate "collected work" license of the Rust + distribution (MIT/ASL2) nor any other components of it. We + believe that the terms governing distribution of the + binary Windows installer built from modpath.iss are + therefore LGPL, but not the terms governing distribution + of any of the files installed by such an installer (such + as the Rust compiler or runtime libraries themselves). + +* The src/rt/miniz.c file, carrying an implementation of + RFC1950/RFC1951 DEFLATE, by Rich Geldreich + . All uses of this file are + permitted by the embedded "unlicense" notice + (effectively: public domain with warranty disclaimer). + +* LLVM. Code for this package is found in src/llvm. + + Copyright (c) 2003-2013 University of Illinois at + Urbana-Champaign. All rights reserved. + + Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + + Permission is hereby granted, free of charge, to any + person obtaining a copy of this software and associated + documentation files (the "Software"), to deal with 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: + + * Redistributions of source code must retain the + above copyright notice, this list of conditions + and the following disclaimers. + + * Redistributions in binary form must reproduce the + above copyright notice, this list of conditions + and the following disclaimers in the documentation + and/or other materials provided with the + distribution. + + * Neither the names of the LLVM Team, University of + Illinois at Urbana-Champaign, nor the names of its + contributors may be used to endorse or promote + products derived from this Software without + specific prior written permission. + + 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 CONTRIBUTORS 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 WITH THE SOFTWARE. + +* Additional libraries included in LLVM carry separate + BSD-compatible licenses. See src/llvm/LICENSE.txt for + details. + +* compiler-rt, in src/compiler-rt is dual licensed under + LLVM's license and MIT: + + Copyright (c) 2009-2014 by the contributors listed in + CREDITS.TXT + + All rights reserved. + + Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + + Permission is hereby granted, free of charge, to any + person obtaining a copy of this software and associated + documentation files (the "Software"), to deal with 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: + + * Redistributions of source code must retain the + above copyright notice, this list of conditions + and the following disclaimers. + + * Redistributions in binary form must reproduce the + above copyright notice, this list of conditions + and the following disclaimers in the documentation + and/or other materials provided with the + distribution. + + * Neither the names of the LLVM Team, University of + Illinois at Urbana-Champaign, nor the names of its + contributors may be used to endorse or promote + products derived from this Software without + specific prior written permission. + + 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 CONTRIBUTORS 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 WITH THE SOFTWARE. + + ======================================================== + + Copyright (c) 2009-2014 by the contributors listed in + CREDITS.TXT + + 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. + +* Portions of the FFI code for interacting with the native ABI + is derived from the Clay programming language, which carries + the following license. + + Copyright (C) 2008-2010 Tachyon Technologies. + All rights reserved. + + Redistribution and use in source and binary forms, with + or without modification, are permitted provided that the + following conditions are met: + + 1. Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + + 2. Redistributions in binary form must reproduce the + above copyright notice, this list of conditions and + the following disclaimer in the documentation and/or + other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + DEVELOPERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + OF SUCH DAMAGE. + +* Hoedown, the markdown parser, under src/rt/hoedown, is + licensed as follows. + + Copyright (c) 2008, Natacha Porté + Copyright (c) 2011, Vicent Martí + Copyright (c) 2013, Devin Torres and the Hoedown authors + + Permission to use, copy, modify, and distribute this + software for any purpose with or without fee is hereby + granted, provided that the above copyright notice and + this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR + DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE + INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR + ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA + OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +* libbacktrace, under src/libbacktrace: + + Copyright (C) 2012-2014 Free Software Foundation, Inc. + Written by Ian Lance Taylor, Google. + + Redistribution and use in source and binary forms, with + or without modification, are permitted provided that the + following conditions are met: + + (1) Redistributions of source code must retain the + above copyright notice, this list of conditions and + the following disclaimer. + + (2) Redistributions in binary form must reproduce + the above copyright notice, this list of conditions + and the following disclaimer in the documentation + and/or other materials provided with the + distribution. + + (3) The name of the author may not be used to + endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + OF SUCH DAMAGE. */ + +* jemalloc, under src/jemalloc: + + Copyright (C) 2002-2014 Jason Evans + . All rights reserved. + Copyright (C) 2007-2012 Mozilla Foundation. + All rights reserved. + Copyright (C) 2009-2014 Facebook, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright notice(s), + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice(s), + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) + BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + OF SUCH DAMAGE. + +* Additional copyright may be retained by contributors other + than Mozilla, the Rust Project Developers, or the parties + enumerated in this file. Such copyright can be determined + on a case-by-case basis by examining the author of each + portion of a file in the revision-control commit records + of the project, or by consulting representative comments + claiming copyright ownership for a file. + + For example, the text: + + "Copyright (c) 2011 Google Inc." + + appears in some files, and these files thereby denote + that their author and copyright-holder is Google Inc. + + In all such cases, the absence of explicit licensing text + indicates that the contributor chose to license their work + for distribution under identical terms to those Mozilla + has chosen for the collective work, enumerated at the top + of this file. The only difference is the retention of + copyright itself, held by the contributor. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: cexpr 0.6.0 +---------------------------------------------------------------------------- +(C) Copyright 2016 Jethro G. Beekman + +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 following license (LICENSE-MIT) applies to: cfg-expr 0.20.8 +---------------------------------------------------------------------------- +Copyright (c) 2019 Embark Studios + +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 following license (LICENSE) applies to: cfg_aliases 0.2.1 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2020 Katharos Technology + +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 following license (NOTICES.md) applies to: cfg_aliases 0.2.1 +---------------------------------------------------------------------------- +# 3rd Party Notices + +The `cfg_aliases!` macro uses a lot of the code from [`tectonic_cfg_support::target_cfg!`] macro which is under the following license: + +[`tectonic_cfg_support::target_cfg!`]: https://github.com/tectonic-typesetting/tectonic/blob/f2439b936470ad27bdf92882064bc4702ee01899/cfg_support/src/lib.rs#L166 + + tectonic_cfg_support is licensed under the MIT License. + + 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 following license (LICENSE-MIT) applies to: chacha20 0.9.1 +---------------------------------------------------------------------------- +Copyright (c) 2019-2023 The RustCrypto Project Developers + +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 following license (LICENSE) applies to: ciborium 0.2.2, ciborium-io 0.2.2, ciborium-ll 0.2.2, clang-sys 1.8.1, flume 0.12.0, lru-slab 0.1.2, quinn 0.11.11, quinn-proto 0.11.15, quinn-udp 0.5.14, rpkg-config 0.1.2, rustls-platform-verifier 0.6.2, tinyvec 1.11.0, unarray 0.1.4, ureq 2.12.1, utf8_iter 1.0.4, zeroize 1.9.0 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: cipher 0.4.4 +---------------------------------------------------------------------------- +Copyright (c) 2016-2020 RustCrypto Developers + +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 following license (LICENSE) applies to: combine 4.6.7 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Markus Westerlind + +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 following license (LICENSE-MIT) applies to: const-oid 0.9.6 +---------------------------------------------------------------------------- +Copyright (c) 2020-2022 The RustCrypto Project Developers + +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 following license (LICENSE) applies to: convert_case 0.8.0 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2025 rutrum + +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 following license (LICENSE-MIT) applies to: core-foundation 0.10.1, core-foundation-sys 0.8.7 +---------------------------------------------------------------------------- +Copyright (c) 2012-2013 Mozilla Foundation + +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 following license (LICENSE-MIT) applies to: cpufeatures 0.2.17 +---------------------------------------------------------------------------- +Copyright (c) 2020-2025 The RustCrypto Project Developers + +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 following license (LICENSE-MIT) applies to: crc32fast 1.5.0 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2018 Sam Rijs, Alex Crichton and 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 following license (LICENSE-MIT) applies to: criterion 0.5.1, criterion-plot 0.5.0 +---------------------------------------------------------------------------- +Copyright (c) 2014 Jorge Aparicio + +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 following license (LICENSE-MIT) applies to: crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2019 The Crossbeam Project Developers + +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 following license (LICENSE) applies to: crunchy 0.2.4 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright 2017-2023 Eira Fransham. + +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 following license (LICENSE-MIT) applies to: crypto-common 0.1.7 +---------------------------------------------------------------------------- +Copyright (c) 2021 RustCrypto Developers + +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 following license (LICENSE-MIT) applies to: ctr 0.9.2 +---------------------------------------------------------------------------- +Copyright (c) 2018-2022 RustCrypto Developers +Copyright (c) 2018 Artyom Pavlov + +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 following license (LICENSE) applies to: curve25519-dalek 4.1.3 +---------------------------------------------------------------------------- +Copyright (c) 2016-2021 isis agora lovecruft. All rights reserved. +Copyright (c) 2016-2021 Henry de Valence. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== + +Portions of curve25519-dalek were originally derived from Adam Langley's +Go ed25519 implementation, found at , +under the following licence: + +======================================================================== + +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: defmt 1.1.1, defmt-macros 1.1.1 +---------------------------------------------------------------------------- +Copyright (c) Ferrous Systems + +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 following license (LICENSE-Apache) applies to: deranged 0.5.8 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Jacob Pratt et al. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: deranged 0.5.8 +---------------------------------------------------------------------------- +Copyright (c) 2024 Jacob Pratt et al. + +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 following license (LICENSE-MIT) applies to: digest 0.10.7, hmac 0.12.1 +---------------------------------------------------------------------------- +Copyright (c) 2017 Artyom Pavlov + +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 following license (LICENSE-MIT) applies to: either 1.16.0, itertools 0.10.5, itertools 0.13.0 +---------------------------------------------------------------------------- +Copyright (c) 2015 + +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 following license (LICENSE-MIT) applies to: equivalent 1.0.2 +---------------------------------------------------------------------------- +Copyright (c) 2016--2023 + +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 following license (LICENSE-MIT) applies to: errno 0.3.14 +---------------------------------------------------------------------------- +Copyright (c) 2014 Chris Wong + +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 following license (LICENSE-MIT) applies to: fastbloom 0.14.1 +---------------------------------------------------------------------------- +Copyright (c) 2023 Thomas Pendock + +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 following license (COPYRIGHT) applies to: fiat-crypto 0.2.9 +---------------------------------------------------------------------------- +SPDX-License-Identifier: MIT OR Apache-2.0 OR BSD-1-Clause + +Fiat Cryptography is licensed under the MIT License or +, the Apache License, Version 2.0 + or , or +the BSD 1-Clause License or +, at your option. + + +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: fiat-crypto 0.2.9 +---------------------------------------------------------------------------- +The Apache License, Version 2.0 (Apache-2.0) + +Copyright 2015-2020 the fiat-crypto authors (see the AUTHORS file) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-BSD-1) applies to: fiat-crypto 0.2.9 +---------------------------------------------------------------------------- +The BSD 1-Clause License (BSD-1-Clause) + +Copyright (c) 2015-2020 the fiat-crypto authors (see the AUTHORS file) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +THIS SOFTWARE IS PROVIDED BY the fiat-crypto authors "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Berkeley Software Design, +Inc. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: fiat-crypto 0.2.9 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015-2020 the fiat-crypto authors (see the AUTHORS file). + +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 following license (LICENSE-MIT) applies to: fixedbitset 0.5.7 +---------------------------------------------------------------------------- +Copyright (c) 2015-2017 + +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 following license (LICENSE-MIT) applies to: flate2 1.1.9 +---------------------------------------------------------------------------- +Copyright (c) 2014-2026 Alex Crichton + +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 following license (LICENSE-MIT) applies to: fnv 1.0.7 +---------------------------------------------------------------------------- +Copyright (c) 2017 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 following license (LICENSE) applies to: foldhash 0.2.0 +---------------------------------------------------------------------------- +Copyright (c) 2024 Orson Peters + +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a product, + an acknowledgment in the product documentation would be appreciated but is + not required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (font-awesome-brands.txt) applies to: Font Awesome Free brand icons (vendored, assets/os-icons) +---------------------------------------------------------------------------- +Font Awesome Free — brand icons (apple, linux, steam, ubuntu, fedora, opensuse in +assets/os-icons/) are from Font Awesome Free. + +Copyright (c) Fonticons, Inc. (https://fontawesome.com) + +Font Awesome Free icons are licensed under the Creative Commons Attribution 4.0 +International license (CC BY 4.0), https://creativecommons.org/licenses/by/4.0/. +The icons are redistributed here as monochrome SVG path data with no +modifications beyond color normalization (fill="currentColor"). + +Per the Font Awesome Free license (https://fontawesome.com/license/free): +"Font Awesome Free is free, open source, and GPL friendly. You can use it for +commercial projects, open source projects, or really almost whatever you want. +Attribution is required by MIT, SIL OFL, and CC BY licenses." + +Brand icons are trademarks of their respective owners and are used for +identification purposes only; their use does not imply endorsement. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: form_urlencoded 1.2.2 +---------------------------------------------------------------------------- +Copyright (c) 2013-2016 The rust-url developers + +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 following license (LICENSE-APACHE) applies to: futures-channel 0.3.32, futures-core 0.3.32, futures-io 0.3.32, futures-macro 0.3.32, futures-sink 0.3.32, futures-task 0.3.32, futures-util 0.3.32 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: futures-channel 0.3.32, futures-core 0.3.32, futures-io 0.3.32, futures-macro 0.3.32, futures-sink 0.3.32, futures-task 0.3.32, futures-util 0.3.32 +---------------------------------------------------------------------------- +Copyright (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +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 following license (LICENSE) applies to: generic-array 0.14.7 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Bartłomiej Kamiński + +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 following license (LICENSE-APACHE) applies to: getrandom 0.2.17, getrandom 0.3.4, getrandom 0.4.3 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: getrandom 0.2.17 +---------------------------------------------------------------------------- +Copyright (c) 2018-2024 The rust-random Project Developers +Copyright (c) 2014 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: getrandom 0.3.4 +---------------------------------------------------------------------------- +Copyright (c) 2018-2025 The rust-random Project Developers +Copyright (c) 2014 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: getrandom 0.4.3 +---------------------------------------------------------------------------- +Copyright (c) 2018-2026 The rust-random Project Developers +Copyright (c) 2014 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: ghash 0.5.1 +---------------------------------------------------------------------------- +Copyright (c) 2019 RustCrypto Developers + +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 following license (LICENSE) applies to: Granite subset (vendored, crates/pyrowave-sys) +---------------------------------------------------------------------------- +Copyright (c) 2017-2026 Hans-Kristian Arntzen + +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 following license (LICENSE-APACHE) applies to: half 2.7.1, miniz_oxide 0.8.9, num-conv 0.2.2, pin-project-lite 0.2.17, portable-atomic 1.14.0, portable-atomic-util 0.2.7, time 0.3.51, time-core 0.1.9, time-macros 0.2.30 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: half 2.7.1 +---------------------------------------------------------------------------- +MIT License + +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 following license (LICENSE-MIT) applies to: hashbrown 0.17.1 +---------------------------------------------------------------------------- +Copyright (c) 2016 Amanieu d'Antras + +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 following license (LICENSE-MIT) applies to: heck 0.5.0, unicode-segmentation 1.13.3, unicode-width 0.2.2 +---------------------------------------------------------------------------- +Copyright (c) 2015 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: hkdf 0.12.4 +---------------------------------------------------------------------------- +Copyright (c) 2015-2018 Vlad Filippov +Copyright (c) 2018-2021 RustCrypto Developers + +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 following license (LICENSE) applies to: icu_collections 2.2.0, icu_locale_core 2.2.0, icu_normalizer 2.2.0, icu_normalizer_data 2.2.0, icu_properties 2.2.0, icu_properties_data 2.2.0, icu_provider 2.2.0, litemap 0.8.2, potential_utf 0.1.5, tinystr 0.8.3, writeable 0.6.3, yoke 0.8.3, yoke-derive 0.8.2, zerofrom 0.1.8, zerofrom-derive 0.1.7, zerotrie 0.2.4, zerovec 0.11.6, zerovec-derive 0.11.3 +---------------------------------------------------------------------------- +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: idna 1.1.0, percent-encoding 2.3.2, url 2.5.8 +---------------------------------------------------------------------------- +Copyright (c) 2013-2025 The rust-url developers + +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 following license (LICENSE-MIT) applies to: idna_adapter 1.2.2 +---------------------------------------------------------------------------- +Copyright (c) The rust-url developers + +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 following license (LICENSE-BSD) applies to: if-addrs 0.13.4, if-addrs 0.15.0 +---------------------------------------------------------------------------- +Copyright 2018 MaidSafe.net limited. +Copyright 2020 messense + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: if-addrs 0.13.4, if-addrs 0.15.0 +---------------------------------------------------------------------------- +Copyright 2018 MaidSafe.net limited. +Copyright 2020 messense + +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 following license (LICENSE-MIT) applies to: indexmap 2.14.0 +---------------------------------------------------------------------------- +Copyright (c) 2016--2017 + +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 following license (LICENSE-MIT) applies to: inout 0.1.4 +---------------------------------------------------------------------------- +Copyright (c) 2022 The RustCrypto Project Developers +Copyright (c) 2022 Artyom Pavlov + +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 following license (LICENSE-MIT-atty) applies to: is-terminal 0.4.17 +---------------------------------------------------------------------------- +Portions of this project are derived from atty, which bears the following +copyright notice and permission notice: + +Copyright (c) 2015-2019 Doug Tangren + +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 following license (LICENSE-MIT) applies to: jni 0.21.1 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2016 Prevoty, Inc. and jni-rs 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 following license (LICENSE-MIT) applies to: jni-sys 0.3.1, jni-sys 0.4.1 +---------------------------------------------------------------------------- +Copyright (c) 2015 The rust-jni-sys Developers + +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 following license (LICENSE-MIT) applies to: lazy_static 1.5.0, rayon 1.12.0, rayon-core 1.13.0 +---------------------------------------------------------------------------- +Copyright (c) 2010 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: libc 0.2.186 +---------------------------------------------------------------------------- +Copyright (c) The Rust Project Developers + +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 following license (LICENSE) applies to: libloading 0.8.9 +---------------------------------------------------------------------------- +Copyright © 2015, Simonas Kazlauskas + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without +fee is hereby granted, provided that the above copyright notice and this permission notice appear +in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +---------------------------------------------------------------------------- +The following license (LICENSE.txt) applies to: libm 0.2.16 +---------------------------------------------------------------------------- +rust-lang/libm as a whole is available for use under the MIT license: + +------------------------------------------------------------------------------ +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. +------------------------------------------------------------------------------ + +As a contributor, you agree that your code can be used under either the MIT +license or the Apache-2.0 license: + +------------------------------------------------------------------------------ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +------------------------------------------------------------------------------ + +This Rust library contains the following copyrights: + + Copyright (c) 2018 Jorge Aparicio + +Portions of this software are derived from third-party works licensed under +terms compatible with the above MIT license: + +* musl libc https://www.musl-libc.org/. This library contains the following + copyright: + + Copyright © 2005-2020 Rich Felker, et al. + +* The CORE-MATH project https://core-math.gitlabpages.inria.fr/. CORE-MATH + routines are available under the MIT license on a per-file basis. + +The musl libc COPYRIGHT file also includes the following notice relevant to +math portions of the library: + +------------------------------------------------------------------------------ +Much of the math library code (src/math/* and src/complex/*) is +Copyright © 1993,2004 Sun Microsystems or +Copyright © 2003-2011 David Schultz or +Copyright © 2003-2009 Steven G. Kargl or +Copyright © 2003-2009 Bruce D. Evans or +Copyright © 2008 Stephen L. Moshier or +Copyright © 2017-2018 Arm Limited +and labelled as such in comments in the individual source files. All +have been licensed under extremely permissive terms. +------------------------------------------------------------------------------ + +Copyright notices are retained in src/* files where relevant. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: libspa 0.9.2, libspa-sys 0.9.2, pipewire 0.9.2, pipewire-sys 0.9.2 +---------------------------------------------------------------------------- +Copyright The pipewire-rs 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 (including the next +paragraph) 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 following license (COPYRIGHT) applies to: linux-raw-sys 0.12.1 +---------------------------------------------------------------------------- +Short version for non-lawyers: + +`linux-raw-sys` is triple-licensed under Apache 2.0 with the LLVM Exception, +Apache 2.0, and MIT terms. + + +Longer version: + +Copyrights in the `linux-raw-sys` project are retained by their contributors. +No copyright assignment is required to contribute to the `linux-raw-sys` +project. + +Some files include code derived from Rust's `libstd`; see the comments in +the code for details. + +Except as otherwise noted (below and/or in individual files), `linux-raw-sys` +is licensed under: + + - the Apache License, Version 2.0, with the LLVM Exception + or + + - the Apache License, Version 2.0 + or + , + - or the MIT license + or + , + +at your option. + + +---------------------------------------------------------------------------- +The following license (LICENSE-Apache-2.0_WITH_LLVM-exception) applies to: linux-raw-sys 0.12.1, rustix 1.1.4, target-lexicon 0.13.5, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: lock_api 0.4.14, nasm-rs 0.3.2, parking_lot 0.12.5, parking_lot_core 0.9.12, rustc_version 0.4.1, thread_local 1.1.9 +---------------------------------------------------------------------------- +Copyright (c) 2016 The Rust Project Developers + +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 following license (LICENSE-MIT) applies to: lru-slab 0.1.2 +---------------------------------------------------------------------------- +Copyright (c) 2024 The lru-slab Developers + +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 following license (LICENSE-ZLIB) applies to: lru-slab 0.1.2 +---------------------------------------------------------------------------- +Copyright (c) 2024 The lru-slab Developers + +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a product, an + acknowledgment in the product documentation would be appreciated but is not + required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: matchers 0.2.0 +---------------------------------------------------------------------------- +Copyright (c) 2019 Eliza Weisman + +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 following license (LICENSE-APACHE) applies to: mdns-sd 0.20.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [2021-2022] [Han Xu, keepsimple@gmail.com] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: mdns-sd 0.20.1 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2021-2022, Han Xu, keepsimple@gmail.com + +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 following license (LICENSE.md) applies to: minimal-lexical 0.2.1 +---------------------------------------------------------------------------- +Minimal-lexical is dual licensed under the Apache 2.0 license as well as the MIT +license. See the LICENCE-MIT and the LICENCE-APACHE files for the licenses. + +--- + +`src/bellerophon.rs` is loosely based off the Golang implementation, +found [here](https://github.com/golang/go/blob/b10849fbb97a2244c086991b4623ae9f32c212d0/src/strconv/extfloat.go). +That code (used if the `compact` feature is enabled) is subject to a +[3-clause BSD license](https://github.com/golang/go/blob/b10849fbb97a2244c086991b4623ae9f32c212d0/LICENSE): + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: miniz_oxide 0.8.9 +---------------------------------------------------------------------------- +MIT License + +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2017 Frommi +Copyright (c) 2017-2024 oyvindln + + +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 following license (LICENSE-MIT.md) applies to: miniz_oxide 0.8.9 +---------------------------------------------------------------------------- +MIT License + +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2017 Frommi +Copyright (c) 2017-2024 oyvindln + +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 following license (LICENSE-ZLIB.md) applies to: miniz_oxide 0.8.9 +---------------------------------------------------------------------------- +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2020 Frommi +Copyright (c) 2017-2024 oyvindln + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: mio 1.2.1 +---------------------------------------------------------------------------- +Copyright (c) 2014 Carl Lerche and other MIO 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 following license (LICENSE) applies to: nix 0.30.1 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Carl Lerche + nix-rust Authors + +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 following license (LICENSE) applies to: nom 7.1.3, nom 8.0.0 +---------------------------------------------------------------------------- +Copyright (c) 2014-2019 Geoffroy Couprie + +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 following license (LICENSE) applies to: nu-ansi-term 0.50.3 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2014 Benjamin Sago +Copyright (c) 2021-2022 The Nushell Project Developers + +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 following license (LICENSE-MIT) applies to: num-conv 0.2.2 +---------------------------------------------------------------------------- +Copyright (c) Jacob Pratt + +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 following license (LICENSE-MIT) applies to: oorandom 11.1.5 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2019 Simon Heath + +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 following license (LICENSE-MIT) applies to: opaque-debug 0.3.1 +---------------------------------------------------------------------------- +Copyright (c) 2018-2024 The RustCrypto Project Developers + +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 following license (LICENSE-MIT) applies to: opus 0.3.1 +---------------------------------------------------------------------------- +Copyright (c) 2016 Tad Hardesty + +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 following license (LICENSE-THIRD-PARTY) applies to: parking 2.2.1 +---------------------------------------------------------------------------- +=============================================================================== + +Copyright 2014-2020 The Rust Project Developers + +Licensed under the Apache License, Version 2.0 or the MIT license +, at your +option. All files in the project carrying such notice may not be +copied, modified, or distributed except according to those terms. + + +---------------------------------------------------------------------------- +The following license (LICENSE.md) applies to: pem 3.0.6 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2016 Jonathan Creekmore + +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 following license (LICENSE-MIT) applies to: poly1305 0.8.0 +---------------------------------------------------------------------------- +Copyright (c) 2015-2019 RustCrypto Developers + +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 following license (LICENSE-MIT) applies to: polyval 0.6.2 +---------------------------------------------------------------------------- +Copyright (c) 2019-2023 RustCrypto Developers + +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 following license (LICENSE-Apache) applies to: powerfmt 0.2.0 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Jacob Pratt et al. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: powerfmt 0.2.0 +---------------------------------------------------------------------------- +Copyright (c) 2023 Jacob Pratt et al. + +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 following license (LICENSE-APACHE) applies to: ppv-lite86 0.2.21 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2019 The CryptoCorrosion Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: ppv-lite86 0.2.21 +---------------------------------------------------------------------------- +Copyright (c) 2019 The CryptoCorrosion 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 following license (LICENSE-MIT) applies to: proptest 1.11.0, rusty-fork 0.3.1 +---------------------------------------------------------------------------- +Copyright (c) 2016 FullContact, Inc + +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 following license (LICENSE) applies to: pyrowave (vendored, crates/pyrowave-sys) +---------------------------------------------------------------------------- +Copyright (c) 2025 Hans-Kristian Arntzen + +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 following license (LICENSE-MIT) applies to: quick-error 1.2.3 +---------------------------------------------------------------------------- +Copyright (c) 2015 The quick-error Developers + +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 following license (LICENSE-MIT) applies to: quinn 0.11.11, quinn-proto 0.11.15, quinn-udp 0.5.14 +---------------------------------------------------------------------------- +Copyright (c) 2018 The quinn Developers + +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 following license (COPYRIGHT) applies to: rand 0.9.4, rand_chacha 0.9.0, rand_core 0.6.4, rand_core 0.9.5, rand_xorshift 0.4.0 +---------------------------------------------------------------------------- +Copyrights in the Rand project are retained by their contributors. No +copyright assignment is required to contribute to the Rand project. + +For full authorship information, see the version control history. + +Except as otherwise noted (below and/or in individual files), Rand is +licensed under the Apache License, Version 2.0 or + or the MIT license + or , at your option. + +The Rand project includes code from the Rust project +published under these same licenses. + + +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: rand 0.9.4, rand_chacha 0.9.0, rand_xorshift 0.4.0 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: rand 0.9.4, rand_chacha 0.9.0, rand_core 0.6.4, rand_core 0.9.5, rand_xorshift 0.4.0 +---------------------------------------------------------------------------- +Copyright 2018 Developers of the Rand project +Copyright (c) 2014 The Rust Project Developers + +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 following license (LICENSE-APACHE) applies to: rand_core 0.6.4, rand_core 0.9.5 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + +---------------------------------------------------------------------------- +The following license (COPYING) applies to: rav1d 1.1.0 +---------------------------------------------------------------------------- +Copyright © 2018-2019, VideoLAN and dav1d authors +Copyright © 2023-2024, VideoLAN, dav1d authors, and Internet Security Research Group +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE.md) applies to: raw-cpuid 11.6.0 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Gerd Zellweger + +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 following license (LICENSE) applies to: rcgen 0.13.2 +---------------------------------------------------------------------------- +Copyright (c) 2019-2022 est31 and contributors + +Licensed under MIT or Apache License 2.0, +at your option. + +The full list of contributors can be obtained by looking +at the VCS log (originally, this crate was git versioned, +there you can do "git shortlog -sn" for this task). + +MIT License +----------- + +The MIT License (MIT) + +Copyright (c) 2019-2022 est31 and 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. + + + +Apache License, version 2.0 +--------------------------- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: readme-rustdocifier 0.1.1 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2022 Markus Laire + +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 following license (LICENSE) applies to: redox_syscall 0.5.18 +---------------------------------------------------------------------------- +Copyright (c) 2017 Redox OS Developers + +MIT License + +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 following license (LICENSE) applies to: reed-solomon-simd 3.1.0 +---------------------------------------------------------------------------- +All code from Anders Trier Olesen is under the MIT License (1st license below). +All code from Markus Laire is under MIT License (2nd license below). + +This crate is based on [1] which uses BSD-3-Clause License (3rd license below). + +[1] https://github.com/catid/leopard + +----- + +Copyright (c) 2023 Anders Trier Olesen + +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. + +----- + +Copyright (c) 2022 Markus Laire + +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. + +----- + +Copyright (c) 2017 Christopher A. Taylor. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of Leopard-RS nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: ring 0.17.14 +---------------------------------------------------------------------------- +*ring* uses an "ISC" license, like BoringSSL used to use, for new code +files. See LICENSE-other-bits for the text of that license. + +See LICENSE-BoringSSL for code that was sourced from BoringSSL under the +Apache 2.0 license. Some code that was sourced from BoringSSL under the ISC +license. In each case, the license info is at the top of the file. + +See src/polyfill/once_cell/LICENSE-APACHE and src/polyfill/once_cell/LICENSE-MIT +for the license to code that was sourced from the once_cell project. + + +---------------------------------------------------------------------------- +The following license (LICENSE-BoringSSL) applies to: ring 0.17.14 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +Licenses for support code +------------------------- + +Parts of the TLS test suite are under the Go license. This code is not included +in BoringSSL (i.e. libcrypto and libssl) when compiled, however, so +distributing code linked against BoringSSL does not trigger this license: + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +BoringSSL uses the Chromium test infrastructure to run a continuous build, +trybots etc. The scripts which manage this, and the script for generating build +metadata, are under the Chromium license. Distributing code linked against +BoringSSL does not trigger this license. + +Copyright 2015 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-other-bits) applies to: ring 0.17.14 +---------------------------------------------------------------------------- +Copyright 2015-2025 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT.md) applies to: rpkg-config 0.1.2 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2024 Maia S. R. + +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 following license (LICENSE-ZLIB.md) applies to: rpkg-config 0.1.2, sdl3-src 3.4.10 +---------------------------------------------------------------------------- +zlib License + +(C) 2024 Maia S. R. + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (COPYRIGHT) applies to: rustix 1.1.4 +---------------------------------------------------------------------------- +Short version for non-lawyers: + +`rustix` is triple-licensed under Apache 2.0 with the LLVM Exception, +Apache 2.0, and MIT terms. + + +Longer version: + +Copyrights in the `rustix` project are retained by their contributors. +No copyright assignment is required to contribute to the `rustix` +project. + +Some files include code derived from Rust's `libstd`; see the comments in +the code for details. + +Except as otherwise noted (below and/or in individual files), `rustix` +is licensed under: + + - the Apache License, Version 2.0, with the LLVM Exception + or + + - the Apache License, Version 2.0 + or + , + - or the MIT license + or + , + +at your option. + + +---------------------------------------------------------------------------- +The following license (LICENSE-ISC) applies to: rustls 0.23.41, rustls-native-certs 0.8.4 +---------------------------------------------------------------------------- +ISC License (ISC) +Copyright (c) 2016, Joseph Birr-Pixton + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: rustls 0.23.41, rustls-native-certs 0.8.4 +---------------------------------------------------------------------------- +Copyright (c) 2016 Joseph Birr-Pixton + +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 following license (LICENSE) applies to: rustls-native-certs 0.8.4 +---------------------------------------------------------------------------- +Rustls is distributed under the following three licenses: + +- Apache License version 2.0. +- MIT license. +- ISC license. + +These are included as LICENSE-APACHE, LICENSE-MIT and LICENSE-ISC +respectively. You may use this software under the terms of any +of these licenses, at your option. + + +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: rustls-pki-types 1.14.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2023 Dirkjan Ochtman + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: rustls-pki-types 1.14.1 +---------------------------------------------------------------------------- +Copyright (c) 2023 Dirkjan Ochtman + +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 following license (LICENSE-MIT) applies to: rustls-platform-verifier 0.6.2 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2022 1Password + +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 following license (LICENSE) applies to: rustls-webpki 0.103.13 +---------------------------------------------------------------------------- +Except as otherwise noted, this project is licensed under the following +(ISC-style) terms: + +Copyright 2015 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +The files under third-party/chromium are licensed as described in +third-party/chromium/LICENSE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT.md) applies to: safe_arch 0.7.4 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2023 Daniel "Lokathor" Gee. + +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 (including the next paragraph) 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 following license (LICENSE-ZLIB.md) applies to: safe_arch 0.7.4, wide 0.7.33 +---------------------------------------------------------------------------- +Copyright (c) 2020 Daniel "Lokathor" Gee. + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: same-file 1.0.6, winapi-util 0.1.11 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2017 Andrew Gallant + +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 following license (LICENSE.md) applies to: schannel 0.1.29 +---------------------------------------------------------------------------- +Copyright (c) 2015 steffengy + +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 following license (LICENSE-MIT) applies to: scopeguard 1.2.0 +---------------------------------------------------------------------------- +Copyright (c) 2016-2019 Ulrik Sverdrup "bluss" and scopeguard developers + +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 following license (LICENSE) applies to: sdl3 0.18.4 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2013 Mozilla Foundation + +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 following license (LICENSE.md) applies to: sdl3-image-sys 0.6.4+SDL-image-3.4.4, sdl3-mixer-sys 0.6.3+SDL-mixer-3.2.4, sdl3-ttf-sys 0.6.1+SDL-ttf-3.2.2 +---------------------------------------------------------------------------- +zlib License + +(C) 2025 Maia S Ravn + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (LICENSE.md) applies to: sdl3-sys 0.6.6+SDL-3.4.10 +---------------------------------------------------------------------------- +zlib License + +(C) 2024-2025 Maia S Ravn + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: security-framework 3.7.0, security-framework-sys 2.17.0 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Steven Fackler + +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 following license (LICENSE-MIT) applies to: sha2 0.10.9 +---------------------------------------------------------------------------- +Copyright (c) 2006-2009 Graydon Hoare +Copyright (c) 2009-2013 Mozilla Foundation +Copyright (c) 2016 Artyom Pavlov + +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 following license (LICENSE) applies to: sharded-slab 0.1.7 +---------------------------------------------------------------------------- +Copyright (c) 2019 Eliza Weisman + +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 following license (LICENSE-APACHE) applies to: shlex 1.3.0, shlex 2.0.1 +---------------------------------------------------------------------------- +Copyright 2015 Nicholas Allegra (comex). + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: shlex 1.3.0, shlex 2.0.1 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Nicholas Allegra (comex). + +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 following license (LICENSE-MIT) applies to: signal-hook-registry 1.4.8 +---------------------------------------------------------------------------- +Copyright (c) 2017 tokio-jsonrpc developers + +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 following license (LICENSE.md) applies to: simd-adler32 0.3.9 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) [2021] [Marvin Countryman] + +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 following license (simple-icons.txt) applies to: Simple Icons (vendored, assets/os-icons) +---------------------------------------------------------------------------- +Simple Icons — brand icons (arch, nixos, debian, cachyos, nobara in assets/os-icons/) +are from Simple Icons +(https://simpleicons.org, https://github.com/simple-icons/simple-icons). + +The Simple Icons SVG path data is released under CC0 1.0 Universal (public domain +dedication), https://creativecommons.org/publicdomain/zero/1.0/ — no attribution +required; this notice is provided for provenance. + +Brand icons are trademarks of their respective owners and are used for +identification purposes only; their use does not imply endorsement. See +https://github.com/simple-icons/simple-icons/blob/develop/DISCLAIMER.md. + + +---------------------------------------------------------------------------- +The following license (COPYING) applies to: siphasher 1.0.3 +---------------------------------------------------------------------------- +Copyright 2012-2016 The Rust Project Developers. +Copyright 2016-2026 Frank Denis. + +Licensed under the Apache License, Version 2.0 or the MIT license +, at your +option. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: slab 0.4.12 +---------------------------------------------------------------------------- +Copyright (c) 2019 Carl Lerche + +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 following license (LICENSE-MIT) applies to: smallvec 1.15.2 +---------------------------------------------------------------------------- +Copyright (c) 2018 The Servo Project Developers + +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 following license (LICENSE) applies to: socket-pktinfo 0.4.0 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2025 Pixsper + +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 following license (LICENSE-MIT) applies to: spake2 0.4.0 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2017-2023 Brian Warner + +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 following license (LICENSE) applies to: spin 0.9.8 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2014 Mathijs van de Nes + +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 following license (LICENSE-MIT) applies to: stable_deref_trait 1.2.1 +---------------------------------------------------------------------------- +Copyright (c) 2017 Robert Grosse + +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 following license (LICENSE) applies to: strsim 0.11.1 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Danny Guo +Copyright (c) 2016 Titus Wormer +Copyright (c) 2018 Akash Kurdekar + +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 following license (LICENSE) applies to: strum 0.26.3, strum_macros 0.26.4 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2019 Peter Glotfelty + +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 following license (LICENSE) applies to: subtle 2.6.1 +---------------------------------------------------------------------------- +Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. +Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: synstructure 0.13.2 +---------------------------------------------------------------------------- +Copyright 2016 Nika Layzell + +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 following license (LICENSE-MIT) applies to: tar 0.4.46 +---------------------------------------------------------------------------- +Copyright (c) The tar-rs Project 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 following license (LICENSE-MIT) applies to: tempfile 3.27.0, xattr 1.6.1 +---------------------------------------------------------------------------- +Copyright (c) 2015 Steven Allen + +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 following license (LICENSE-MIT) applies to: time 0.3.51, time-core 0.1.9, time-macros 0.2.30 +---------------------------------------------------------------------------- +Copyright (c) Jacob Pratt et al. + +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 following license (LICENSE-MIT) applies to: tinytemplate 1.2.1 +---------------------------------------------------------------------------- +Copyright (c) 2019 Brook Heisler + +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 following license (LICENSE-MIT.md) applies to: tinyvec 1.11.0 +---------------------------------------------------------------------------- +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 following license (LICENSE-APACHE.md) applies to: tinyvec_macros 0.1.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Tomasz "Soveu" Marx + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT.md) applies to: tinyvec_macros 0.1.1 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2020 Soveu + +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 following license (LICENSE-ZLIB.md) applies to: tinyvec_macros 0.1.1 +---------------------------------------------------------------------------- +zlib License + +(C) 2020 Tomasz "Soveu" Marx + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: to_method 1.1.0 +---------------------------------------------------------------------------- +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. + + +---------------------------------------------------------------------------- +The following license (LICENSE) applies to: tokio 1.52.3 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) Tokio 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 following license (LICENSE) applies to: tokio-macros 2.7.0 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2019 Yoshua Wuyts +Copyright (c) Tokio 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 following license (LICENSE) applies to: tracing 0.1.44, tracing-attributes 0.1.31, tracing-core 0.1.36, tracing-log 0.2.0, tracing-subscriber 0.3.23 +---------------------------------------------------------------------------- +Copyright (c) 2019 Tokio 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 following license (LICENSE) applies to: typenum 1.20.1 +---------------------------------------------------------------------------- +MIT OR Apache-2.0 + + +---------------------------------------------------------------------------- +The following license (LICENSE-APACHE) applies to: typenum 1.20.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2014 Paho Lurie-Gregg + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: typenum 1.20.1 +---------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2014 Paho Lurie-Gregg + +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 following license (LICENSE-MIT) applies to: unarray 0.1.4 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) [year] [fullname] + +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 following license (LICENSE-UNICODE) applies to: unicode-ident 1.0.24 +---------------------------------------------------------------------------- +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 1991-2023 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + + +---------------------------------------------------------------------------- +The following license (COPYRIGHT) applies to: unicode-segmentation 1.13.3, unicode-width 0.2.2 +---------------------------------------------------------------------------- +Licensed under the Apache License, Version 2.0 + or the MIT +license , +at your option. All files in the project carrying such +notice may not be copied, modified, or distributed except +according to those terms. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: universal-hash 0.5.1 +---------------------------------------------------------------------------- +Copyright (c) 2019-2020 RustCrypto Developers + +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 following license (LICENSE.txt) applies to: untrusted 0.9.0 +---------------------------------------------------------------------------- +// Copyright 2015-2016 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: ureq 2.12.1 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2019 Martin Algesten + +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 following license (COPYRIGHT) applies to: utf8_iter 1.0.4 +---------------------------------------------------------------------------- +Copyright Mozilla Foundation + +Licensed under the Apache License (Version 2.0), or the MIT license, +(the "Licenses") at your option. You may not use this file except in +compliance with one of the Licenses. You may obtain copies of the +Licenses at: + + https://www.apache.org/licenses/LICENSE-2.0 + https://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software +distributed under the Licenses is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Licenses for the specific language governing permissions and +limitations under the Licenses. + +-- + +Test code is dedicated to the Public Domain when so designated (see +the individual files for PD/CC0-dedicated sections). + +-- + +The implementation for Utf8CharIndices was adapted from the +CharIndices implementation of the Rust standard library at revision +ab32548539ec38a939c1b58599249f3b54130026 +(https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/library/core/src/str/iter.rs). + +Excerpt from https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/COPYRIGHT , +which refers to +https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/LICENSE-APACHE +and +https://github.com/rust-lang/rust/blob/ab32548539ec38a939c1b58599249f3b54130026/LICENSE-MIT +: + +For full authorship information, see the version control history or +https://thanks.rust-lang.org + +Except as otherwise noted (below and/or in individual files), Rust is +licensed under the Apache License, Version 2.0 or + or the MIT license + or , at your option. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: utf8_iter 1.0.4 +---------------------------------------------------------------------------- +Copyright Mozilla Foundation + +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 following license (LICENSE-MIT) applies to: utf8parse 0.2.2 +---------------------------------------------------------------------------- +Copyright (c) 2016 Joe Wilm + +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 following license (LICENSE-MIT) applies to: vcpkg 0.2.15 +---------------------------------------------------------------------------- +Copyright (c) 2017 Jim McGrath + +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 following license (LICENSE) applies to: version-compare 0.2.1 +---------------------------------------------------------------------------- +Copyright (c) 2017 Tim Visée + +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 following license (LICENSE-MIT) applies to: version_check 0.9.5 +---------------------------------------------------------------------------- +The MIT License (MIT) +Copyright (c) 2017-2018 Sergio Benitez + +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 following license (LICENSE.md) applies to: volk (vendored, crates/pyrowave-sys) +---------------------------------------------------------------------------- +Copyright (c) 2018-2026 Arseny Kapoulkine + +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 following license (LICENSE.md) applies to: Vulkan-Headers (vendored, crates/pyrowave-sys) +---------------------------------------------------------------------------- +Copyright 2015-2023 The Khronos Group Inc. + +Files in this repository fall under one of these licenses: + +- `Apache-2.0` +- `MIT` + +Note: With the exception of `parse_dependency.py` the files using `MIT` license +also fall under `Apache-2.0`. Example: + +``` +SPDX-License-Identifier: Apache-2.0 OR MIT +``` + +Full license text of these licenses is available at: + + * Apache-2.0: https://opensource.org/licenses/Apache-2.0 + * MIT: https://opensource.org/licenses/MIT + + +---------------------------------------------------------------------------- +The following license (LICENSE.txt) applies to: wasapi 0.23.0 +---------------------------------------------------------------------------- +Copyright (c) 2020 Henrik Enquist + +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 following license (LICENSE-APACHE) applies to: web-time 1.1.0 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 dAxpeDDa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: web-time 1.1.0 +---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2023 dAxpeDDa + +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 following license (LICENSE) applies to: webpki-root-certs 1.0.8, webpki-roots 0.26.11, webpki-roots 1.0.8 +---------------------------------------------------------------------------- +# Community Data License Agreement - Permissive - Version 2.0 + +This is the Community Data License Agreement - Permissive, Version +2.0 (the "agreement"). Data Provider(s) and Data Recipient(s) agree +as follows: + +## 1. Provision of the Data + +1.1. A Data Recipient may use, modify, and share the Data made +available by Data Provider(s) under this agreement if that Data +Recipient follows the terms of this agreement. + +1.2. This agreement does not impose any restriction on a Data +Recipient's use, modification, or sharing of any portions of the +Data that are in the public domain or that may be used, modified, +or shared under any other legal exception or limitation. + +## 2. Conditions for Sharing Data + +2.1. A Data Recipient may share Data, with or without modifications, so +long as the Data Recipient makes available the text of this agreement +with the shared Data. + +## 3. No Restrictions on Results + +3.1. This agreement does not impose any restriction or obligations +with respect to the use, modification, or sharing of Results. + +## 4. No Warranty; Limitation of Liability + +4.1. All Data Recipients receive the Data subject to the following +terms: + +THE DATA IS PROVIDED ON AN "AS IS" BASIS, WITHOUT REPRESENTATIONS, +WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED +INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, +NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +NO DATA PROVIDER SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING +WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE DATA OR RESULTS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +## 5. Definitions + +5.1. "Data" means the material received by a Data Recipient under +this agreement. + +5.2. "Data Provider" means any person who is the source of Data +provided under this agreement and in reliance on a Data Recipient's +agreement to its terms. + +5.3. "Data Recipient" means any person who receives Data directly +or indirectly from a Data Provider and agrees to the terms of this +agreement. + +5.4. "Results" means any outcome obtained by computational analysis +of Data, including for example machine learning models and models' +insights. + + +---------------------------------------------------------------------------- +The following license (license-apache-2.0) applies to: windows 0.62.2, windows-canvas 0.0.0, windows-collections 0.3.2, windows-composition 0.0.0, windows-core 0.62.2, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-numerics 0.3.1, windows-reactor 0.0.0, windows-reactor-setup 0.0.0, windows-reference 0.1.0, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.45.0, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.60.2, windows-sys 0.61.2, windows-targets 0.42.2, windows-targets 0.52.6, windows-targets 0.53.5, windows-threading 0.2.1, windows-time 0.1.0, windows-window 0.0.0, windows_aarch64_gnullvm 0.42.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_gnullvm 0.53.1, windows_aarch64_msvc 0.42.2, windows_aarch64_msvc 0.52.6, windows_aarch64_msvc 0.53.1, windows_i686_gnu 0.42.2, windows_i686_gnu 0.52.6, windows_i686_gnu 0.53.1, windows_i686_gnullvm 0.52.6, windows_i686_gnullvm 0.53.1, windows_i686_msvc 0.42.2, windows_i686_msvc 0.52.6, windows_i686_msvc 0.53.1, windows_x86_64_gnu 0.42.2, windows_x86_64_gnu 0.52.6, windows_x86_64_gnu 0.53.1, windows_x86_64_gnullvm 0.42.2, windows_x86_64_gnullvm 0.52.6, windows_x86_64_gnullvm 0.53.1, windows_x86_64_msvc 0.42.2, windows_x86_64_msvc 0.52.6, windows_x86_64_msvc 0.53.1 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (license-mit) applies to: windows 0.62.2, windows-canvas 0.0.0, windows-collections 0.3.2, windows-composition 0.0.0, windows-core 0.62.2, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-numerics 0.3.1, windows-reactor 0.0.0, windows-reactor-setup 0.0.0, windows-reference 0.1.0, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.45.0, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.60.2, windows-sys 0.61.2, windows-targets 0.42.2, windows-targets 0.52.6, windows-targets 0.53.5, windows-threading 0.2.1, windows-time 0.1.0, windows-window 0.0.0, windows_aarch64_gnullvm 0.42.2, windows_aarch64_gnullvm 0.52.6, windows_aarch64_gnullvm 0.53.1, windows_aarch64_msvc 0.42.2, windows_aarch64_msvc 0.52.6, windows_aarch64_msvc 0.53.1, windows_i686_gnu 0.42.2, windows_i686_gnu 0.52.6, windows_i686_gnu 0.53.1, windows_i686_gnullvm 0.52.6, windows_i686_gnullvm 0.53.1, windows_i686_msvc 0.42.2, windows_i686_msvc 0.52.6, windows_i686_msvc 0.53.1, windows_x86_64_gnu 0.42.2, windows_x86_64_gnu 0.52.6, windows_x86_64_gnu 0.53.1, windows_x86_64_gnullvm 0.42.2, windows_x86_64_gnullvm 0.52.6, windows_x86_64_gnullvm 0.53.1, windows_x86_64_msvc 0.42.2, windows_x86_64_msvc 0.52.6, windows_x86_64_msvc 0.53.1 +---------------------------------------------------------------------------- +MIT License + + Copyright (c) Microsoft Corporation. + + 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 following license (LICENSE-MIT) applies to: winnow 0.7.15, winnow 1.0.3 +---------------------------------------------------------------------------- +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 following license (LICENSE) applies to: winreg 0.56.0 +---------------------------------------------------------------------------- +Copyright (c) 2015 Igor Shaula + +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 following license (LICENSE) applies to: winresource 0.1.31 +---------------------------------------------------------------------------- +Copyright 2016 Max Resch + +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 following license (LICENSE-APACHE) applies to: zerocopy 0.7.35, zerocopy 0.8.52, zerocopy-derive 0.7.35, zerocopy-derive 0.8.52 +---------------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 The Fuchsia Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------------------------------------------------------------------- +The following license (LICENSE-BSD) applies to: zerocopy 0.7.35, zerocopy 0.8.52, zerocopy-derive 0.7.35, zerocopy-derive 0.8.52 +---------------------------------------------------------------------------- +Copyright 2019 The Fuchsia Authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------------------------------------------------------------------- +The following license (LICENSE-MIT) applies to: zerocopy 0.7.35, zerocopy 0.8.52, zerocopy-derive 0.7.35, zerocopy-derive 0.8.52 +---------------------------------------------------------------------------- +Copyright 2023 The Fuchsia Authors + +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 following license (LICENSE-MIT) applies to: zeroize 1.9.0 +---------------------------------------------------------------------------- +Copyright (c) 2018-2026 The RustCrypto Project Developers + +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. + diff --git a/clients/windows/packaging/README.md b/clients/windows/packaging/README.md index 2af63acc..e91d3138 100644 --- a/clients/windows/packaging/README.md +++ b/clients/windows/packaging/README.md @@ -9,8 +9,9 @@ touches the client (canary) and on `vX.Y.Z` release tags (stable) — see **Two architectures, one x64 runner.** Both `x64` and `arm64` packages are produced off the single x64 Windows runner — `x86_64-pc-windows-msvc` builds natively, `aarch64-pc-windows-msvc` is -cross-compiled (the x64 MSVC toolset ships the ARM64 cross compiler; the matrix points `FFMPEG_DIR` -at the runner's ARM64 FFmpeg tree, `C:\Users\Public\ffmpeg-arm64`). Artifacts are arch-suffixed +cross-compiled (the x64 MSVC toolset ships the ARM64 cross compiler; since M10 nothing in the +package links FFmpeg, so neither arch needs a per-arch `FFMPEG_DIR` tree staged on the runner — +one less thing the ARM64 leg can be missing). Artifacts are arch-suffixed (`..._x64.msix` / `..._arm64.msix`, each with its matching `.cer`); `pack-msix.ps1 -Arch x64|arm64` stamps the manifest `ProcessorArchitecture` and names the output. See [`windows.yml`](../../../.gitea/workflows/windows.yml) for the cross-build rationale. @@ -25,10 +26,17 @@ stamps the manifest `ProcessorArchitecture` and names the output. See | `punktfunk-session.exe` | the release build — the Vulkan session client the shell spawns for every stream (sibling resolution, `src/spawn.rs`). Skia links statically; `vulkan-1.dll` is a GPU-driver component, never bundled. ARM64 builds it `--no-default-features` (no Skia console UI) until rust-skia ships aarch64-pc-windows-msvc prebuilts | | `Microsoft.WindowsAppRuntime.Bootstrap.dll`, `resources.pri` | staged by the client's `build.rs` via `windows-reactor-setup::as_framework_dependent()` | | `SDL3.dll` | auto-staged by the `sdl3` crate | -| `avcodec/avformat/avutil/swscale/swresample/...-*.dll` | `FFMPEG_DIR\bin` | +| `licenses\*` | the project's MIT/Apache texts + the generated `THIRD-PARTY-NOTICES.txt` (MSIX has no installer EULA page, so attribution ships as files) | | `Assets\*.png` | checked-in tile/store logos (rasterized from `packaging/flatpak/io.unom.Punktfunk.svg`) | | `AppxManifest.xml` | the template here, with `{VERSION}`/`{PUBLISHER}` substituted | +**No FFmpeg DLLs.** The client decodes natively since M10 (`pf-vkdecode` / `pf-dxvadec` / +OpenH264+rav1d — punktfunk-planning `design/client-native-decode.md` §6), so nothing here +link-imports `libav*` and the wildcard `avcodec/avformat/avutil/swscale/swresample-*.dll` copy is +gone, along with the FFmpeg LGPL notice that accompanied it — shipping that notice now would claim +a dependency the package doesn't have. The **host** installer is unchanged: +`packaging/windows/pack-host-installer.ps1` still ships those DLLs for its AMF/QSV encode path. + ### Why an "unpackaged" WinUI app packages cleanly `main` calls `windows_reactor::bootstrap()`, which runs `MddBootstrapInitialize2` with @@ -87,8 +95,7 @@ cargo build --release -p punktfunk-client-windows --target x86_64-pc-windows-msv pwsh -File clients/windows/packaging/pack-msix.ps1 ` -Version 0.2.0.0 -TargetDir C:\t\x86_64-pc-windows-msvc\release -OutDir C:\t\msix -# arm64 (cross-compiled; point FFMPEG_DIR at the ARM64 tree) -$env:FFMPEG_DIR = 'C:\Users\Public\ffmpeg-arm64' +# arm64 (cross-compiled; no extra environment — the client links no FFmpeg) cargo build --release -p punktfunk-client-windows --target aarch64-pc-windows-msvc pwsh -File clients/windows/packaging/pack-msix.ps1 ` -Version 0.2.0.0 -Arch arm64 -TargetDir C:\t\aarch64-pc-windows-msvc\release -OutDir C:\t\msix diff --git a/clients/windows/packaging/pack-msix.ps1 b/clients/windows/packaging/pack-msix.ps1 index 8fccc866..807ee550 100644 --- a/clients/windows/packaging/pack-msix.ps1 +++ b/clients/windows/packaging/pack-msix.ps1 @@ -4,9 +4,14 @@ .DESCRIPTION Builds a packaging layout from a release `cargo build` output (exe + the reactor/SDL3 auto-staged - DLLs + resources.pri + FFmpeg DLLs + the checked-in Assets + the manifest), runs makeappx, and + DLLs + resources.pri + the checked-in Assets + the manifest), runs makeappx, and signs with signtool. Idempotent; safe to re-run. + NO FFmpeg DLLs since M10 (design/client-native-decode.md §6): the client decodes natively + (pf-vkdecode / pf-dxvadec / openh264+rav1d) and link-imports no libav* at all, so the + wildcard copy and its LGPL notice are gone with it. The HOST installer is unchanged — + packaging/windows/pack-host-installer.ps1 still ships them for its amf-qsv encode path. + Signing cert precedence: 1. -PfxBase64 / -PfxPassword (a real or shared code-signing cert, e.g. from CI secrets) — the cert's subject DN MUST match -Publisher (which is stamped into the manifest Identity). @@ -22,8 +27,7 @@ .EXAMPLE # x64 (default arch): pwsh -File pack-msix.ps1 -Version 0.2.137.0 -TargetDir C:\t\x86_64-pc-windows-msvc\release -OutDir C:\t\msix - # arm64 (point -TargetDir + FFMPEG_DIR at the ARM64 build/tree): - $env:FFMPEG_DIR='C:\Users\Public\ffmpeg-arm64' + # arm64 (point -TargetDir at the ARM64 build): pwsh -File pack-msix.ps1 -Version 0.2.137.0 -Arch arm64 -TargetDir C:\t-a64\aarch64-pc-windows-msvc\release -OutDir C:\t-a64\msix #> [CmdletBinding()] @@ -31,7 +35,6 @@ param( [Parameter(Mandatory = $true)][string]$Version, # 4-part numeric, e.g. 0.2.137.0 [Parameter(Mandatory = $true)][string]$TargetDir, # cargo --release output dir (has the exe) [ValidateSet('x64', 'arm64')][string]$Arch = 'x64', # package ProcessorArchitecture + artifact suffix - [string]$FfmpegBin = $(if ($env:FFMPEG_DIR) { Join-Path $env:FFMPEG_DIR 'bin' } else { 'C:\Users\Public\ffmpeg\bin' }), [string]$OutDir = (Join-Path $TargetDir 'msix'), [string]$Publisher = 'CN=unom', # MUST equal the signing cert subject DN [string]$PfxBase64 = $env:MSIX_CERT_PFX_B64, # optional: base64 of a code-signing .pfx @@ -83,28 +86,32 @@ foreach ($f in $required) { Copy-Item $src (Join-Path $layout $f) -Force } -# FFmpeg runtime DLLs (the exe link-imports the decode set; copy them all — small and correct). -# These are unmodified BtbN *lgpl-shared* builds, linked dynamically (replaceable DLLs) — FFmpeg is -# used under the LGPL v2.1+; the license text + notice ship in licenses\ below. -$ff = Get-ChildItem -Path $FfmpegBin -Filter *.dll -ErrorAction SilentlyContinue -if (-not $ff) { throw "no FFmpeg DLLs in $FfmpegBin" } -$ff | ForEach-Object { Copy-Item $_.FullName (Join-Path $layout $_.Name) -Force } - -# license/attribution payload (MSIX has no installer EULA page, so ship them as files): FFmpeg's LGPL -# notice + license text, the project's own MIT/Apache texts, and the generated third-party notices. +# license/attribution payload (MSIX has no installer EULA page, so ship them as files): the +# project's own MIT/Apache texts plus the generated third-party notices, which is where every +# vendored/statically-linked dependency's attribution lives (openh264 BSD-2, rav1d BSD-2, …). +# +# The FFmpeg LGPL notice + license texts that used to be copied here went with the DLLs at M10: +# nothing in this package links libav* any more, so shipping an LGPL notice would be claiming a +# dependency that is not there. +# +# For the same reason the notices come from clients/windows/ and NOT from the repo root: the root +# file is workspace-wide, it is what the HOST ships out of, and it still lists ffmpeg-next plus the +# full FFmpeg licence text. The client-scoped file (same generator, `--packages +# punktfunk-client-windows,punktfunk-client-session,punktfunk-cli`) is the one that describes what +# is actually inside this .msix — and it is the same file the app's Licenses page shows. $licDir = Join-Path $layout 'licenses' New-Item -ItemType Directory -Force -Path $licDir | Out-Null $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path -Copy-Item (Join-Path $repoRoot 'packaging\windows\licenses\FFmpeg-LGPL-NOTICE.txt') $licDir -Force -ErrorAction SilentlyContinue -foreach ($n in @('THIRD-PARTY-NOTICES.txt', 'LICENSE-MIT', 'LICENSE-APACHE')) { +$clientRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +foreach ($n in @('LICENSE-MIT', 'LICENSE-APACHE')) { $p = Join-Path $repoRoot $n if (Test-Path $p) { Copy-Item $p $licDir -Force } } -$ffRoot = Split-Path $FfmpegBin -Parent -foreach ($lic in @('LICENSE.txt', 'LICENSE', 'COPYING.LGPLv2.1', 'COPYING.LGPLv3', 'COPYING.txt')) { - $p = Join-Path $ffRoot $lic - if (Test-Path $p) { Copy-Item $p $licDir -Force } +$notices = Join-Path $clientRoot 'THIRD-PARTY-NOTICES.txt' +if (-not (Test-Path $notices)) { + throw "missing $notices — run scripts/gen-third-party-notices.sh (it generates the per-client copies)" } +Copy-Item $notices $licDir -Force # tile/store assets Copy-Item (Join-Path $assets '*') (Join-Path $layout 'Assets') -Force diff --git a/clients/windows/src/app/licenses.rs b/clients/windows/src/app/licenses.rs index 03df1cd6..95fa180a 100644 --- a/clients/windows/src/app/licenses.rs +++ b/clients/windows/src/app/licenses.rs @@ -12,9 +12,15 @@ const APP_LICENSE: &str = concat!( "\n\n================================ Apache-2.0 ================================\n\n", include_str!("../../../../LICENSE-APACHE"), ); -/// Third-party software notices for the linked Rust crates (generated by -/// scripts/gen-third-party-notices.sh; the MSIX also ships this under licenses/). -const THIRD_PARTY_NOTICES: &str = include_str!("../../../../THIRD-PARTY-NOTICES.txt"); +/// Third-party software notices for the Rust crates THIS CLIENT links — the shell, the +/// session streamer and the headless CLI (generated by scripts/gen-third-party-notices.sh; +/// the MSIX ships the same file under licenses/). +/// +/// Deliberately the client-scoped file and not the workspace-wide one at the repo root: +/// that root file is the HOST's, it still carries `ffmpeg-next` and the full FFmpeg licence +/// text — and after M10 this app bundles no FFmpeg at all, so printing that attribution +/// three lines under a card saying so would be a false statement to the user's face. +const THIRD_PARTY_NOTICES: &str = include_str!("../../THIRD-PARTY-NOTICES.txt"); pub(crate) fn licenses_page(ctx: &Arc, set_screen: &AsyncSetState) -> Element { let back_btn = button("Back").accent().icon(Symbol::Back).on_click({ @@ -46,9 +52,10 @@ pub(crate) fn licenses_page(ctx: &Arc, set_screen: &AsyncSetState String { } } /// Decode backend presets: `(stored value, display label)`. -// A stored legacy "hardware" (the D3D11VA era) matches no preset, so the combo shows -// Automatic — which is exactly how the session's decoder chain reads that value. +// A stored legacy value that matches no preset (the D3D11VA-era "hardware", and since M10 +// the bare "vulkan"/"d3d11va" that named libavcodec's rungs) shows as Automatic — which is +// how the session's ladder reads "hardware", and near enough for the other two, which +// `pf_client_core::video::migrate_decoder_pref` maps onto the entries below anyway. const DECODERS: &[(&str, &str)] = &[ ("auto", "Automatic (GPU, fall back to CPU)"), - ("vulkan", "Hardware (Vulkan Video)"), - ("d3d11va", "Hardware (Direct3D 11 / DXVA)"), + ("native-vulkan", "Hardware (Vulkan Video)"), + ("native-d3d11va", "Hardware (Direct3D 11 / DXVA)"), ("software", "Software (CPU)"), ]; /// Audio channel presets: `(channel count, display label)`. The host clamps to what it can @@ -862,7 +864,11 @@ pub(crate) fn settings_page( ); // --- Video ----------------------------------------------------------------------------- - let (dec_names, dec_i) = presets(DECODERS, |v| *v == s.decoder); + // Migrated for the LOOKUP only (the store is left alone): a pre-M10 settings file + // holds `vulkan`/`d3d11va`, which match no preset — the combo would show Automatic and + // a save would silently rewrite the user's hardware preference to `auto`. + let stored_decoder = pf_client_core::video::migrate_decoder_pref(&s.decoder); + let (dec_names, dec_i) = presets(DECODERS, |v| *v == stored_decoder); let decoder_combo = setting_combo(ctx, scope, (rev, set_rev), dec_names, dec_i, |s, i| { s.decoder = DECODERS[i].0.to_string(); }); diff --git a/clients/windows/src/app/stream.rs b/clients/windows/src/app/stream.rs index d3cc87bb..46ad3222 100644 --- a/clients/windows/src/app/stream.rs +++ b/clients/windows/src/app/stream.rs @@ -81,8 +81,13 @@ pub(crate) fn session_page(ctx: &Arc, hud: &HudSample) -> Element .map(str::trim) .filter(|c| !c.is_empty()) .map(|c| { + // The `stats:` decode-path tags (see pf-client-core's session + // pump). M10 removed the `vulkan`/`vaapi`/`d3d11va` tags with their + // rungs; a hardware rung is now always a `native-*` one. let kind = match c { - "vulkan" | "vaapi" => Pill::Good, + "native-vulkan" | "native-vaapi" | "native-d3d11va" | "pyrowave" => { + Pill::Good + } "software" => Pill::Info, _ => Pill::Neutral, }; diff --git a/clients/windows/src/probe.rs b/clients/windows/src/probe.rs index b574bb37..5bbd2b61 100644 --- a/clients/windows/src/probe.rs +++ b/clients/windows/src/probe.rs @@ -6,26 +6,27 @@ //! over the real data plane, so it stays here. [`decodable_codecs`] rode along for the same //! reason — the probe connect still advertises which codecs this client can decode. -use ffmpeg_next as ffmpeg; use punktfunk_core::client::NativeClient; use punktfunk_core::config::{CompositorPref, GamepadPref, Mode}; use std::time::{Duration, Instant}; -/// The `quic` codec bitfield this client can decode — whatever FFmpeg has a decoder for (HEVC/H.264 -/// always; AV1 when built in). Advertised to the host so it never emits a codec we can't decode. +/// The `quic` codec bitfield this client can decode. Advertised to the host so it never emits +/// a codec we can't decode. +/// +/// It is pf-client-core's [`decodable_codecs`](pf_client_core::video::decodable_codecs) — +/// the codecs the SESSION BINARY's rungs speak, which is the process that actually decodes. +/// This shell used to walk libavcodec's registry (`ffmpeg::decoder::find` per id) for the +/// same answer; that was wrong in two ways even before M10 deleted the dependency. It +/// described the decoders in THIS process, which decodes nothing, and it answered "a +/// decoder exists" where the question is "a rung can keep up" — the AV1-on-CPU promise +/// `decodable_codecs_for` exists to refuse. +/// +/// ⚠ Deliberately the DEVICE-FREE answer ([`decodable_codecs`], not +/// `decodable_codecs_for`): this connect creates no presenter and has no `VulkanDecodeDevice` +/// to gate AV1 on, and it decodes nothing — the codec it advertises is never exercised. A +/// real session's Hello is built in the session binary, with the device in hand. pub fn decodable_codecs() -> u8 { - let _ = ffmpeg::init(); - let mut bits = 0u8; - for (id, bit) in [ - (ffmpeg::codec::Id::HEVC, punktfunk_core::quic::CODEC_HEVC), - (ffmpeg::codec::Id::H264, punktfunk_core::quic::CODEC_H264), - (ffmpeg::codec::Id::AV1, punktfunk_core::quic::CODEC_AV1), - ] { - if ffmpeg::decoder::find(id).is_some() { - bits |= bit; - } - } - bits + pf_client_core::video::decodable_codecs() } /// Blocking speed-test probe (the GUI's per-host "Test" and the `--headless --speed-test` CLI): diff --git a/compliance/sbom/manual-components.cdx.json b/compliance/sbom/manual-components.cdx.json index ba80fbb8..ff90d43f 100644 --- a/compliance/sbom/manual-components.cdx.json +++ b/compliance/sbom/manual-components.cdx.json @@ -46,8 +46,8 @@ { "type": "library", "name": "FFmpeg", - "version": "7.x/8.x (system-provided on Linux; replaceable DLLs bundled with the Windows packages)", - "description": "Dynamically linked libav* decode/encode; LGPL notice at packaging/windows/licenses/FFmpeg-LGPL-NOTICE.txt", + "version": "7.x/8.x (HOST only \u2014 system-provided on Linux; replaceable DLLs bundled with the Windows host installer)", + "description": "Dynamically linked libav* ENCODE for the host (pf-encode: NVENC-libav, VAAPI, AMF/QSV); LGPL notice at packaging/windows/licenses/FFmpeg-LGPL-NOTICE.txt. No punktfunk CLIENT links FFmpeg since M10 \u2014 client decode is Vulkan Video / DXVA / VAAPI / VideoToolbox / MediaCodec with openh264 + rav1d as the CPU floor.", "licenses": [{ "license": { "id": "LGPL-2.1-or-later" } }], "externalReferences": [{ "type": "website", "url": "https://ffmpeg.org" }] }, diff --git a/crates/pf-bitstream/Cargo.toml b/crates/pf-bitstream/Cargo.toml new file mode 100644 index 00000000..fd565e38 --- /dev/null +++ b/crates/pf-bitstream/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "pf-bitstream" +description = "Client-side bitstream layer for native decode: AU parsing, POC/DPB/reference derivation and per-AU DecodePlans (H.264/HEVC/AV1) on the vendored cros-codecs parsers — the layer libavcodec used to be (design/client-native-decode.md §3.1)" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true + +[dependencies] +cros-codecs = { path = "vendor/cros-codecs" } +tracing = "0.1" + +[lints] +workspace = true diff --git a/crates/pf-bitstream/src/av1.rs b/crates/pf-bitstream/src/av1.rs new file mode 100644 index 00000000..2a59d881 --- /dev/null +++ b/crates/pf-bitstream/src/av1.rs @@ -0,0 +1,1101 @@ +//! AV1 access-unit planning — M7's foundation, and the third planner in this crate. +//! +//! Same contract as [`crate::h264`] and [`crate::h265`]: one access unit in, one +//! [`AuPlan`] out, carrying everything a hardware backend needs to submit the frame +//! and everything the client needs to manage surfaces. The vendored cros-codecs +//! parser does the bitstream reading; this module owns the reference ledger, the +//! output bookkeeping and the concealment posture. +//! +//! # AV1's reference model is simpler than H.264's, and explicit +//! +//! There is no sliding window, no MMCO, no POC derivation and no bumping process. +//! There are **eight numbered reference slots**, and each frame says outright what it +//! does with them: +//! +//! * `ref_frame_idx[0..7]` names the slots this frame READS (seven references, which +//! may repeat a slot) — and its POSITION is the AV1 reference name, which is why +//! [`AuPlan::refs`] is name-indexed and a lost reference leaves a hole; +//! * `refresh_frame_flags` is an eight-bit mask naming the slots this frame WRITES +//! once decoded; +//! * `show_frame` says whether the frame displays now, and `show_existing_frame` +//! displays a slot's existing contents with no decode at all. +//! +//! That means this planner's job is bookkeeping rather than derivation, and the whole +//! of it is checkable against the stream: a frame that names a slot holding nothing is +//! a lost reference, full stop, with no spec process that might legitimately have +//! emptied it. +//! +//! # What is deliberately NOT here +//! +//! The per-backend conversions. Vulkan's `StdVideoDecodeAV1PictureInfo`, DXVA's +//! `DXVA_PicParams_AV1` and libva's `VAPictureParameterBufferAV1` are three more +//! spellings of the same plan, and they belong in `pf-vkdecode` / `pf-dxvadec` / +//! `pf-vaadec` beside their H.264 and H.265 siblings — for the reason the HEVC +//! reference-set disaster taught: the three APIs disagree about what a "reference +//! list" even indexes, and each conversion is where its own convention is written +//! down and tested. + +use std::ops::Range; +use std::rc::Rc; + +use cros_codecs::codec::av1::parser::FrameHeaderObu; +use cros_codecs::codec::av1::parser::ObuAction; +use cros_codecs::codec::av1::parser::ParsedObu; +use cros_codecs::codec::av1::parser::Parser; +use cros_codecs::codec::av1::parser::SequenceHeaderObu; + +use crate::h264::ColourDescription; + +/// The parsed types a backend conversion names, re-exported so each names them +/// through this module rather than reaching into the vendored crate — the same +/// courtesy [`crate::h264`] does with its `Sps`/`Pps`. +pub use cros_codecs::codec::av1::parser::FrameHeaderObu as ParsedFrameHeader; +pub use cros_codecs::codec::av1::parser::FrameType; +pub use cros_codecs::codec::av1::parser::SequenceHeaderObu as ParsedSequenceHeader; + +/// A stable identity for a decoded picture, the same currency the other two planners +/// deal in: the backends key their surface tables by it and never by slot index. +pub type PicId = u64; + +/// AV1's reference slot count (`NUM_REF_FRAMES`). +pub const NUM_REF_SLOTS: usize = 8; + +/// References a single inter frame may name (`REFS_PER_FRAME`). +pub const REFS_PER_FRAME: usize = 7; + +/// One reference: which picture, which slot holds it, and what that picture's OWN +/// frame header said. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RefPic { + pub id: PicId, + /// The slot index, 0..8. Backends that address references by slot (Vulkan) want + /// this; backends that address them by surface resolve `id` through their own + /// table. + pub slot: u8, + /// The reference's own header state — see [`RefState`], and note it is the + /// REFERENCE's, never the frame being decoded. + pub state: RefState, +} + +/// What one picture's own frame header said, kept for as long as that picture can +/// serve as a reference. +/// +/// Two of the three backends have a per-REFERENCE structure — Vulkan's +/// `StdVideoDecodeAV1ReferenceInfo` and DXVA's `DXVA_PicEntry_AV1` — and each of them +/// asks questions about the reference picture, not about the frame being decoded. +/// Answering them from the CURRENT header is the shape of a whole bug class: it +/// compiles, it looks like the fields are filled, and the hardware predicts from a +/// picture it has been told the wrong things about. So the answers are recorded once, +/// where they are unambiguous — when the picture is STORED into its slots — and travel +/// on the slot. +/// +/// ⚠ **VA-API has no such structure at all.** An earlier revision of this comment +/// named a `VAReferenceFrameAV1`; libva 2.23.0 does not declare one (measured, `grep +/// -c` is 0). Its `ref_frame_map` is a bare array of `VASurfaceID`, and a driver reads +/// every per-reference answer off the surface — which is why the same revision's claim +/// about [`Self::upscaled_width`] below was wrong too, and why an invented type name +/// is worth correcting rather than leaving as harmless prose: it is what sent somebody +/// looking for a field to fill. +/// +/// [`Av1Planner::refresh_slots`] is the only writer, and [`RefState::of`] the only +/// way to build one. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RefState { + /// The picture's `OrderHint`. + pub order_hint: u32, + /// The picture's own `UpscaledWidth` — the post-superres coded width it was + /// decoded at. + /// + /// AV1 lets every frame pick its own size up to the sequence maximum without a + /// key frame, and a decoder predicting from a differently-sized reference + /// SCALES the motion (7.11.3.3 derives `xStep` from `RefUpscaledWidth[refIdx]`). + /// So **DXVA** asks for it per reference: `DXVA_PicEntry_AV1` has `width` and + /// `height` fields, and answering them from the CURRENT header makes every + /// scaled prediction read as unscaled. + /// + /// ⚠ **VA-API does not.** `VADecPictureParameterBufferAV1` carries no + /// `ref_frame_width`/`ref_frame_height` at all (measured against libva 2.23.0's + /// `va_dec_av1.h`: `grep -c ref_frame_width` is 0), because its `ref_frame_map` + /// holds `VASurfaceID`s and a driver reads each reference's dimensions off the + /// surface. An earlier revision of this comment claimed otherwise. + pub upscaled_width: u32, + /// The picture's own `FrameHeight`, on the same terms as + /// [`Self::upscaled_width`]. (There is no superres in the vertical direction, + /// so this is simply the reference's coded height.) + pub frame_height: u32, + /// The picture's own frame type — a reference is routinely a different type + /// from the frame reading it. + pub frame_type: FrameType, + /// `RefFrameSignBias` packed the way Vulkan wants it: bit `i` set where + /// `RefFrameSignBias[i]` is 1, `i` being an AV1 reference frame index + /// (`INTRA_FRAME` = 0, `LAST_FRAME` = 1 … `ALTREF_FRAME` = 7). + /// + /// This is what tells a decoder that a reference lies in the FUTURE, so it + /// drives compound prediction and motion-field projection. All-zero means + /// "every reference is in the past", which for any stream with hidden ALTREFs + /// — the ordinary case — is wrong rather than merely conservative. + pub ref_frame_sign_bias: u8, + /// The picture's own `OrderHints[]`, which become `SavedOrderHints` once it is + /// a reference (7.20). Indexed by AV1 reference frame index, as above. + pub saved_order_hints: [u32; NUM_REF_SLOTS], + pub disable_frame_end_update_cdf: bool, + pub segmentation_enabled: bool, +} + +impl RefState { + /// Read one frame header's reference-relevant state. + /// + /// Called by the planner when a picture is stored, and by a backend for the + /// picture it is about to decode (which activates a slot, so it needs the same + /// answers). One function so the two can never drift. + pub fn of(header: &FrameHeaderObu) -> RefState { + // ⚠⚠ INDEX SHIFT, and it is the vendored parser's, not ours. + // + // AV1 7.8 writes `RefFrameSignBias[ refFrame ]` with `refFrame = + // LAST_FRAME + i`, and libavcodec's `av1dec.c` (`order_hint_info`) does + // exactly that — so `RefFrameSignBias` bit 1 is LAST_FRAME. The vendored + // cros-codecs parser writes `fh.ref_frame_sign_bias[i]` in the SAME loop + // body where it writes `fh.order_hints[ref_frame]`, so its array is + // shifted one down: index 0 holds LAST_FRAME's bias and index 7 is never + // written. (Its own VP9 parser gets this right, which is how the AV1 one + // reads as a slip rather than a convention.) + // + // Corrected here rather than in the vendored tree so the pin stays clean, + // and pinned by `the_sign_bias_mask_is_spec_indexed_not_parser_indexed`, + // which recomputes the bias from `order_hints` through the parser's own + // `get_relative_dist`. + let mut ref_frame_sign_bias = 0u8; + for (i, biased) in header + .ref_frame_sign_bias + .iter() + .take(REFS_PER_FRAME) + .enumerate() + { + if *biased { + ref_frame_sign_bias |= 1 << (i + 1); + } + } + RefState { + order_hint: header.order_hint, + upscaled_width: header.upscaled_width, + frame_height: header.frame_height, + frame_type: header.frame_type, + ref_frame_sign_bias, + saved_order_hints: header.order_hints, + disable_frame_end_update_cdf: header.disable_frame_end_update_cdf, + segmentation_enabled: header.segmentation_params.segmentation_enabled, + } + } +} + +/// One CDEF secondary strength as every hardware API wants it: the **coded +/// two-bit syntax element**, `0..=3`. +/// +/// ⚠⚠ The parser does not hold that value. AV1 5.9.19 reads `cdef_y_sec_strength[i]` +/// as `f(2)` and then mutates the variable of the same name in place — +/// `if (cdef_y_sec_strength[i] == 3) cdef_y_sec_strength[i] += 1` — so the spec's +/// own `cdef_y_sec_strength` afterwards holds `0, 1, 2` or **`4`**, and cros-codecs +/// follows the spec literally (`parser.rs`, `parse_cdef_params`). Every decode API +/// wants the value BEFORE that fixup, and applies the expansion itself: +/// +/// * **Vulkan** — libavcodec's `vulkan_av1.c` sends `frame_header->cdef_y_sec_strength[i]` +/// straight out of CBS, and `cbs_av1_syntax_template.c` reads it as a bare +/// `fbs(2, …)` with no fixup. Vulkan's `StdVideoAV1CDEF` therefore carries the +/// coded value, because libavcodec is what every driver was validated against; +/// * **VA-API** — `vaapi_av1.c` packs `(pri << 2) + sec`, two bits for `sec`; +/// * **NVDEC** — `nvdec_av1.c` packs `(pri & 0x0F) | (sec << 4)`, two bits again; +/// * **DXVA** — `DXVA_PicParams_AV1`'s `cdef_y_strength[i].secondary` IS a two-bit +/// bitfield. +/// +/// So sending `4` is not "a bigger number": on three of those four it overflows a +/// two-bit field and the strength reads back as **0** — no secondary CDEF filtering +/// at all, on exactly the blocks that asked for the strongest. That is a small, +/// everywhere, in-loop pixel difference, which is the hardest kind to see and the +/// easiest kind to propagate: CDEF runs before the frame is stored as a reference. +/// +/// Frame 0 of the vendored 25fps vector codes it (`cdef_y_sec_strength[3]` and +/// `cdef_uv_sec_strength[0]` are both 4), as do 68 of its 274 frames — +/// [`crate::av1::tests::the_cdef_secondary_strength_is_the_coded_value`] pins both +/// numbers. +/// +/// Values `0..=2` are untouched by the fixup and pass through; a hand-built header +/// carrying the coded `3` passes through too. Anything wider is CLAMPED rather than +/// masked, because `& 3` is precisely the truncation this function exists to +/// prevent. +pub fn coded_cdef_sec_strength(parsed: u32) -> u8 { + match parsed { + 0..=2 => parsed as u8, + _ => 3, + } +} + +/// What this access unit does to the decoded-picture store. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct DpbUpdate { + /// The id assigned to this AU's picture — allocate a surface for it. `None` for a + /// `show_existing_frame` access unit, which decodes nothing. + pub stored: Option, + /// Display-ready pictures, in output order. + pub outputs: Vec, + /// Pictures no slot holds any more; free once displayed. + pub removed: Vec, +} + +/// One tile group's payload, as a byte range in the access unit. +/// +/// AV1 hands the hardware whole tile-group OBUs rather than the slice-by-slice +/// records H.264 and H.265 use, so the range is the OBU's data, and the backends +/// concatenate in order. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TilePlan { + pub data: Range, + pub tg_start: u32, + pub tg_end: u32, +} + +/// Per-picture parameters a hardware picture-parameters struct wants. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PicturePlan { + pub frame_type: FrameType, + /// A key frame that refreshes every slot — the stream's re-anchor point. + pub is_key: bool, + pub show_frame: bool, + pub showable_frame: bool, + pub order_hint: u32, + /// Post-superres width; `frame_width` is the coded width before upscaling. + pub upscaled_width: u32, + pub frame_width: u32, + pub frame_height: u32, + /// The display region — AV1's counterpart to a conformance window. + pub render_width: u32, + pub render_height: u32, + pub bit_depth: u8, + /// 0 = monochrome, 1 = 4:2:0, 2 = 4:2:2, 3 = 4:4:4 — expressed in H.264's + /// `chroma_format_idc` vocabulary so a backend's format decision is one function + /// for all three codecs. + pub chroma_format_idc: u8, + /// Colour signalling, per picture and never latched — the same rule the other two + /// planners follow, because a host can switch an HDR desktop to PQ/BT.2020 in band. + pub colour: ColourDescription, +} + +/// One planned access unit. +#[derive(Debug, Clone)] +pub struct AuPlan { + pub picture: PicturePlan, + pub tiles: Vec, + /// The references this frame names, **indexed by AV1 reference NAME** — + /// position `i` is `ref_frame_idx[i]`, i.e. `LAST_FRAME + i`. + /// + /// `None` where the named slot held nothing: the reference is lost, it is also + /// reported as [`PlanWarning::MissingReference`], and it leaves a HOLE. The + /// array shape is the point. A `Vec` of the references that happened to resolve + /// renumbers every name after the first loss — name 4 silently becomes name 3 — + /// and every backend that read position-as-name then predicted from the wrong + /// picture. Repeats are preserved for the same reason: a frame may legitimately + /// point several of its seven names at one slot. + pub refs: [Option; REFS_PER_FRAME], + pub dpb: DpbUpdate, + /// Every slot that holds a picture as this AU decodes — AV1's answer to the + /// "marked DPB" the DXVA and VAAPI conversions want, and a superset of the + /// pictures [`Self::refs`] names. Slot order, each slot once. + pub dpb_refs: Vec, + pub warnings: Vec, + pub sequence: Rc, + /// The frame header this plan was built from, whole. + /// + /// [`Self::picture`] is the digest the CLIENT needs — size, depth, colour, + /// keyframe — while a hardware backend needs nearly all of the header: + /// AV1 puts tile info, quantisation, segmentation, loop filter, CDEF, loop + /// restoration, global motion and film grain in the per-frame header rather + /// than in a parameter set, and every one of them reaches the driver. Carried + /// whole for the same reason the H.264 and H.265 plans carry their activated + /// SPS/PPS: a backend must build its structures from exactly what was parsed, + /// never by re-reading the access unit. + pub header: Rc, +} + +/// Concealment signals: planning continues, the session layer requests recovery. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanWarning { + /// A frame named a slot holding no picture. Unlike H.264's equivalent this needs + /// no interpretation: no AV1 process empties a slot behind the stream's back, so + /// the reference was lost upstream. + MissingReference { slot: u8, ref_index: u8 }, + /// `show_existing_frame` named an empty slot — nothing to display. + MissingShowExisting { slot: u8 }, + /// The OBU walk stopped early: a malformed OBU with data behind it. The plan + /// covers what was read; `offset` is where the walk stopped. + TruncatedAu { offset: usize }, +} + +/// Why an access unit cannot be planned at all. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanError { + /// No frame header in the access unit — nothing to decode or display. + NoFrame, + /// A frame arrived before any sequence header. Every dimension, depth and colour + /// value lives there, so there is nothing to plan against. + NoSequenceHeader, + /// The parser rejected the bitstream. + Parse(String), + /// A frame outside this decoder's envelope. + Unsupported(&'static str), +} + +impl std::fmt::Display for PlanError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlanError::NoFrame => write!(f, "the access unit carried no frame header"), + PlanError::NoSequenceHeader => { + write!(f, "a frame arrived before any sequence header") + } + PlanError::Parse(e) => write!(f, "AV1 parse: {e}"), + PlanError::Unsupported(what) => write!(f, "outside the envelope: {what}"), + } + } +} + +impl std::error::Error for PlanError {} + +/// The AV1 planner: the vendored parser plus this crate's reference ledger. +pub struct Av1Planner { + parser: Parser, + /// Slot → the picture it holds. AV1's whole reference model, and the reason this + /// planner is bookkeeping rather than derivation. + slots: [Option; NUM_REF_SLOTS], + next_id: PicId, + sequence: Option>, +} + +impl Default for Av1Planner { + fn default() -> Self { + Self::new() + } +} + +impl Av1Planner { + pub fn new() -> Av1Planner { + Av1Planner { + parser: Parser::default(), + slots: [None; NUM_REF_SLOTS], + next_id: 1, + sequence: None, + } + } + + /// The slots currently holding pictures, in slot order. + pub fn dpb_refs(&self) -> Vec { + self.slots.iter().flatten().copied().collect() + } + + /// Plan one access unit — **one temporal unit, which may carry SEVERAL frames**. + /// + /// That is why this returns a vector and its H.264/H.265 siblings do not. An AV1 + /// temporal unit is free to hold a hidden frame and the `show_existing_frame` + /// that displays it, or several frames of a scalability layer; the vendored + /// conformance vector puts 274 frames in 250 temporal units, so the case is not + /// hypothetical even though punktfunk hosts (low-delay, no hidden frames) emit + /// one frame per unit. Planning only the last header seen would silently drop + /// the others — decoding fewer frames than the stream contains, with nothing to + /// say so. + /// + /// Plans come back in decode order; each carries its own reference set and its + /// own share of the store update. + pub fn plan_au(&mut self, au: &[u8]) -> Result, PlanError> { + let mut warnings = Vec::new(); + let mut plans: Vec = Vec::new(); + // The frame being accumulated: its header, and the tile groups seen since. + let mut pending: Option<(FrameHeaderObu, Vec)> = None; + let mut consumed = 0usize; + + while consumed < au.len() { + let action = match self.parser.read_obu(&au[consumed..]) { + Ok(action) => action, + Err(e) => { + // A malformed OBU with real data behind it is concealment + // material, not a parse failure, exactly as the other two + // planners treat a truncated NALU walk — but only once + // something has been read. Nothing at all is a hard error. + if pending.is_some() || !plans.is_empty() { + warnings.push(PlanWarning::TruncatedAu { offset: consumed }); + break; + } + return Err(PlanError::Parse(e)); + } + }; + let obu = match action { + ObuAction::Process(obu) => obu, + ObuAction::Drop(n) => { + consumed += n as usize; + continue; + } + }; + let used = obu.bytes_used; + // The OBU's payload as a range in THIS access unit, so a backend can + // hand the driver bytes without re-parsing. + let obu_start = consumed; + consumed += used; + + match self.parser.parse_obu(obu) { + Ok(ParsedObu::SequenceHeader(seq)) => self.sequence = Some(seq), + Ok(ParsedObu::FrameHeader(fh)) => { + // A new header ends the previous frame — its tile groups are + // all in by now. + if let Some((h, t)) = pending.take() { + plans.push(self.plan_one(h, t, std::mem::take(&mut warnings))?); + } + pending = Some((fh, Vec::new())); + } + Ok(ParsedObu::Frame(frame)) => { + // A Frame OBU is a header and its tile group in one, so it ends + // any previous frame and is itself complete. + if let Some((h, t)) = pending.take() { + plans.push(self.plan_one(h, t, std::mem::take(&mut warnings))?); + } + let tile = TilePlan { + data: obu_start..consumed, + tg_start: frame.tile_group.tg_start, + tg_end: frame.tile_group.tg_end, + }; + plans.push(self.plan_one( + frame.header, + vec![tile], + std::mem::take(&mut warnings), + )?); + } + Ok(ParsedObu::TileGroup(tg)) => { + let tile = TilePlan { + data: obu_start..consumed, + tg_start: tg.tg_start, + tg_end: tg.tg_end, + }; + match pending.as_mut() { + Some((_, tiles)) => tiles.push(tile), + // Tiles with no header ahead of them: the header was lost. + // Dropped rather than guessed at — there is no picture to + // attach them to. + None => warnings.push(PlanWarning::TruncatedAu { offset: obu_start }), + } + } + Ok(_) => {} + Err(e) => { + if pending.is_some() || !plans.is_empty() { + warnings.push(PlanWarning::TruncatedAu { offset: obu_start }); + break; + } + return Err(PlanError::Parse(e)); + } + } + } + + if let Some((h, t)) = pending.take() { + plans.push(self.plan_one(h, t, std::mem::take(&mut warnings))?); + } + if plans.is_empty() { + return Err(PlanError::NoFrame); + } + // Warnings raised after the last frame was planned (a truncated tail) still + // belong to this access unit; attach them to the frame they cut short. + if !warnings.is_empty() { + if let Some(last) = plans.last_mut() { + last.warnings.append(&mut warnings); + } + } + Ok(plans) + } + + fn plan_one( + &mut self, + header: FrameHeaderObu, + tiles: Vec, + warnings: Vec, + ) -> Result { + let sequence = self.sequence.clone().ok_or(PlanError::NoSequenceHeader)?; + self.plan_frame(header, sequence, tiles, warnings) + } + + fn plan_frame( + &mut self, + header: FrameHeaderObu, + sequence: Rc, + tiles: Vec, + mut warnings: Vec, + ) -> Result { + // Shared with the plan: the backends need the whole header and there is no + // reason for each to own a copy of a struct this size. + let header = Rc::new(header); + let dpb_refs = self.dpb_refs(); + + // `show_existing_frame` decodes nothing: it displays a slot's contents. + if header.show_existing_frame { + let slot = header.frame_to_show_map_idx; + let shown = self.slots.get(usize::from(slot)).copied().flatten(); + if shown.is_none() { + warnings.push(PlanWarning::MissingShowExisting { slot }); + } + // Showing a KEY frame this way resets the whole reference store (7.20): + // the shown frame's state is loaded and every slot refreshed. Handled + // through the same slot writer as an ordinary refresh so there is one + // place removals are computed. + let removed = if header.frame_type == FrameType::KeyFrame { + match shown { + // The SHOWN picture's state is what every refreshed slot takes + // (7.20 loads the shown frame's state), not this header's — + // a show_existing_frame header carries none of its own. + Some(pic) => self.refresh_slots(0xff, pic.id, pic.state), + None => Vec::new(), + } + } else { + Vec::new() + }; + let picture = picture_plan(&header, &sequence); + return Ok(AuPlan { + picture, + tiles, + refs: [None; REFS_PER_FRAME], + dpb: DpbUpdate { + stored: None, + outputs: shown.map(|p| p.id).into_iter().collect(), + removed, + }, + dpb_refs, + header: header.clone(), + warnings, + sequence, + }); + } + + // The references this frame names, BY NAME. A slot holding nothing leaves + // its name empty rather than shortening the list (field docs): position is + // the AV1 reference name and nothing may renumber it. + let mut refs = [None; REFS_PER_FRAME]; + if !matches!( + header.frame_type, + FrameType::KeyFrame | FrameType::IntraOnlyFrame + ) { + for (ref_index, &slot) in header.ref_frame_idx.iter().enumerate() { + match self.slots.get(usize::from(slot)).copied().flatten() { + Some(pic) => refs[ref_index] = Some(pic), + None => warnings.push(PlanWarning::MissingReference { + slot, + // Seven references; the cast cannot truncate. + ref_index: ref_index as u8, + }), + } + } + } + + let id = self.next_id; + self.next_id += 1; + + // The parser keeps its OWN reference state — sizes and order hints derived + // from references — and it must be updated whether or not our ledger is + // happy, or every later inter frame fails to parse. + if let Err(e) = self.parser.ref_frame_update(&header) { + return Err(PlanError::Parse(e)); + } + let removed = self.refresh_slots(header.refresh_frame_flags, id, RefState::of(&header)); + + let picture = picture_plan(&header, &sequence); + let outputs = if header.show_frame { + vec![id] + } else { + Vec::new() + }; + Ok(AuPlan { + picture, + tiles, + refs, + dpb: DpbUpdate { + stored: Some(id), + outputs, + removed, + }, + dpb_refs, + header: header.clone(), + warnings, + sequence, + }) + } + + /// Write `id` into every slot `refresh_frame_flags` names, and report the + /// pictures that no longer occupy ANY slot. + /// + /// The "any slot" part is the whole subtlety: one picture routinely occupies + /// several slots at once (a key frame refreshes all eight), so a slot being + /// overwritten does not mean its picture is gone. Reporting it as removed while + /// another slot still holds it would free a surface the next frame references — + /// which is the reference-loss shape this program exists to catch. + fn refresh_slots( + &mut self, + refresh_frame_flags: u32, + id: PicId, + state: RefState, + ) -> Vec { + let mut displaced: Vec = Vec::new(); + for slot in 0..NUM_REF_SLOTS { + if refresh_frame_flags & (1 << slot) == 0 { + continue; + } + if let Some(old) = self.slots[slot] { + if !displaced.contains(&old.id) { + displaced.push(old.id); + } + } + self.slots[slot] = Some(RefPic { + id, + // Eight slots; the cast cannot truncate. + slot: slot as u8, + state, + }); + } + displaced.retain(|gone| !self.slots.iter().flatten().any(|held| held.id == *gone)); + displaced + } +} + +fn picture_plan(header: &FrameHeaderObu, sequence: &SequenceHeaderObu) -> PicturePlan { + let color = &sequence.color_config; + let bit_depth = if color.high_bitdepth { + if color.twelve_bit { + 12 + } else { + 10 + } + } else { + 8 + }; + // AV1 spells the sampling as two subsampling flags plus a monochrome flag; + // every backend in this program decides formats in H.264's vocabulary, so the + // translation happens once, here. + let chroma_format_idc = match (color.mono_chrome, color.subsampling_x, color.subsampling_y) { + (true, _, _) => 0, + (false, true, true) => 1, + (false, true, false) => 2, + (false, false, false) => 3, + // 4:4:0 (subsampling_y only) has no AV1 profile; report it as monochrome's + // neighbour rather than silently calling it 4:2:0, and let the backend's + // format decision refuse it. + (false, false, true) => 4, + }; + PicturePlan { + frame_type: header.frame_type, + is_key: header.frame_type == FrameType::KeyFrame, + show_frame: header.show_frame, + showable_frame: header.showable_frame, + order_hint: header.order_hint, + upscaled_width: header.upscaled_width, + frame_width: header.frame_width, + frame_height: header.frame_height, + render_width: header.render_width, + render_height: header.render_height, + bit_depth, + chroma_format_idc, + colour: ColourDescription { + colour_primaries: color.color_primaries as u8, + transfer_characteristics: color.transfer_characteristics as u8, + matrix_coefficients: color.matrix_coefficients as u8, + video_full_range: color.color_range, + }, + } +} +#[cfg(test)] +mod tests { + use super::*; + use cros_codecs::bitstream_utils::IvfIterator; + + /// The vendored conformance vector: 250 temporal units, 274 frames — the same + /// file the crate's vendor-pinning smoke test walks, here driven through the + /// PLANNER instead of the parser. + const AV1_25FPS: &[u8] = + include_bytes!("../vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1"); + + /// Walk the whole vector and check the plan is self-consistent at every frame. + /// + /// **Measured composition: 250 temporal units, 274 frames, 24 units carrying two + /// frames, 250 displayed, and no `show_existing_frame` at all.** So the 24 extra + /// frames are HIDDEN frames — decoded, not displayed, referenced later. That is + /// what makes the multi-frame walk load-bearing rather than tidy: a planner that + /// took only the last header in each unit would decode 250 frames and silently + /// drop 24 REFERENCES, and the damage would surface later as missing-reference + /// concealment on frames that were never damaged. + /// + /// ⚠ Coverage this vector does NOT give: `show_existing_frame` appears zero + /// times, so [`Av1Planner::plan_frame`]'s display-only path — including the + /// key-frame slot reset — is exercised by no test here. It needs a vector that + /// uses it, or a synthesised one, before that path can be called verified. + #[test] + fn the_whole_vendored_vector_plans_and_the_frame_count_is_the_parsers() { + let mut planner = Av1Planner::new(); + let (mut units, mut frames, mut shown, mut show_existing) = (0u32, 0u32, 0u32, 0u32); + let mut multi_frame_units = 0u32; + let mut warnings = 0usize; + let mut max_refs = 0usize; + + for packet in IvfIterator::new(AV1_25FPS) { + units += 1; + let plans = planner + .plan_au(packet) + .unwrap_or_else(|e| panic!("temporal unit {units}: {e}")); + if plans.len() > 1 { + multi_frame_units += 1; + } + for plan in &plans { + frames += 1; + warnings += plan.warnings.len(); + shown += plan.dpb.outputs.len() as u32; + if plan.dpb.stored.is_none() { + show_existing += 1; + assert!( + plan.tiles.is_empty(), + "a show_existing_frame decodes nothing and can carry no tiles" + ); + } + max_refs = max_refs.max(plan.refs.iter().flatten().count()); + + // Every tile range must lie inside the access unit it came from. + for tile in &plan.tiles { + assert!( + tile.data.start < tile.data.end && tile.data.end <= packet.len(), + "frame {frames}: tile range {:?} is not inside a {}-byte unit", + tile.data, + packet.len() + ); + } + // A reference must name a slot that holds the picture it claims, + // and the name it sits under must be the one the bitstream coded. + for (name, r) in plan.refs.iter().enumerate() { + let Some(r) = r else { continue }; + assert!(usize::from(r.slot) < NUM_REF_SLOTS); + assert_eq!( + r.slot, plan.header.ref_frame_idx[name], + "frame {frames}: reference name {name} holds the picture in \ + slot {}, but ref_frame_idx[{name}] names slot {}", + r.slot, plan.header.ref_frame_idx[name] + ); + // The marked store is a superset of what this frame reads. + assert!( + plan.dpb_refs.iter().any(|d| d.id == r.id), + "frame {frames}: reference {} is not in the marked store", + r.id + ); + } + } + } + + assert_eq!(units, 250, "the vendored vector is 250 temporal units"); + assert_eq!( + frames, 274, + "the parser's own golden is 274 frames; a planner that sees fewer is \ + dropping frames a multi-frame temporal unit carried" + ); + assert_eq!( + multi_frame_units, 24, + "the 24 units carrying two frames are the whole reason plan_au returns a \ + vector; if this reaches 0 the count above is being met some other way" + ); + assert_eq!( + warnings, 0, + "a clean conformance vector must plan without concealment" + ); + assert_eq!( + shown, 250, + "one displayed frame per temporal unit — the other 24 are hidden" + ); + assert_eq!( + show_existing, 0, + "this vector uses no show_existing_frame; if that ever changes, the \ + display-only path stops being untested and the doc above must say so" + ); + assert_eq!( + max_refs, REFS_PER_FRAME, + "an inter frame names all seven references" + ); + } + + /// A picture can hold several slots at once, and losing ONE of them must not + /// report the picture as removed. + /// + /// This is the whole reason [`Av1Planner::refresh_slots`] filters what it + /// displaces: a key frame refreshes all eight slots, so the next frame to + /// refresh a single slot displaces that picture from ONE slot while seven still + /// hold it. Reporting it removed would free the surface under a live reference — + /// the reference-loss shape this program exists to catch. + #[test] + fn a_picture_held_by_several_slots_is_not_removed_until_the_last_one_goes() { + let mut planner = Av1Planner::new(); + let at = |order_hint: u32| RefState { + order_hint, + ..RefState::of(&FrameHeaderObu::default()) + }; + // A key frame in every slot. + let removed = planner.refresh_slots(0xff, 1, at(0)); + assert!(removed.is_empty(), "nothing was there to displace"); + assert_eq!(planner.dpb_refs().len(), NUM_REF_SLOTS); + + // A frame takes one slot: picture 1 still holds the other seven. + let removed = planner.refresh_slots(0b0000_0001, 2, at(1)); + assert!( + removed.is_empty(), + "picture 1 still occupies seven slots — reporting it removed would free \ + a surface every later frame still references" + ); + + // Take the rest: now it really is gone, and reported exactly once. + let removed = planner.refresh_slots(0b1111_1110, 3, at(2)); + assert_eq!(removed, vec![1], "reported once, not once per slot"); + + // And picture 2's single slot. + let removed = planner.refresh_slots(0b0000_0001, 4, at(3)); + assert_eq!(removed, vec![2]); + } + + /// A lost reference must leave a HOLE at its own name, not shorten the list. + /// + /// This is the defect the name-indexed [`AuPlan::refs`] closes, and it is worth + /// a synthetic case because the clean vector never loses a reference: with a + /// `Vec` of survivors, dropping the picture behind name 2 slid names 3..6 down + /// one, and every backend that reads position-as-name then predicted LAST from + /// the picture GOLDEN should have supplied. Nothing else in the plan would say + /// so — the reference count is still plausible and every entry is still a real + /// picture. + #[test] + fn a_lost_reference_leaves_its_name_empty_and_does_not_renumber_the_others() { + // The vector's first unit is a key frame: it gives the vendored parser its + // sequence header (`ref_frame_update` needs one) and fills all eight slots. + let mut planner = Av1Planner::new(); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let sequence = planner + .plan_au(first) + .expect("the key frame plans") + .first() + .expect("a frame") + .sequence + .clone(); + assert_eq!(planner.dpb_refs().len(), NUM_REF_SLOTS); + + // Empty the slot name 2 will point at — a reference lost upstream. + planner.slots[5] = None; + + let header = FrameHeaderObu { + frame_type: FrameType::InterFrame, + ref_frame_idx: [0, 1, 5, 3, 4, 2, 6], + // Refresh nothing: this frame is here to be PLANNED, not to disturb + // the ledger the assertions read. + refresh_frame_flags: 0, + ..Default::default() + }; + let plan = planner + .plan_frame(header, sequence, Vec::new(), Vec::new()) + .expect("an inter frame with a lost reference still plans"); + + assert_eq!( + plan.warnings, + vec![PlanWarning::MissingReference { + slot: 5, + ref_index: 2 + }] + ); + assert!(plan.refs[2].is_none(), "the lost name stays empty"); + let named: Vec> = plan.refs.iter().map(|r| r.map(|p| p.slot)).collect(); + assert_eq!( + named, + vec![Some(0), Some(1), None, Some(3), Some(4), Some(2), Some(6)], + "every surviving name must still sit at ITS OWN index — a compacted \ + list would read [0, 1, 3, 4, 2, 6] and rename four references" + ); + } + + /// `RefFrameSignBias` must come out SPEC-indexed (bit 1 = `LAST_FRAME`), which + /// the vendored parser's array is not. + /// + /// Recomputed here from `order_hints` — which the parser DOES index by + /// reference name — through the spec's own `get_relative_dist` (5.9.3), + /// transcribed rather than borrowed because cros-codecs' `helpers` module is + /// private. So this does not restate [`RefState::of`]'s shift; it restates the + /// spec, and the two must agree on every frame of the vector. Without the + /// shift, ALTREF's bias lands on GOLDEN and `INTRA_FRAME` (bit 0, which the + /// spec never sets) picks up LAST's. + #[test] + fn the_sign_bias_mask_is_spec_indexed_not_parser_indexed() { + /// AV1 5.9.3 `get_relative_dist`, verbatim. + fn get_relative_dist(enable_order_hint: bool, bits: i32, a: i32, b: i32) -> i32 { + if !enable_order_hint { + return 0; + } + let diff = a - b; + let m = 1 << (bits - 1); + (diff & (m - 1)) - (diff & m) + } + + let mut planner = Av1Planner::new(); + let (mut frames, mut nonzero_masks, mut future_refs) = (0u32, 0u32, 0u32); + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("the clean vector plans") { + if plan.dpb.stored.is_none() { + continue; + } + frames += 1; + let h = &*plan.header; + let seq = &*plan.sequence; + let bits = seq.order_hint_bits_minus_1 + 1; + let state = RefState::of(h); + + let mut expected = 0u8; + if !h.frame_is_intra { + for name in 1..=REFS_PER_FRAME { + let dist = get_relative_dist( + seq.enable_order_hint, + bits, + h.order_hints[name] as i32, + h.order_hint as i32, + ); + if dist > 0 { + expected |= 1 << name; + future_refs += 1; + } + } + } + assert_eq!( + state.ref_frame_sign_bias, expected, + "frame {frames}: sign-bias mask {:#010b} does not match the \ + spec's own RefFrameSignBias[1..8] {expected:#010b}", + state.ref_frame_sign_bias + ); + assert_eq!( + state.ref_frame_sign_bias & 1, + 0, + "bit 0 is INTRA_FRAME and the spec never sets it — a set bit \ + there is the parser's off-by-one leaking through" + ); + if state.ref_frame_sign_bias != 0 { + nonzero_masks += 1; + } + } + } + assert_eq!(frames, 274); + assert!( + nonzero_masks > 0 && future_refs > 0, + "this vector is the hidden-ALTREF one: if no frame ever biased a \ + reference into the future, this test compared zero against zero and \ + the shift above is untested" + ); + eprintln!("frames {frames} · frames with a future reference {nonzero_masks}"); + } + + /// A reference carries ITS OWN frame type, not the frame reading it. + #[test] + fn a_reference_carries_its_own_frame_type() { + let mut planner = Av1Planner::new(); + let mut mixed = 0u32; + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("plans") { + if plan.dpb.stored.is_none() { + continue; + } + for r in plan.refs.iter().flatten() { + if r.state.frame_type != plan.header.frame_type { + mixed += 1; + } + } + } + } + assert!( + mixed > 0, + "no frame of the vector ever referenced a picture of a DIFFERENT frame \ + type, so nothing here can tell the reference's own type from the \ + current frame's — the exact substitution this field exists to prevent" + ); + } + + /// [`coded_cdef_sec_strength`] inverts the spec's in-place fixup — and the + /// vendored vector really does code the value that needs it, on frame 0. + /// + /// Both halves matter. The mapping is three lines and could be asserted against + /// itself forever; what makes it load-bearing is that the parser DOES hand out + /// `4`, on the very first frame the parity leg compares, and on 68 of 274 + /// frames overall. If a re-synced vector ever stopped coding a secondary + /// strength of 3, this test would be comparing a correction against a stream + /// that never needs it, and the four hardware APIs' two-bit fields would be + /// untested again. + #[test] + fn the_cdef_secondary_strength_is_the_coded_value() { + // The fixup's inverse, and the identity everywhere else. + assert_eq!( + [0, 1, 2, 3, 4].map(coded_cdef_sec_strength), + [0, 1, 2, 3, 3], + "0..=2 pass through, the spec's 4 is the coded 3, and a hand-built 3 is \ + already coded" + ); + + let mut planner = Av1Planner::new(); + let (mut frames, mut needing_fixup, mut strengths) = (0u32, 0u32, 0u32); + let mut frame0_raw: Vec = Vec::new(); + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("the clean vector plans") { + if plan.dpb.stored.is_none() { + continue; + } + frames += 1; + let cdef = &plan.header.cdef_params; + let coded = 1usize << cdef.cdef_bits; + let mut any = false; + for i in 0..coded { + for raw in [cdef.cdef_y_sec_strength[i], cdef.cdef_uv_sec_strength[i]] { + assert!( + raw <= 2 || raw == 4, + "frame {frames}: the parser can only hold 0, 1, 2 or the \ + fixed-up 4 — {raw} means the vendored parse changed" + ); + assert!( + coded_cdef_sec_strength(raw) <= 3, + "the corrected value must fit the two bits every hardware \ + API gives it" + ); + if raw == 4 { + any = true; + strengths += 1; + } + } + } + if any { + needing_fixup += 1; + } + if frames == 1 { + frame0_raw = cdef.cdef_y_sec_strength[..coded] + .iter() + .chain(cdef.cdef_uv_sec_strength[..coded].iter()) + .copied() + .collect(); + } + } + } + assert_eq!(frames, 274); + assert_eq!( + frame0_raw, + vec![1, 2, 0, 4, 4, 0, 0, 0], + "frame 0's four luma then four chroma secondary strengths — the first \ + frame the parity leg hashes, and it needs the correction" + ); + assert_eq!( + needing_fixup, 68, + "68 of 274 frames of this vector carry a secondary strength the spec \ + fixed up; at zero the correction above is untested by any real stream" + ); + eprintln!( + "frames {frames} · frames needing the fixup {needing_fixup} · strengths \ + corrected {strengths}" + ); + } + + #[test] + fn an_access_unit_with_no_frame_is_refused() { + let mut planner = Av1Planner::new(); + // A lone temporal delimiter: a valid OBU, no frame. + assert_eq!( + planner.plan_au(&[0x12, 0x00]).err(), + Some(PlanError::NoFrame) + ); + } +} diff --git a/crates/pf-bitstream/src/h264.rs b/crates/pf-bitstream/src/h264.rs new file mode 100644 index 00000000..17314b81 --- /dev/null +++ b/crates/pf-bitstream/src/h264.rs @@ -0,0 +1,2779 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file (vendor/cros-codecs/LICENSE). +// +// Adapted from cros-codecs `decoder/stateless/h264.rs` (see +// vendor/cros-codecs/PROVENANCE.md for the snapshot pin). The spec machinery — POC +// computation (8.2.1), frame_num-gap handling (8.2.5.2), reference list initialization +// and modification (8.2.4), sliding-window and adaptive MMCO marking (8.2.5), DPB +// bumping/output (C.4.5.3) — is ported faithfully and keeps upstream's structure and +// spec-section comments so future upstream diffs stay legible. Stripped: the +// StatelessDecoder/backend trait plumbing, fd/event machinery, pooled-buffer handling, +// and the interlaced field-splitting paths (the envelope gate below rejects interlaced +// streams outright). + +//! Per-AU H.264 planning: [`H264Planner::plan_au`] turns one access unit exactly as the +//! pump hands it to a decoder (Annex-B, parameter sets + the slices of one picture) into +//! an [`AuPlan`] — everything a stateless hardware decoder needs before submission and +//! nothing it has to re-derive: parsed headers, POC, per-slice reference lists (with +//! long-term/MMCO state, which host RFI recovery leans on) and the DPB delta. +//! +//! Concealment posture: a `frame_num` gap or a reference that is not in the DPB is a +//! [`PlanWarning`], never an error — the session layer sees the warning and requests +//! recovery while planning continues. [`PlanError`] is reserved for AUs that cannot be +//! planned at all. + +use std::collections::btree_map::Entry; +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::io::Cursor; +use std::mem; +use std::ops::Range; +use std::rc::Rc; + +use cros_codecs::codec::h264::dpb::Dpb; +use cros_codecs::codec::h264::dpb::DpbEntry; +use cros_codecs::codec::h264::dpb::DpbPicRefList; +use cros_codecs::codec::h264::dpb::MmcoError; +use cros_codecs::codec::h264::dpb::ReferencePicLists; +use cros_codecs::codec::h264::parser::MaxLongTermFrameIdx; +use cros_codecs::codec::h264::parser::Nalu; +use cros_codecs::codec::h264::parser::NaluType; +use cros_codecs::codec::h264::parser::Parser; +use cros_codecs::codec::h264::parser::Pps; +use cros_codecs::codec::h264::parser::RefPicListModification; +use cros_codecs::codec::h264::parser::Slice; +use cros_codecs::codec::h264::parser::SliceType; +use cros_codecs::codec::h264::parser::Sps; +use cros_codecs::codec::h264::picture::Field; +use cros_codecs::codec::h264::picture::FieldRank; +use cros_codecs::codec::h264::picture::IsIdr; +use cros_codecs::codec::h264::picture::PictureData; +use cros_codecs::codec::h264::picture::RcPictureData; +use cros_codecs::codec::h264::picture::Reference; +use cros_codecs::Resolution; +use tracing::trace; + +pub use cros_codecs::codec::h264::parser::Level; +pub use cros_codecs::codec::h264::parser::SliceHeader; + +use crate::sei; +pub use crate::sei::RecoveryPoint; + +/// Stable identity of a stored picture, monotonically increasing per stored picture. +/// +/// This is what backends map to hardware DPB slots. Indices into the live DPB `Vec` +/// shift on bumping and must never be exposed; the `Dpb` handle parameter carries +/// this id instead. +pub type PicId = u64; + +/// Everything a backend needs to submit one access unit. +#[derive(Debug, Clone)] +pub struct AuPlan { + pub picture: PicturePlan, + pub slices: Vec, + pub dpb: DpbUpdate, + /// Every picture the DPB holds marked "used for reference" at the moment this AU + /// decodes — the MARKED DPB, not this AU's reference lists. + /// + /// [`Self::dpb`] reports a delta (stored/outputs/removed) because that is what a + /// surface allocator needs. This is the other half: the STATE, which is what the + /// DXVA picture-parameters formats ask for. `DXVA_PicParams_H264::RefFrameList` + /// is spec-defined as the pictures currently marked used for reference — with + /// `UsedForReferenceFlags` a statement about the DPB, not about this access unit + /// — and libavcodec's DXVA path fills it by walking its whole DPB + /// (`short_ref` then `long_ref`), not the derived lists. Vulkan's + /// `pReferenceSlots` is the opposite: spec-defined as the slots THIS decode + /// operation uses, so a subset is correct there and the native Vulkan rung binds + /// the AU's own set. + /// + /// The difference bites on the long-term/RFI class: a long-term reference held + /// across pictures that none of them names is absent from every derived list yet + /// must stay in `RefFrameList`, because a driver keeping per-reference state is + /// entitled to read its absence as "no longer a reference" and discard it. + /// + /// Captured at BEGIN-picture time — after 8.2.5.2 gap placeholders are inserted + /// and after any IDR drain, before this AU's own end-of-picture marking (8.2.5) + /// stores or evicts anything. That is exactly the DPB the hardware decodes this + /// picture against, and it is why a picture named here may still appear in + /// [`DpbUpdate::removed`] of the same plan: it was a valid reference for this + /// decode and stopped being one at its end. + /// + /// Order is the DPB's own (oldest stored first). DXVA imposes none — a driver + /// resolves an entry by its `FrameNumList`/`FieldOrderCntList` pair — so + /// backends are free to reorder, and pf-dxvadec does. + /// + /// 8.2.5.2 gap placeholders are ABSENT: they carry no [`PicId`], so there is no + /// surface a backend could name. A frame the DPB holds only for OUTPUT (already + /// unmarked) is absent too — it is not a reference. + pub dpb_refs: Vec, + pub warnings: Vec, + /// The SPS the planner activated for this AU — the one [`Self::picture`]'s + /// parameters derive from (the FIRST slice's PPS's SPS; a later slice may + /// legally reference another PPS, and that drift deliberately does not reach + /// here). Cloned out of the parser's table so backends build their parameter + /// objects from exactly what was activated, never by re-parsing the AU. + pub sps: Rc, + /// The PPS the picture was begun with (the first slice's), same contract as + /// [`Self::sps`]. Its `sps` field is the same `Rc` as [`Self::sps`]. + pub pps: Rc, +} + +/// Per-picture parameters, captured after 8.2.1 POC derivation and before end-of-picture +/// marking (the values a hardware picture-parameters struct wants). +#[derive(Debug, Clone)] +pub struct PicturePlan { + pub is_idr: bool, + pub nal_ref_idc: u8, + pub is_reference: bool, + pub frame_num: u16, + pub top_field_order_cnt: i32, + pub bottom_field_order_cnt: i32, + /// The final PicOrderCnt of the picture (min of top/bottom for a frame). + pub pic_order_cnt: i32, + pub coded_width: u32, + pub coded_height: u32, + /// Conformance-window crop (7.4.2.1.1), in luma samples of the coded picture. + pub display_crop: DisplayCrop, + /// Colour signalling from the ACTIVE SPS's VUI (E.2.1 inference where absent). + /// Per picture, like [`Self::display_crop`], never latched at session start: + /// the Windows host switches an HDR desktop to PQ/BT.2020 IN-BAND with a new + /// SPS mid-stream, so a backend that captured the first AU's colour would + /// paint HDR frames washed out. + pub colour: ColourDescription, + pub profile_idc: u8, + pub level_idc: Level, + pub bit_depth_luma_minus8: u8, + pub bit_depth_chroma_minus8: u8, + pub chroma_format_idc: u8, + /// DPB size in frames per A.3.1 — backends size their slot pool from this. + pub max_dpb_frames: usize, + pub recovery_point: Option, +} + +/// The region of the coded picture that is actually displayed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DisplayCrop { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, +} + +/// One picture's colour signalling: raw H.273 code points off the active SPS's +/// VUI. When the VUI (or its `video_signal_type`/`colour_description` blocks) is +/// absent these hold E.2.1's INFERRED values — 2/2/2 ("unspecified") with limited +/// range — never a raw struct-zero (0 is a reserved code point no real stream +/// means). That matches the CICP libavcodec reports for such streams, so backends +/// forward these untouched and the consumer's CSC resolves "unspecified" to its +/// SDR default. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ColourDescription { + pub colour_primaries: u8, + pub transfer_characteristics: u8, + pub matrix_coefficients: u8, + /// `video_full_range_flag` (E.2.1 infers limited range when absent). + pub video_full_range: bool, +} + +/// One slice NALU of the picture, with its reference lists fully derived. +#[derive(Debug, Clone)] +pub struct SlicePlan { + /// Byte range of the slice NALU in the input AU, start code included — hardware + /// decoders take the raw bitstream, so the plan points instead of copying. + pub data: Range, + /// The parsed slice header, as the vendored parser produced it. + pub header: SliceHeader, + pub ref_list0: Vec, + pub ref_list1: Vec, +} + +/// A reference list entry: the minimum every backend picparams format needs. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RefPic { + pub id: PicId, + /// The stored picture's 8.2.1 field order counts. Equal for a progressive frame + /// UNLESS the PPS set `bottom_field_pic_order_in_frame_present_flag` and the + /// slice carried a nonzero `delta_pic_order_cnt_bottom` — backend picparams + /// formats want the pair, and collapsing to one value would fabricate the bottom + /// count. After an MMCO 5 these are the picture's REBASED values (8.2.5.4.5), + /// which is what later AUs reference it by — see [`PlanWarning::Mmco5Rebase`]. + pub top_field_order_cnt: i32, + pub bottom_field_order_cnt: i32, + pub is_long_term: bool, + /// `frame_num` for short-term references, `LongTermFrameIdx` for long-term ones — + /// the pair DXVA and Vulkan both key reference pictures by. + pub frame_num_or_lt_idx: u16, +} + +/// The DPB delta of one planned AU: what to allocate, what is display-ready, what can +/// be freed. +#[derive(Debug, Clone, Default)] +pub struct DpbUpdate { + /// The id assigned to this AU's picture — allocate a surface for it. + pub stored: Option, + /// Display-ready pictures, in output (bumping) order. + pub outputs: Vec, + /// Pictures the planner will never reference again; free once displayed. + pub removed: Vec, +} + +/// Concealment signals: planning continues, the session layer requests recovery. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanWarning { + /// `frame_num` skipped a value — at least one reference AU was lost upstream. + FrameNumGap { expected: u16, got: u16 }, + /// A slice or MMCO named a reference picture the DPB does not hold. + MissingReference { + context: &'static str, + detail: String, + }, + /// The AU's NALU walk stopped early — a malformed NALU with real data behind it, + /// or a slice belonging to another picture (mis-split AU). The plan covers only + /// the slices before the cut; `offset` is the byte position of the cut in the AU. + TruncatedAu { offset: usize }, + /// The AU carried an MMCO 5 (8.2.5.4.5): the DPB was drained and the CURRENT + /// picture's stored frame_num/POC were rebased to zero AFTER its plan was + /// captured. Spec-legal and fully planned — the [`PicturePlan`] holds the + /// pre-rebase 8.2.1 values a decoder submits with, while later AUs reference the + /// picture by its rebased values ([`RefPic`] carries the stored pair). punktfunk + /// hosts never emit MMCO 5, so this warning is the field signal if that + /// assumption ever breaks. + Mmco5Rebase, +} + +/// The AU cannot be planned at all. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanError { + Parse(String), + /// Legal H.264, but outside what punktfunk hosts emit (clients only decode + /// punktfunk hosts, so this is a stream-integrity failure, not a feature gap). + OutsideEnvelope(&'static str), + NoActiveParamSet { + pps_id: u8, + }, + /// [`H264Planner::flush`] discarded the decoding state; planning resumes only at + /// an IDR (the port of upstream's `Reset` gating). + AwaitingIdr, +} + +impl std::fmt::Display for PlanError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlanError::Parse(msg) => write!(f, "parse error: {msg}"), + PlanError::OutsideEnvelope(what) => { + write!(f, "outside the punktfunk decode envelope: {what}") + } + PlanError::NoActiveParamSet { pps_id } => { + write!(f, "slice references PPS {pps_id}, which has not been seen") + } + PlanError::AwaitingIdr => { + write!(f, "flushed: waiting for an IDR to resume planning") + } + } + } +} + +impl std::error::Error for PlanError {} + +/// Keeps track of the last values seen for negotiation purposes. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct NegotiationInfo { + coded_resolution: Resolution, + profile_idc: u8, + bit_depth_luma_minus8: u8, + bit_depth_chroma_minus8: u8, + chroma_format_idc: u8, + max_dpb_frames: usize, + interlaced: bool, +} + +impl From<&Sps> for NegotiationInfo { + fn from(sps: &Sps) -> Self { + NegotiationInfo { + coded_resolution: Resolution::from((sps.width(), sps.height())), + profile_idc: sps.profile_idc, + bit_depth_luma_minus8: sps.bit_depth_luma_minus8, + bit_depth_chroma_minus8: sps.bit_depth_chroma_minus8, + chroma_format_idc: sps.chroma_format_idc, + max_dpb_frames: sps.max_dpb_frames(), + interlaced: !sps.frame_mbs_only_flag, + } + } +} + +#[derive(Copy, Clone, Debug)] +enum RefPicList { + RefPicList0, + RefPicList1, +} + +/// Cached variables from the previous reference picture (8.2.1). +struct PrevReferencePicInfo { + frame_num: u32, + has_mmco_5: bool, + top_field_order_cnt: i32, + pic_order_cnt_msb: i32, + pic_order_cnt_lsb: i32, + field: Field, +} + +impl Default for PrevReferencePicInfo { + fn default() -> Self { + Self { + frame_num: Default::default(), + has_mmco_5: Default::default(), + top_field_order_cnt: Default::default(), + pic_order_cnt_msb: Default::default(), + pic_order_cnt_lsb: Default::default(), + field: Field::Frame, + } + } +} + +impl PrevReferencePicInfo { + fn fill(&mut self, pic: &PictureData) { + self.has_mmco_5 = pic.has_mmco_5; + self.top_field_order_cnt = pic.top_field_order_cnt; + self.pic_order_cnt_msb = pic.pic_order_cnt_msb; + self.pic_order_cnt_lsb = pic.pic_order_cnt_lsb; + self.field = pic.field; + self.frame_num = pic.frame_num; + } +} + +/// Cached variables from the previous picture (8.2.1). +#[derive(Default)] +struct PrevPicInfo { + frame_num: u32, + frame_num_offset: u32, + has_mmco_5: bool, +} + +impl PrevPicInfo { + fn fill(&mut self, pic: &PictureData) { + self.frame_num = pic.frame_num; + self.has_mmco_5 = pic.has_mmco_5; + self.frame_num_offset = pic.frame_num_offset; + } +} + +/// Used to track that `first_mb_in_slice` increases monotonically (7.4.3). +/// +/// Upstream tracks this too, but with an inverted comparison that fires on every +/// well-formed slice; corrected here (strictly increasing across a picture's slices), +/// and `None`/vacant marks "no slice seen yet" so the first slice never trips it. +enum CurrentMacroblockTracking { + SeparateColorPlane(BTreeMap), + NonSeparateColorPlane(Option), +} + +/// State of the picture being planned, spanning the slices of one AU. +struct CurrentPicState { + /// Data for the current picture as extracted from the stream. + pic: PictureData, + /// PPS at the time of the current picture. Follows the slices — a later slice may + /// reference another PPS — and feeds end-of-picture marking, as upstream does. + pps: Rc, + /// The PPS the picture was BEGUN with. [`H264Planner::picture_plan`] reads this + /// snapshot, like upstream's `start_picture`, so per-picture parameters cannot + /// drift to a later slice's PPS. + first_slice_pps: Rc, + /// The id backends will know this picture by (upstream: the backend picture). + id: PicId, + /// Reference picture lists, derived once per picture, indexed per slice. + ref_pic_lists: ReferencePicLists, + /// The marked DPB as it stands for THIS picture's decode, captured beside the + /// reference lists it was derived from — see [`AuPlan::dpb_refs`]. + dpb_refs: Vec, + current_macroblock: CurrentMacroblockTracking, +} + +/// Plans H.264 access units for stateless hardware decoders. +/// +/// Owns the vendored parser and DPB plus the POC/marking state that upstream keeps in +/// `H264DecoderState`. One instance per elementary stream; feed AUs in decode order. +#[derive(Default)] +pub struct H264Planner { + parser: Parser, + negotiation_info: NegotiationInfo, + dpb: Dpb, + prev_ref_pic_info: PrevReferencePicInfo, + prev_pic_info: PrevPicInfo, + max_long_term_frame_idx: MaxLongTermFrameIdx, + /// Next [`PicId`] to hand out (upstream: the backend allocates here). + next_pic_id: PicId, + /// Display-ready pictures accumulated while planning (upstream: the decoder's + /// ready queue). Not cleared on a failed AU — the next emitted [`DpbUpdate`] + /// carries them, so an error can never swallow a frame. + pending_outputs: Vec, + /// Ids the last emitted [`DpbUpdate`] left alive: the baseline for `removed`. + /// Kept across failed AUs so interim evictions are reported, never dropped. + reported_live: BTreeSet, + /// Set by [`Self::flush`]: planning resumes only at an IDR (upstream: `Reset`). + awaiting_idr: bool, +} + +impl H264Planner { + pub fn new() -> Self { + Default::default() + } + + /// Plan one access unit: Annex-B bytes containing SPS/PPS/SEI/AUD NALUs plus the + /// 1..N slice NALUs of exactly one picture. + /// + /// After a [`PlanError`] the planner state is best-effort; the session should + /// request an IDR before feeding more AUs. Outputs and removals queued by a failed + /// AU are retained and emitted with the next successful plan (or [`Self::flush`]) — + /// never discarded. + pub fn plan_au(&mut self, au: &[u8]) -> Result { + let mut warnings = Vec::new(); + let mut slices = Vec::new(); + let mut recovery_point = None; + let mut current: Option = None; + let mut saw_nalu = false; + + let mut cursor = Cursor::new(au); + loop { + let nalu = match Nalu::next(&mut cursor) { + Ok(nalu) => nalu, + Err(_) => { + // End of the AU — or a NALU whose header failed to parse (reserved + // type, truncated byte). A start code past the cursor means real + // data was cut off: degrade to a concealment signal covering the + // slices already planned. Without one this is benign trailing + // padding — a NALU's own payload is emulation-prevented and cannot + // contain a start code. + let pos = (cursor.position() as usize).min(au.len()); + if au[pos..].windows(3).any(|w| w == [0x00, 0x00, 0x01]) { + warnings.push(PlanWarning::TruncatedAu { offset: pos }); + } + break; + } + }; + saw_nalu = true; + // After `Nalu::next` the cursor sits on the NAL header byte; `offset` is the + // start-code length and `size` the NALU payload length, which pins the + // NALU's absolute byte range in the AU without copying. + let nalu_offset = cursor.position() as usize; + let range = (nalu_offset - nalu.offset)..(nalu_offset + nalu.size); + debug_assert_eq!(&au[range.clone()], nalu.data.as_ref()); + + match nalu.header.type_ { + NaluType::Sps => { + let sps = self.parser.parse_sps(&nalu).map_err(PlanError::Parse)?; + Self::check_envelope(sps)?; + } + NaluType::Pps => { + self.parser.parse_pps(&nalu).map_err(PlanError::Parse)?; + } + NaluType::Sei => { + match sei::parse_recovery_point(nalu.as_ref().get(1..).unwrap_or(&[])) { + Ok(Some(rp)) => recovery_point = Some(rp), + Ok(None) => {} + // A broken SEI must not cost the picture it decorates. + Err(err) => trace!("ignoring unparseable SEI NALU: {err}"), + } + } + NaluType::Slice | NaluType::SliceIdr => { + // Upstream's `Reset` gating: after a flush, only an IDR restarts + // the decoding process. + if current.is_none() && self.awaiting_idr { + if !nalu.header.idr_pic_flag { + return Err(PlanError::AwaitingIdr); + } + self.awaiting_idr = false; + } + let slice = match self.parser.parse_slice_header(nalu) { + Ok(slice) => slice, + Err(err) => return Err(Self::slice_parse_error(err)), + }; + match ¤t { + None => current = Some(self.begin_picture(&slice, &mut warnings)?), + // Upstream would finish the picture and begin another; our + // contract is one picture per AU, so a second first-slice means + // the pump upstream of us is broken. + Some(_) if slice.header.first_mb_in_slice == 0 => { + return Err(PlanError::OutsideEnvelope( + "more than one coded picture in one access unit", + )); + } + Some(cur) => { + // Mis-split-AU guard: a continuation slice must belong to + // the picture the first slice began (7.4.3: same frame_num, + // same IDR-ness). A foreign slice and everything after it + // are dropped behind a concealment signal. + if u32::from(slice.header.frame_num) != cur.pic.frame_num + || slice.nalu.header.idr_pic_flag + != matches!(cur.pic.is_idr, IsIdr::Yes { .. }) + { + warnings.push(PlanWarning::TruncatedAu { + offset: range.start, + }); + break; + } + } + } + let cur = current.as_mut().expect("a picture was begun above"); + slices.push(self.plan_slice(cur, slice, range, &mut warnings)?); + } + NaluType::SliceDpa | NaluType::SliceDpb | NaluType::SliceDpc => { + return Err(PlanError::OutsideEnvelope("data-partitioned slices")); + } + other => trace!("skipping NAL unit type {other:?}"), + } + } + + if !saw_nalu { + return Err(PlanError::Parse("no NAL units in access unit".into())); + } + let cur = current + .ok_or_else(|| PlanError::Parse("access unit contains no coded picture".into()))?; + + // Captured before finish_picture: MMCO5 rewrites the stored POC afterwards, but + // backends submit the picture with its 8.2.1 values. + let picture = Self::picture_plan(&cur, recovery_point); + // The activated parameter sets ride out with the plan (AuPlan field docs); + // cloned before finish_picture consumes `cur`. + let pps = Rc::clone(&cur.first_slice_pps); + let sps = Rc::clone(&pps.sps); + let dpb_refs = cur.dpb_refs.clone(); + let stored = self.finish_picture(cur, &mut warnings)?; + + // `removed` is the delta against what the backend last SAW alive, not against + // this call's start — a failed AU in between may have evicted pictures, and + // those removals must still be reported here. + let live_after = self.live_ids(); + let mut previously_live = mem::take(&mut self.reported_live); + previously_live.insert(stored); + let removed = previously_live.difference(&live_after).copied().collect(); + self.reported_live = live_after; + + Ok(AuPlan { + picture, + slices, + dpb: DpbUpdate { + stored: Some(stored), + outputs: mem::take(&mut self.pending_outputs), + removed, + }, + dpb_refs, + warnings, + sps, + pps, + }) + } + + /// Drain the DPB: every still-buffered picture becomes display-ready and every id is + /// released. The session calls this at teardown or a stream discontinuity. + /// + /// The 8.2.1/8.2.5 decoding state is discarded with the pictures; planning resumes + /// only at an IDR ([`PlanError::AwaitingIdr`] until then). Parameter sets survive — + /// per 7.4.1.2 they persist until replaced. + pub fn flush(&mut self) -> DpbUpdate { + let mut removed = mem::take(&mut self.reported_live); + removed.extend(self.live_ids()); + self.drain_dpb(); + + self.prev_ref_pic_info = Default::default(); + self.prev_pic_info = Default::default(); + self.max_long_term_frame_idx = Default::default(); + self.negotiation_info = Default::default(); + self.awaiting_idr = true; + + DpbUpdate { + stored: None, + outputs: mem::take(&mut self.pending_outputs), + removed: removed.into_iter().collect(), + } + } + + /// The envelope gate: punktfunk clients only decode punktfunk hosts, and no host + /// ever emits interlaced video or separate-colour-plane coding. + fn check_envelope(sps: &Sps) -> Result<(), PlanError> { + if !sps.frame_mbs_only_flag { + return Err(PlanError::OutsideEnvelope( + "interlaced stream (frame_mbs_only_flag == 0)", + )); + } + if sps.separate_colour_plane_flag { + return Err(PlanError::OutsideEnvelope( + "separate colour plane coding (separate_colour_plane_flag == 1)", + )); + } + // A.3.1 caps the DPB at 16 frames; the only route past the cap is the VUI's + // max_dec_frame_buffering, an unbounded ue(v) the vendored parser reads + // uncapped. No hardware decoder implements a deeper DPB — a larger value is a + // corrupt (or hostile) VUI, not a feature request — and backends size real + // slot pools from this number, so it is gated here, at SPS activation. + if sps.max_dpb_frames() > 16 { + return Err(PlanError::OutsideEnvelope( + "DPB deeper than 16 frames (max_dec_frame_buffering)", + )); + } + Ok(()) + } + + /// Map a vendored slice-header parse failure, sniffing the missing-PPS message so it + /// surfaces as [`PlanError::NoActiveParamSet`]. The prefix match is best-effort: if + /// an upstream re-sync rewords it, the error degrades to `Parse`, not silence. + fn slice_parse_error(err: String) -> PlanError { + match err.strip_prefix("Could not get PPS for pic_parameter_set_id ") { + Some(id) => PlanError::NoActiveParamSet { + pps_id: id.trim().parse().unwrap_or(0), + }, + None => PlanError::Parse(err), + } + } + + /// Ids of every picture the DPB currently holds (non-existing gap placeholders carry + /// no id and are invisible to backends by design). + fn live_ids(&self) -> BTreeSet { + self.dpb + .entries() + .iter() + .filter_map(|entry| entry.reference) + .collect() + } + + fn compute_pic_order_count( + &mut self, + pic: &mut PictureData, + sps: &Sps, + ) -> Result<(), PlanError> { + match pic.pic_order_cnt_type { + // Spec 8.2.1.1 + 0 => { + let prev_pic_order_cnt_msb; + let prev_pic_order_cnt_lsb; + + if matches!(pic.is_idr, IsIdr::Yes { .. }) { + prev_pic_order_cnt_lsb = 0; + prev_pic_order_cnt_msb = 0; + } else if self.prev_ref_pic_info.has_mmco_5 { + if !matches!(self.prev_ref_pic_info.field, Field::Bottom) { + prev_pic_order_cnt_msb = 0; + prev_pic_order_cnt_lsb = self.prev_ref_pic_info.top_field_order_cnt; + } else { + prev_pic_order_cnt_msb = 0; + prev_pic_order_cnt_lsb = 0; + } + } else { + prev_pic_order_cnt_msb = self.prev_ref_pic_info.pic_order_cnt_msb; + prev_pic_order_cnt_lsb = self.prev_ref_pic_info.pic_order_cnt_lsb; + } + + let max_pic_order_cnt_lsb = 1 << (sps.log2_max_pic_order_cnt_lsb_minus4 + 4); + + // 8.2.1.1 compares against prevPicOrderCntLsb — the DERIVED value, + // which is 0 or the previous TopFieldOrderCnt after an MMCO5 — in BOTH + // wrap branches. Upstream reads the raw stored lsb in the first branch; + // deliberate divergence from upstream here, in favour of spec + // conformance. + pic.pic_order_cnt_msb = if (pic.pic_order_cnt_lsb < prev_pic_order_cnt_lsb) + && (prev_pic_order_cnt_lsb - pic.pic_order_cnt_lsb >= max_pic_order_cnt_lsb / 2) + { + prev_pic_order_cnt_msb + max_pic_order_cnt_lsb + } else if (pic.pic_order_cnt_lsb > prev_pic_order_cnt_lsb) + && (pic.pic_order_cnt_lsb - prev_pic_order_cnt_lsb > max_pic_order_cnt_lsb / 2) + { + prev_pic_order_cnt_msb - max_pic_order_cnt_lsb + } else { + prev_pic_order_cnt_msb + }; + + if !matches!(pic.field, Field::Bottom) { + pic.top_field_order_cnt = pic.pic_order_cnt_msb + pic.pic_order_cnt_lsb; + } + + if !matches!(pic.field, Field::Top) { + if matches!(pic.field, Field::Frame) { + pic.bottom_field_order_cnt = + pic.top_field_order_cnt + pic.delta_pic_order_cnt_bottom; + } else { + pic.bottom_field_order_cnt = pic.pic_order_cnt_msb + pic.pic_order_cnt_lsb; + } + } + } + + // Spec 8.2.1.2 + 1 => { + if self.prev_pic_info.has_mmco_5 { + self.prev_pic_info.frame_num_offset = 0; + } + + if matches!(pic.is_idr, IsIdr::Yes { .. }) { + pic.frame_num_offset = 0; + } else if self.prev_pic_info.frame_num > pic.frame_num { + pic.frame_num_offset = + self.prev_pic_info.frame_num_offset + sps.max_frame_num(); + } else { + pic.frame_num_offset = self.prev_pic_info.frame_num_offset; + } + + let mut abs_frame_num = if sps.num_ref_frames_in_pic_order_cnt_cycle != 0 { + pic.frame_num_offset + pic.frame_num + } else { + 0 + }; + + if pic.nal_ref_idc == 0 && abs_frame_num > 0 { + abs_frame_num -= 1; + } + + let mut expected_pic_order_cnt = 0; + + if abs_frame_num > 0 { + if sps.num_ref_frames_in_pic_order_cnt_cycle == 0 { + return Err(PlanError::Parse( + "invalid num_ref_frames_in_pic_order_cnt_cycle".into(), + )); + } + + let pic_order_cnt_cycle_cnt = + (abs_frame_num - 1) / sps.num_ref_frames_in_pic_order_cnt_cycle as u32; + let frame_num_in_pic_order_cnt_cycle = + (abs_frame_num - 1) % sps.num_ref_frames_in_pic_order_cnt_cycle as u32; + expected_pic_order_cnt = + pic_order_cnt_cycle_cnt as i32 * sps.expected_delta_per_pic_order_cnt_cycle; + + assert!(frame_num_in_pic_order_cnt_cycle < 255); + + // NOTE: upstream sums the full cycle here where 8.2.1.2 sums + // frame_num_in_pic_order_cnt_cycle + 1 entries; ported as-is — + // punktfunk hosts emit pic_order_cnt_type 0 only. + let cycle = usize::from(sps.num_ref_frames_in_pic_order_cnt_cycle); + for offset in &sps.offset_for_ref_frame[..cycle] { + expected_pic_order_cnt += offset; + } + } + + if pic.nal_ref_idc == 0 { + expected_pic_order_cnt += sps.offset_for_non_ref_pic; + } + + if matches!(pic.field, Field::Frame) { + pic.top_field_order_cnt = expected_pic_order_cnt + pic.delta_pic_order_cnt0; + + pic.bottom_field_order_cnt = pic.top_field_order_cnt + + sps.offset_for_top_to_bottom_field + + pic.delta_pic_order_cnt1; + } else if !matches!(pic.field, Field::Bottom) { + pic.top_field_order_cnt = expected_pic_order_cnt + pic.delta_pic_order_cnt0; + } else { + pic.bottom_field_order_cnt = expected_pic_order_cnt + + sps.offset_for_top_to_bottom_field + + pic.delta_pic_order_cnt0; + } + } + + // Spec 8.2.1.3 + 2 => { + if self.prev_pic_info.has_mmco_5 { + self.prev_pic_info.frame_num_offset = 0; + } + + if matches!(pic.is_idr, IsIdr::Yes { .. }) { + pic.frame_num_offset = 0; + } else if self.prev_pic_info.frame_num > pic.frame_num { + pic.frame_num_offset = + self.prev_pic_info.frame_num_offset + sps.max_frame_num(); + } else { + pic.frame_num_offset = self.prev_pic_info.frame_num_offset; + } + + let pic_order_cnt = if matches!(pic.is_idr, IsIdr::Yes { .. }) { + 0 + } else if pic.nal_ref_idc == 0 { + 2 * (pic.frame_num_offset + pic.frame_num) as i32 - 1 + } else { + 2 * (pic.frame_num_offset + pic.frame_num) as i32 + }; + + if matches!(pic.field, Field::Frame | Field::Top) { + pic.top_field_order_cnt = pic_order_cnt; + } + if matches!(pic.field, Field::Frame | Field::Bottom) { + pic.bottom_field_order_cnt = pic_order_cnt; + } + } + + _ => { + return Err(PlanError::Parse(format!( + "invalid pic_order_cnt_type: {}", + sps.pic_order_cnt_type + ))) + } + } + + match pic.field { + Field::Frame => { + pic.pic_order_cnt = + std::cmp::min(pic.top_field_order_cnt, pic.bottom_field_order_cnt); + } + Field::Top => { + pic.pic_order_cnt = pic.top_field_order_cnt; + } + Field::Bottom => { + pic.pic_order_cnt = pic.bottom_field_order_cnt; + } + } + + Ok(()) + } + + /// Queue the frames that the C.4.5.3 bumping process declares ready for output. + fn bump_as_needed(&mut self, current_pic: &PictureData) { + let bumped = self.dpb.bump_as_needed(current_pic); + self.pending_outputs.extend(bumped.into_iter().flatten()); + } + + /// Queue all frames still present in the DPB for output. + fn drain_dpb(&mut self) { + let pics = self.dpb.drain(); + self.pending_outputs.extend(pics.into_iter().flatten()); + } + + /// Find the first field for the picture started by `hdr`, if any. Always `None` + /// under the envelope gate (the DPB never enters interlaced mode); kept as ported so + /// the upstream diff stays small. + fn find_first_field( + &self, + hdr: &SliceHeader, + ) -> Result, String> { + let mut prev_field = None; + + if self.dpb.interlaced() { + if let Some(last_dpb_entry) = self.dpb.entries().last() { + // Use the last entry in the DPB + let last_pic = last_dpb_entry.pic.borrow(); + + // If the picture is interlaced but doesn't have its other field set yet, + // then it must be the first field. + if !matches!(last_pic.field, Field::Frame) + && matches!(last_pic.field_rank(), FieldRank::Single) + { + if let Some(id) = &last_dpb_entry.reference { + // Still waiting for the second field + prev_field = Some((last_dpb_entry.pic.clone(), *id)); + } + } + } + } + + let prev_field = match prev_field { + None => return Ok(None), + Some(prev_field) => prev_field, + }; + + let prev_field_pic = prev_field.0.borrow(); + + if prev_field_pic.frame_num != u32::from(hdr.frame_num) { + return Err(format!( + "the previous field's frame_num value {} differs from the current one's {}", + prev_field_pic.frame_num, hdr.frame_num + )); + } + + let cur_field = if hdr.bottom_field_flag { + Field::Bottom + } else { + Field::Top + }; + + if !hdr.field_pic_flag || cur_field == prev_field_pic.field { + let field = prev_field_pic.field; + return Err(format!( + "expected complementary field {:?}, got {:?}", + field.opposite(), + field + )); + } + + drop(prev_field_pic); + Ok(Some(prev_field)) + } + + // 8.2.4.3.1 Modification process of reference picture lists for short-term + // reference pictures + #[allow(clippy::too_many_arguments)] + fn short_term_pic_list_modification<'a>( + cur_pic: &PictureData, + dpb: &'a Dpb, + ref_pic_list_x: &mut DpbPicRefList<'a, PicId>, + num_ref_idx_lx_active_minus1: u8, + max_pic_num: i32, + rplm: &RefPicListModification, + pic_num_lx_pred: &mut i32, + ref_idx_lx: &mut usize, + ) -> Result<(), String> { + let pic_num_lx_no_wrap; + let abs_diff_pic_num = rplm.abs_diff_pic_num_minus1 as i32 + 1; + let modification_of_pic_nums_idc = rplm.modification_of_pic_nums_idc; + + if modification_of_pic_nums_idc == 0 { + if *pic_num_lx_pred - abs_diff_pic_num < 0 { + pic_num_lx_no_wrap = *pic_num_lx_pred - abs_diff_pic_num + max_pic_num; + } else { + pic_num_lx_no_wrap = *pic_num_lx_pred - abs_diff_pic_num; + } + } else if modification_of_pic_nums_idc == 1 { + if *pic_num_lx_pred + abs_diff_pic_num >= max_pic_num { + pic_num_lx_no_wrap = *pic_num_lx_pred + abs_diff_pic_num - max_pic_num; + } else { + pic_num_lx_no_wrap = *pic_num_lx_pred + abs_diff_pic_num; + } + } else { + return Err(format!( + "unexpected value for modification_of_pic_nums_idc {modification_of_pic_nums_idc:?}" + )); + } + + *pic_num_lx_pred = pic_num_lx_no_wrap; + + let pic_num_lx = if pic_num_lx_no_wrap > cur_pic.pic_num { + pic_num_lx_no_wrap - max_pic_num + } else { + pic_num_lx_no_wrap + }; + + let handle = dpb + .find_short_term_with_pic_num(pic_num_lx) + .ok_or_else(|| format!("no ShortTerm reference found with pic_num {pic_num_lx}"))?; + + if *ref_idx_lx >= ref_pic_list_x.len() { + return Err("invalid ref_idx_lx index".into()); + } + ref_pic_list_x.insert(*ref_idx_lx, handle); + *ref_idx_lx += 1; + + let mut nidx = *ref_idx_lx; + + for cidx in *ref_idx_lx..=usize::from(num_ref_idx_lx_active_minus1) + 1 { + if cidx == ref_pic_list_x.len() { + break; + } + + let target = &ref_pic_list_x[cidx].pic; + + if target.borrow().pic_num_f(max_pic_num) != pic_num_lx { + ref_pic_list_x[nidx] = ref_pic_list_x[cidx]; + nidx += 1; + } + } + + while ref_pic_list_x.len() > (usize::from(num_ref_idx_lx_active_minus1) + 1) { + ref_pic_list_x.pop(); + } + + Ok(()) + } + + // 8.2.4.3.2 Modification process of reference picture lists for long-term + // reference pictures + fn long_term_pic_list_modification<'a>( + dpb: &'a Dpb, + ref_pic_list_x: &mut DpbPicRefList<'a, PicId>, + num_ref_idx_lx_active_minus1: u8, + max_long_term_frame_idx: MaxLongTermFrameIdx, + rplm: &RefPicListModification, + ref_idx_lx: &mut usize, + ) -> Result<(), String> { + let long_term_pic_num = rplm.long_term_pic_num; + + let handle = dpb + .find_long_term_with_long_term_pic_num(long_term_pic_num) + .ok_or_else(|| { + format!("no LongTerm reference found with long_term_pic_num {long_term_pic_num}") + })?; + + if *ref_idx_lx >= ref_pic_list_x.len() { + return Err("invalid ref_idx_lx index".into()); + } + ref_pic_list_x.insert(*ref_idx_lx, handle); + *ref_idx_lx += 1; + + let mut nidx = *ref_idx_lx; + + for cidx in *ref_idx_lx..=usize::from(num_ref_idx_lx_active_minus1) + 1 { + if cidx == ref_pic_list_x.len() { + break; + } + + let target = &ref_pic_list_x[cidx].pic; + if target.borrow().long_term_pic_num_f(max_long_term_frame_idx) != long_term_pic_num { + ref_pic_list_x[nidx] = ref_pic_list_x[cidx]; + nidx += 1; + } + } + + while ref_pic_list_x.len() > (usize::from(num_ref_idx_lx_active_minus1) + 1) { + ref_pic_list_x.pop(); + } + + Ok(()) + } + + fn modify_ref_pic_list( + &self, + cur_pic: &PictureData, + hdr: &SliceHeader, + ref_pic_list_type: RefPicList, + ref_pic_list_indices: &[usize], + ) -> Result, String> { + let (ref_pic_list_modification_flag_lx, num_ref_idx_lx_active_minus1, rplm) = + match ref_pic_list_type { + RefPicList::RefPicList0 => ( + hdr.ref_pic_list_modification_flag_l0, + hdr.num_ref_idx_l0_active_minus1, + &hdr.ref_pic_list_modification_l0, + ), + RefPicList::RefPicList1 => ( + hdr.ref_pic_list_modification_flag_l1, + hdr.num_ref_idx_l1_active_minus1, + &hdr.ref_pic_list_modification_l1, + ), + }; + + let mut ref_pic_list: Vec<_> = ref_pic_list_indices + .iter() + .map(|&i| &self.dpb.entries()[i]) + .take(usize::from(num_ref_idx_lx_active_minus1) + 1) + .collect(); + + if !ref_pic_list_modification_flag_lx { + return Ok(ref_pic_list); + } + + let mut pic_num_lx_pred = cur_pic.pic_num; + let mut ref_idx_lx = 0; + + for modification in rplm { + let idc = modification.modification_of_pic_nums_idc; + + match idc { + 0 | 1 => { + Self::short_term_pic_list_modification( + cur_pic, + &self.dpb, + &mut ref_pic_list, + num_ref_idx_lx_active_minus1, + hdr.max_pic_num as i32, + modification, + &mut pic_num_lx_pred, + &mut ref_idx_lx, + )?; + } + 2 => Self::long_term_pic_list_modification( + &self.dpb, + &mut ref_pic_list, + num_ref_idx_lx_active_minus1, + self.max_long_term_frame_idx, + modification, + &mut ref_idx_lx, + )?, + 3 => break, + _ => return Err(format!("unexpected modification_of_pic_nums_idc {idc:?}")), + } + } + + Ok(ref_pic_list) + } + + /// [`Self::modify_ref_pic_list`], degrading a failed modification to a + /// [`PlanWarning::MissingReference`] plus the unmodified 8.2.4.2 initial list — + /// upstream aborts the decode here, but a modification naming a lost picture is + /// punktfunk's cue to conceal and request recovery, not to kill the session. + fn modified_or_initial_list( + &self, + cur_pic: &PictureData, + hdr: &SliceHeader, + ref_pic_list_type: RefPicList, + ref_pic_list_indices: &[usize], + warnings: &mut Vec, + ) -> DpbPicRefList<'_, PicId> { + match self.modify_ref_pic_list(cur_pic, hdr, ref_pic_list_type, ref_pic_list_indices) { + Ok(list) => list, + Err(detail) => { + warnings.push(PlanWarning::MissingReference { + context: "ref_pic_list_modification", + detail, + }); + let num_ref_idx_lx_active_minus1 = match ref_pic_list_type { + RefPicList::RefPicList0 => hdr.num_ref_idx_l0_active_minus1, + RefPicList::RefPicList1 => hdr.num_ref_idx_l1_active_minus1, + }; + ref_pic_list_indices + .iter() + .map(|&i| &self.dpb.entries()[i]) + .take(usize::from(num_ref_idx_lx_active_minus1) + 1) + .collect() + } + } + } + + /// Generate RefPicList0 and RefPicList1 for one slice (8.2.4), already converted to + /// backend-facing [`RefPic`]s. + fn create_ref_pic_lists( + &self, + cur_pic: &PictureData, + hdr: &SliceHeader, + ref_pic_lists: &ReferencePicLists, + warnings: &mut Vec, + ) -> (Vec, Vec) { + let ref_pic_list0 = match hdr.slice_type { + SliceType::P | SliceType::Sp => self.modified_or_initial_list( + cur_pic, + hdr, + RefPicList::RefPicList0, + &ref_pic_lists.ref_pic_list_p0, + warnings, + ), + SliceType::B => self.modified_or_initial_list( + cur_pic, + hdr, + RefPicList::RefPicList0, + &ref_pic_lists.ref_pic_list_b0, + warnings, + ), + _ => Vec::new(), + }; + + let ref_pic_list1 = match hdr.slice_type { + SliceType::B => self.modified_or_initial_list( + cur_pic, + hdr, + RefPicList::RefPicList1, + &ref_pic_lists.ref_pic_list_b1, + warnings, + ), + _ => Vec::new(), + }; + + ( + Self::to_ref_pics(&ref_pic_list0, warnings), + Self::to_ref_pics(&ref_pic_list1, warnings), + ) + } + + /// Convert one reference list to backend-facing [`RefPic`]s, preserving list + /// positions: every ref_idx in the slice syntax indexes the returned Vec 1:1. + /// + /// A non-existing picture (8.2.5.2 gap placeholder) has no id a backend could + /// resolve, so it is substituted IN PLACE by the nearest existing reference in + /// list order (the previous existing entry, else the first existing one) — + /// stable-but-wrong concealment, flagged via [`PlanWarning::MissingReference`]. + /// Compacting instead would shift every subsequent ref_idx and make the decoder + /// predict from the wrong pictures. Only a list with no existing reference at all + /// collapses to empty (the caller warns on that separately). + fn to_ref_pics(list: &[&DpbEntry], warnings: &mut Vec) -> Vec { + // Each slot: the resolvable entry plus its picture's frame_num (a long-term + // substitute is re-labelled short-term, so its frame_num is needed). + let mut slots: Vec> = Vec::with_capacity(list.len()); + for entry in list { + let pic = entry.pic.borrow(); + match entry.reference { + Some(id) => { + let is_long_term = matches!(pic.reference(), Reference::LongTerm); + let frame_num_or_lt_idx = if is_long_term { + // long_term_frame_idx is ue(v)-coded; the spec bounds it (<= 15 + // via max_long_term_frame_idx) but the parser does not, so + // saturate rather than truncate — unreachable-in-practice + // hardening. + u16::try_from(pic.long_term_frame_idx).unwrap_or(u16::MAX) + } else { + pic.frame_num as u16 + }; + slots.push(Some(( + RefPic { + id, + top_field_order_cnt: pic.top_field_order_cnt, + bottom_field_order_cnt: pic.bottom_field_order_cnt, + is_long_term, + frame_num_or_lt_idx, + }, + pic.frame_num as u16, + ))); + } + None => { + warnings.push(PlanWarning::MissingReference { + context: "non-existing picture (frame_num gap placeholder) in list", + detail: format!("frame_num {}", pic.frame_num), + }); + slots.push(None); + } + } + } + + let first_existing = slots.iter().flatten().next().copied(); + let mut out = Vec::with_capacity(slots.len()); + let mut prev_existing: Option<(RefPic, u16)> = None; + for slot in &slots { + match slot { + Some(real) => { + prev_existing = Some(*real); + out.push(real.0); + } + None => { + // Index mapping preserved; the substituted entry is stable-but- + // wrong concealment. Placeholders are short-term (8.2.5.2), so the + // substitute is labelled short-term with its own frame_num. + if let Some((substitute, frame_num)) = prev_existing.or(first_existing) { + out.push(RefPic { + id: substitute.id, + top_field_order_cnt: substitute.top_field_order_cnt, + bottom_field_order_cnt: substitute.bottom_field_order_cnt, + is_long_term: false, + frame_num_or_lt_idx: frame_num, + }); + } + } + } + } + out + } + + fn handle_memory_management_ops(&mut self, pic: &mut PictureData) -> Result<(), MmcoError> { + let markings = pic.ref_pic_marking.clone(); + + for marking in &markings.inner { + match marking.memory_management_control_operation { + 0 => break, + 1 => self.dpb.mmco_op_1(pic, marking)?, + 2 => self.dpb.mmco_op_2(pic, marking)?, + 3 => self.dpb.mmco_op_3(pic, marking)?, + 4 => self.max_long_term_frame_idx = self.dpb.mmco_op_4(marking), + 5 => self.max_long_term_frame_idx = self.dpb.mmco_op_5(pic), + 6 => self.dpb.mmco_op_6(pic, marking), + other => return Err(MmcoError::UnknownMmco(other)), + } + } + + Ok(()) + } + + fn reference_pic_marking(&mut self, pic: &mut PictureData, sps: &Sps) -> Result<(), MmcoError> { + /* 8.2.5.1 */ + if matches!(pic.is_idr, IsIdr::Yes { .. }) { + self.dpb.mark_all_as_unused_for_ref(); + + if pic.ref_pic_marking.long_term_reference_flag { + pic.set_reference(Reference::LongTerm, false); + pic.long_term_frame_idx = 0; + self.max_long_term_frame_idx = MaxLongTermFrameIdx::Idx(0); + } else { + pic.set_reference(Reference::ShortTerm, false); + self.max_long_term_frame_idx = MaxLongTermFrameIdx::NoLongTermFrameIndices; + } + + return Ok(()); + } + + if pic.ref_pic_marking.adaptive_ref_pic_marking_mode_flag { + self.handle_memory_management_ops(pic)?; + } else { + self.dpb.sliding_window_marking(pic, sps); + } + + Ok(()) + } + + // Apply the parameters of `sps` to the planning state. + fn apply_sps(&mut self, sps: &Sps) { + self.negotiation_info = NegotiationInfo::from(sps); + + let max_dpb_frames = sps.max_dpb_frames(); + let interlaced = !sps.frame_mbs_only_flag; + let max_num_order_frames = sps.max_num_order_frames() as usize; + let max_num_reorder_frames = if max_num_order_frames > max_dpb_frames { + 0 + } else { + max_num_order_frames + }; + + self.dpb.set_limits(max_dpb_frames, max_num_reorder_frames); + self.dpb.set_interlaced(interlaced); + } + + fn negotiation_possible(sps: &Sps, old_negotiation_info: &NegotiationInfo) -> bool { + let negotiation_info = NegotiationInfo::from(sps); + *old_negotiation_info != negotiation_info + } + + fn renegotiate_if_needed(&mut self, sps: &Sps) -> Result<(), PlanError> { + if Self::negotiation_possible(sps, &self.negotiation_info) { + Self::check_envelope(sps)?; + // Make sure all the frames planned so far are display-ready before the + // stream parameters change under them. + self.drain_dpb(); + self.apply_sps(sps); + } + + Ok(()) + } + + fn handle_frame_num_gap( + &mut self, + sps: &Sps, + frame_num: u32, + warnings: &mut Vec, + ) -> Result<(), PlanError> { + if self.dpb.is_empty() { + return Ok(()); + } + + trace!("frame_num gap detected"); + + // Upstream refuses the gap when gaps_in_frame_num_value_allowed_flag is unset. + // Here the caller has already emitted PlanWarning::FrameNumGap and the 8.2.5.2 + // process runs regardless: losing a reference AU on the wire must degrade to + // concealment + recovery, and inserting the non-existing pictures keeps the + // frame_num/pic_num bookkeeping of everything that follows spec-true. + let mut unused_short_term_frame_num = + (self.prev_ref_pic_info.frame_num + 1) % sps.max_frame_num(); + while unused_short_term_frame_num != frame_num { + let max_frame_num = sps.max_frame_num(); + + let mut pic = PictureData::new_non_existing(unused_short_term_frame_num, 0); + self.compute_pic_order_count(&mut pic, sps)?; + + self.dpb + .update_pic_nums(unused_short_term_frame_num, max_frame_num, &pic); + + self.dpb.sliding_window_marking(&mut pic, sps); + + self.bump_as_needed(&pic); + + // Interlaced field-splitting dropped: the envelope gate keeps the DPB in + // progressive mode. + if let Err(err) = self.dpb.store_picture(pic.into_rc(), None) { + // A full DPB must not error the AU (warnings-not-errors contract): + // stop inserting placeholders. The pic_num bookkeeping degrades from + // here, which the recovery the warning triggers will heal. + warnings.push(PlanWarning::MissingReference { + context: "frame_num gap placeholder dropped (DPB full)", + detail: err.to_string(), + }); + break; + } + + unused_short_term_frame_num += 1; + unused_short_term_frame_num %= max_frame_num; + } + + Ok(()) + } + + /// Init the current picture being planned. + fn init_current_pic( + &mut self, + slice: &Slice, + sps: &Sps, + first_field: Option<&RcPictureData>, + ) -> Result { + let mut pic = PictureData::new_from_slice(slice, sps, 0, first_field); + self.compute_pic_order_count(&mut pic, sps)?; + + if matches!(pic.is_idr, IsIdr::Yes { .. }) { + // C.4.5.3 "Bumping process" + // The bumping process is invoked in the following cases: + // Clause 2: + // The current picture is an IDR picture and + // no_output_of_prior_pics_flag is not equal to 1 and is not + // inferred to be equal to 1, as specified in clause C.4.4. + if !pic.ref_pic_marking.no_output_of_prior_pics_flag { + self.drain_dpb(); + } else { + // C.4.4 When no_output_of_prior_pics_flag is equal to 1 or is + // inferred to be equal to 1, all frame buffers in the DPB are + // emptied without output of the pictures they contain, and DPB + // fullness is set to 0. + self.dpb.clear(); + } + } + + self.dpb + .update_pic_nums(u32::from(slice.header.frame_num), sps.max_frame_num(), &pic); + + Ok(pic) + } + + /// Called once per picture, on its first slice. + fn begin_picture( + &mut self, + slice: &Slice, + warnings: &mut Vec, + ) -> Result { + let hdr = &slice.header; + let pps = Rc::clone(self.parser.get_pps(hdr.pic_parameter_set_id).ok_or( + PlanError::NoActiveParamSet { + pps_id: hdr.pic_parameter_set_id, + }, + )?); + + // A picture's SPS may require renegotiation. + self.renegotiate_if_needed(&pps.sps)?; + + let first_field = self.find_first_field(hdr).map_err(PlanError::Parse)?; + + // Upstream secures the backend picture here; the plan's equivalent is the id + // backends will allocate against. + let id = self.next_pic_id; + self.next_pic_id += 1; + + if slice.nalu.header.idr_pic_flag { + self.prev_ref_pic_info.frame_num = 0; + } + + let frame_num = u32::from(hdr.frame_num); + + let current_macroblock = match pps.sps.separate_colour_plane_flag { + true => CurrentMacroblockTracking::SeparateColorPlane(Default::default()), + false => CurrentMacroblockTracking::NonSeparateColorPlane(None), + }; + + if frame_num != self.prev_ref_pic_info.frame_num + && frame_num != (self.prev_ref_pic_info.frame_num + 1) % pps.sps.max_frame_num() + { + if !self.dpb.is_empty() { + warnings.push(PlanWarning::FrameNumGap { + expected: ((self.prev_ref_pic_info.frame_num + 1) % pps.sps.max_frame_num()) + as u16, + got: hdr.frame_num, + }); + } + self.handle_frame_num_gap(&pps.sps, frame_num, warnings)?; + } + + let pic = self.init_current_pic(slice, &pps.sps, first_field.as_ref().map(|f| &f.0))?; + let ref_pic_lists = self.dpb.build_ref_pic_lists(&pic); + // Taken here, beside the lists 8.2.4 derives from the same DPB state, and + // never later: `finish_picture` runs 8.2.5's marking and stores the picture, + // which is the DPB the NEXT AU decodes against, not this one. + let dpb_refs = self.dpb_snapshot(); + + Ok(CurrentPicState { + pic, + first_slice_pps: Rc::clone(&pps), + pps, + id, + ref_pic_lists, + dpb_refs, + current_macroblock, + }) + } + + /// The marked DPB as [`AuPlan::dpb_refs`] reports it. + /// + /// The filter is the pair of conditions a backend needs to name a surface: the + /// picture is marked used for reference (short- or long-term — anything the + /// sliding window or an MMCO has unmarked is skipped, even while the DPB still + /// holds it for output), and it carries a [`PicId`], which 8.2.5.2's non-existing + /// gap placeholders deliberately do not. + fn dpb_snapshot(&self) -> Vec { + self.dpb + .entries() + .iter() + .filter_map(|entry| { + let id = entry.reference?; + let pic = entry.pic.borrow(); + let is_long_term = match pic.reference() { + Reference::LongTerm => true, + Reference::ShortTerm => false, + Reference::None => return None, + }; + Some(RefPic { + id, + top_field_order_cnt: pic.top_field_order_cnt, + bottom_field_order_cnt: pic.bottom_field_order_cnt, + is_long_term, + // The same pair-key derivation `to_ref_pics` makes, and for the + // same reason: DXVA's `FrameNumList` carries `LongTermFrameIdx` + // for a long-term reference and `frame_num` for a short-term one. + frame_num_or_lt_idx: if is_long_term { + u16::try_from(pic.long_term_frame_idx).unwrap_or(u16::MAX) + } else { + pic.frame_num as u16 + }, + }) + }) + .collect() + } + + // Check whether first_mb_in_slice increases monotonically for the current + // picture as required by 7.4.3. + fn check_first_mb_in_slice(current_macroblock: &mut CurrentMacroblockTracking, slice: &Slice) { + let first_mb_in_slice = slice.header.first_mb_in_slice; + match current_macroblock { + CurrentMacroblockTracking::SeparateColorPlane(current_macroblock) => { + match current_macroblock.entry(slice.header.colour_plane_id) { + Entry::Vacant(entry) => { + entry.insert(first_mb_in_slice); + } + Entry::Occupied(mut entry) => { + let current_macroblock = entry.get_mut(); + if first_mb_in_slice <= *current_macroblock { + trace!( + "first_mb_in_slice does not increase monotonically, expect \ + corrupted output" + ); + } + *current_macroblock = first_mb_in_slice; + } + } + } + CurrentMacroblockTracking::NonSeparateColorPlane(current_macroblock) => { + if current_macroblock.is_some_and(|current| first_mb_in_slice <= current) { + trace!( + "first_mb_in_slice does not increase monotonically, expect corrupted \ + output" + ); + } + *current_macroblock = Some(first_mb_in_slice); + } + } + } + + /// Handle one slice of the current picture (upstream: `handle_slice`). + fn plan_slice( + &self, + cur: &mut CurrentPicState, + slice: Slice, + data: Range, + warnings: &mut Vec, + ) -> Result { + Self::check_first_mb_in_slice(&mut cur.current_macroblock, &slice); + + // A slice can technically refer to another PPS. + let pps = self + .parser + .get_pps(slice.header.pic_parameter_set_id) + .ok_or(PlanError::NoActiveParamSet { + pps_id: slice.header.pic_parameter_set_id, + })?; + cur.pps = Rc::clone(pps); + + // Make sure that no negotiation is possible mid-picture. How could it? + // We'd lose the context of the previous slices. + if Self::negotiation_possible(&cur.pps.sps, &self.negotiation_info) { + return Err(PlanError::Parse( + "invalid stream: inter-picture renegotiation requested".into(), + )); + } + + let (ref_list0, ref_list1) = + self.create_ref_pic_lists(&cur.pic, &slice.header, &cur.ref_pic_lists, warnings); + + // 8.2.4.2.1: an inter slice shall have at least one usable reference. Ending up + // empty (every candidate lost or a gap placeholder) is undecodable-as-intended, + // and it can happen without any per-entry warning when the DPB holds only + // non-existing pictures — flag it so the session requests recovery. + let slice_type = slice.header.slice_type; + if matches!(slice_type, SliceType::P | SliceType::Sp | SliceType::B) && ref_list0.is_empty() + { + warnings.push(PlanWarning::MissingReference { + context: "inter slice with no usable RefPicList0", + detail: format!("slice_type {slice_type:?}"), + }); + } + if matches!(slice_type, SliceType::B) && ref_list1.is_empty() { + warnings.push(PlanWarning::MissingReference { + context: "B slice with no usable RefPicList1", + detail: format!("slice_type {slice_type:?}"), + }); + } + + Ok(SlicePlan { + data, + header: slice.header, + ref_list0, + ref_list1, + }) + } + + /// Adds the picture to the output queue when it could not be added to the DPB. + fn add_to_ready_queue(&mut self, pic: PictureData, id: PicId) { + if matches!(pic.field, Field::Frame) { + self.pending_outputs.push(id); + } else if let FieldRank::Second(..) = pic.field_rank() { + self.pending_outputs.push(id); + } + } + + fn finish_picture( + &mut self, + cur: CurrentPicState, + warnings: &mut Vec, + ) -> Result { + let CurrentPicState { + mut pic, pps, id, .. + } = cur; + + if matches!(pic.reference(), Reference::ShortTerm | Reference::LongTerm) { + // Upstream aborts on a failed MMCO; an op naming a picture the DPB lost is + // a concealment signal here, and the remaining marking state stays usable. + if let Err(err) = self.reference_pic_marking(&mut pic, &pps.sps) { + warnings.push(PlanWarning::MissingReference { + context: "reference picture marking (MMCO)", + detail: err.to_string(), + }); + } + self.prev_ref_pic_info.fill(&pic); + } + + self.prev_pic_info.fill(&pic); + + if pic.has_mmco_5 { + warnings.push(PlanWarning::Mmco5Rebase); + // C.4.5.3 "Bumping process" + // The bumping process is invoked in the following cases: + // Clause 3: + // The current picture has memory_management_control_operation equal + // to 5, as specified in clause C.4.4. + self.drain_dpb(); + } + + // Bump the DPB as per C.4.5.3 to cover clauses 1, 4, 5 and 6. + self.bump_as_needed(&pic); + + // C.4.5.1, C.4.5.2 + // If the current decoded picture is the second field of a complementary + // reference field pair, add to DPB. + // C.4.5.1 + // For a reference decoded picture, the "bumping" process is invoked + // repeatedly until there is an empty frame buffer, by which point it is + // added to the DPB. Notice that Dpb::needs_bumping already accounts for + // this. + // C.4.5.2 + // For a non-reference decoded picture, if there is empty frame buffer + // after bumping the smaller POC, add to DPB. Otherwise, add it to the + // output queue. + if pic.is_second_field_of_complementary_ref_pair() + || pic.is_ref() + || self.dpb.has_empty_frame_buffer() + { + // Upstream splits frames into complementary field pairs when the DPB is in + // interlaced mode; the envelope gate keeps that path unreachable. + self.dpb + .store_picture(pic.into_rc(), Some(id)) + .map_err(|err| PlanError::Parse(err.to_string()))?; + } else { + self.add_to_ready_queue(pic, id); + } + + Ok(id) + } + + fn picture_plan(cur: &CurrentPicState, recovery_point: Option) -> PicturePlan { + let pic = &cur.pic; + // The first slice's PPS defines the picture's parameters (upstream's + // start_picture semantics); `cur.pps` may have drifted to a later slice's. + let sps = &cur.first_slice_pps.sps; + let rect = sps.visible_rectangle(); + + PicturePlan { + is_idr: matches!(pic.is_idr, IsIdr::Yes { .. }), + nal_ref_idc: pic.nal_ref_idc, + is_reference: pic.is_ref(), + frame_num: pic.frame_num as u16, + top_field_order_cnt: pic.top_field_order_cnt, + bottom_field_order_cnt: pic.bottom_field_order_cnt, + pic_order_cnt: pic.pic_order_cnt, + coded_width: sps.width(), + coded_height: sps.height(), + // The vendored `visible_rectangle()` returns the crop OFFSET in `min` and + // the visible SIZE in `max` (not an edge coordinate): subtracting would + // double-count the left/top crop and underflow on large offsets. + display_crop: DisplayCrop { + x: rect.min.x, + y: rect.min.y, + width: rect.max.x, + height: rect.max.y, + }, + // Read unconditionally: the vendored parser builds every SPS from + // `Default`, whose `VuiParams` already holds E.2.1's inferred values + // (2/2/2, limited range), and parsing only overwrites them under the + // present flags — so this IS the spec inference whether or not the + // stream carried a VUI. + colour: ColourDescription { + colour_primaries: sps.vui_parameters.colour_primaries, + transfer_characteristics: sps.vui_parameters.transfer_characteristics, + matrix_coefficients: sps.vui_parameters.matrix_coefficients, + video_full_range: sps.vui_parameters.video_full_range_flag, + }, + profile_idc: sps.profile_idc, + level_idc: sps.level_idc, + bit_depth_luma_minus8: sps.bit_depth_luma_minus8, + bit_depth_chroma_minus8: sps.bit_depth_chroma_minus8, + chroma_format_idc: sps.chroma_format_idc, + max_dpb_frames: sps.max_dpb_frames(), + recovery_point, + } + } +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + use std::rc::Rc; + + use cros_codecs::codec::h264::nalu_writer::NaluWriter; + use cros_codecs::codec::h264::parser::Nalu; + use cros_codecs::codec::h264::parser::NaluType; + use cros_codecs::codec::h264::parser::PpsBuilder; + use cros_codecs::codec::h264::parser::Profile; + use cros_codecs::codec::h264::parser::SpsBuilder; + use cros_codecs::codec::h264::parser::VuiParams; + use cros_codecs::codec::h264::synthesizer::Synthesizer; + + use super::*; + + const TEST_25FPS: &[u8] = + include_bytes!("../vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264"); + // The plain 64x64-I-P-B-P.h264 is a constrained-baseline encode: x264 silently + // dropped the requested B frame (its slices parse as I, P, P). The -high variant of + // the same sequence carries the real B slice. + const TEST_64X64_I_P_B_P_HIGH: &[u8] = + include_bytes!("../vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P-high.h264"); + + /// Test-only AU splitter: the vendored vectors are raw Annex-B streams, while + /// `plan_au` takes the pre-split AUs punktfunk's pump produces. A new AU starts at a + /// non-slice NALU following slices, or at a slice with first_mb_in_slice == 0 + /// (whose ue(v) encoding makes the first RBSP bit 1) when the current AU already + /// has slices. + fn split_into_aus(stream: &[u8]) -> Vec<&[u8]> { + let mut aus = Vec::new(); + let mut cursor = Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + + while let Ok(nalu) = Nalu::next(&mut cursor) { + let nalu_offset = cursor.position() as usize; + let start = nalu_offset - nalu.offset; + let is_slice = matches!(nalu.header.type_, NaluType::Slice | NaluType::SliceIdr); + let first_mb_zero = + is_slice && stream.get(nalu_offset + 1).is_some_and(|b| b & 0x80 != 0); + + if au_has_slice && (!is_slice || first_mb_zero) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus + } + + #[test] + fn the_full_25fps_vector_plans_every_picture_and_every_pic_id_reaches_output() { + let aus = split_into_aus(TEST_25FPS); + let mut planner = H264Planner::new(); + let mut plans = Vec::new(); + for au in &aus { + plans.push( + planner + .plan_au(au) + .expect("the clean vector must plan without errors"), + ); + } + + assert_eq!(plans.len(), 250); + assert_eq!(plans.iter().map(|p| p.slices.len()).sum::(), 500); + assert!(plans[0].picture.is_idr); + assert!(plans.iter().all(|p| p.warnings.is_empty())); + assert_eq!( + (plans[0].picture.coded_width, plans[0].picture.coded_height), + (320, 240), + "coded size comes from the vector's SPS" + ); + + for plan in &plans { + if plan.picture.is_idr { + assert_eq!(plan.picture.pic_order_cnt, 0, "POC must reset at an IDR"); + } + for slice in &plan.slices { + if slice.header.slice_type.is_p() || slice.header.slice_type.is_b() { + assert!(!slice.ref_list0.is_empty()); + } + } + } + + let stored: BTreeSet = plans.iter().filter_map(|p| p.dpb.stored).collect(); + assert_eq!(stored.len(), 250); + let mut emitted: Vec = plans + .iter() + .flat_map(|p| p.dpb.outputs.iter().copied()) + .collect(); + emitted.extend(planner.flush().outputs); + let output: BTreeSet = emitted.iter().copied().collect(); + assert_eq!( + output, stored, + "bumping plus the final flush must output every picture" + ); + + // Output ORDER, not just coverage: within each IDR period, ids must emerge in + // ascending POC — the invariant the C.4.5.3 bumping process exists to provide. + // (POC was recorded at plan time; an IDR resets it, hence the period key.) + let mut period = 0usize; + let mut order_key: BTreeMap = BTreeMap::new(); + for plan in &plans { + if plan.picture.is_idr { + period += 1; + } + order_key.insert( + plan.dpb.stored.unwrap(), + (period, plan.picture.pic_order_cnt), + ); + } + let mut last: Option<(usize, i32)> = None; + for id in &emitted { + let key = order_key[id]; + if let Some(last) = last { + assert!( + key > last, + "outputs must emerge in ascending POC order per IDR period: \ + {key:?} emitted after {last:?}" + ); + } + last = Some(key); + } + } + + #[test] + fn b_slices_get_a_poc_ordered_list1_distinct_from_list0() { + let aus = split_into_aus(TEST_64X64_I_P_B_P_HIGH); + let mut planner = H264Planner::new(); + let mut b_slices_seen = 0usize; + + for au in &aus { + let plan = planner + .plan_au(au) + .expect("the clean vector must plan without errors"); + for slice in &plan.slices { + if !slice.header.slice_type.is_b() { + continue; + } + b_slices_seen += 1; + assert!(!slice.ref_list0.is_empty()); + assert!(!slice.ref_list1.is_empty()); + + let ids0: Vec = slice.ref_list0.iter().map(|r| r.id).collect(); + let ids1: Vec = slice.ref_list1.iter().map(|r| r.id).collect(); + assert_ne!(ids0, ids1, "list1 must not be list0's ordering"); + + // 8.2.4.2.3: list0 leads with the past, list1 with the future + // (a frame's PicOrderCnt is the min of its field order counts). + let poc = |r: &RefPic| r.top_field_order_cnt.min(r.bottom_field_order_cnt); + assert!(poc(&slice.ref_list0[0]) < plan.picture.pic_order_cnt); + assert!(poc(&slice.ref_list1[0]) > plan.picture.pic_order_cnt); + } + } + + assert!(b_slices_seen > 0, "the vector must contain B slices"); + } + + /// Byte-level authoring for the MMCO/LTR test: parameter sets via the vendored + /// builders + synthesizer, slice headers written by hand with the vendored + /// `NaluWriter` (upstream has no slice-header synthesizer — its encoder packs + /// headers in hardware). The planner only reads headers, so no slice data follows + /// the rbsp stop bit. + fn base_sps() -> SpsBuilder { + SpsBuilder::new() + .seq_parameter_set_id(0) + .profile_idc(Profile::Main) + .level_idc(Level::L4) + .frame_mbs_only_flag(true) + .direct_8x8_inference_flag(true) + .max_num_ref_frames(4) + .log2_max_frame_num_minus4(0) + .pic_order_cnt_type(0) + .log2_max_pic_order_cnt_lsb_minus4(0) + } + + fn authored_sps_pps() -> (Rc, Rc) { + let sps = base_sps().resolution(64, 64).build(); + let pps = PpsBuilder::new(Rc::clone(&sps)) + .pic_parameter_set_id(0) + .pic_init_qp(26) + .build(); + (sps, pps) + } + + fn param_set_au(sps: &Sps, pps: &Pps) -> Vec { + let mut au = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, sps, &mut au, true).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(3, pps, &mut au, true).unwrap(); + au + } + + fn write_idr_slice() -> Vec { + write_idr_slice_at(0, 0) + } + + fn write_idr_slice_at(first_mb: u32, pps_id: u32) -> Vec { + let mut buf = Vec::new(); + { + let mut w = NaluWriter::new(&mut buf, true); + w.write_header(3, NaluType::SliceIdr as u8).unwrap(); + w.write_ue(first_mb).unwrap(); // first_mb_in_slice + w.write_ue(2u32).unwrap(); // slice_type: I + w.write_ue(pps_id).unwrap(); // pic_parameter_set_id + w.write_f(4, 0u32).unwrap(); // frame_num, u(4): log2_max_frame_num_minus4 = 0 + w.write_ue(0u32).unwrap(); // idr_pic_id + w.write_f(4, 0u32).unwrap(); // pic_order_cnt_lsb, u(4) + w.write_f(1, 0u32).unwrap(); // no_output_of_prior_pics_flag + w.write_f(1, 0u32).unwrap(); // long_term_reference_flag + w.write_se(0i32).unwrap(); // slice_qp_delta + w.write_f(1, 1u32).unwrap(); // rbsp stop bit + while !w.aligned() { + w.write_f(1, 0u32).unwrap(); + } + } + buf + } + + /// One P slice NALU. `mmco_ops` = `None` for sliding-window marking, `Some(ops)` for + /// adaptive marking with `(operation, single-argument)` pairs (ops 2/4/6 all take + /// exactly one) — the writer appends the terminating op 0. + fn write_p_slice( + frame_num: u32, + poc_lsb: u32, + ref_idc: u8, + num_ref_idx_l0_active: u32, + mmco_ops: Option<&[(u32, u32)]>, + ) -> Vec { + write_p_slice_at( + 0, + 0, + frame_num, + poc_lsb, + ref_idc, + num_ref_idx_l0_active, + mmco_ops, + ) + } + + fn write_p_slice_at( + first_mb: u32, + pps_id: u32, + frame_num: u32, + poc_lsb: u32, + ref_idc: u8, + num_ref_idx_l0_active: u32, + mmco_ops: Option<&[(u32, u32)]>, + ) -> Vec { + let mut buf = Vec::new(); + { + let mut w = NaluWriter::new(&mut buf, true); + w.write_header(ref_idc, NaluType::Slice as u8).unwrap(); + w.write_ue(first_mb).unwrap(); // first_mb_in_slice + w.write_ue(0u32).unwrap(); // slice_type: P + w.write_ue(pps_id).unwrap(); // pic_parameter_set_id + w.write_f(4, frame_num).unwrap(); // frame_num, u(4) + w.write_f(4, poc_lsb).unwrap(); // pic_order_cnt_lsb, u(4) + w.write_f(1, 1u32).unwrap(); // num_ref_idx_active_override_flag + w.write_ue(num_ref_idx_l0_active - 1).unwrap(); + w.write_f(1, 0u32).unwrap(); // ref_pic_list_modification_flag_l0 + if ref_idc != 0 { + match mmco_ops { + None => w.write_f(1, 0u32).map(|_| ()).unwrap(), + Some(ops) => { + w.write_f(1, 1u32).unwrap(); // adaptive_ref_pic_marking_mode_flag + for (op, arg) in ops { + w.write_ue(*op).unwrap(); + w.write_ue(*arg).unwrap(); + } + w.write_ue(0u32).unwrap(); // memory_management_control_operation end + } + } + } + w.write_se(0i32).unwrap(); // slice_qp_delta + w.write_f(1, 1u32).unwrap(); // rbsp stop bit + while !w.aligned() { + w.write_f(1, 0u32).unwrap(); + } + } + buf + } + + #[test] + fn mmco_marks_a_picture_long_term_later_lists_carry_it_and_mmco2_evicts_it() { + let (sps, pps) = authored_sps_pps(); + let mut au0 = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au0, true).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(3, &pps, &mut au0, true).unwrap(); + au0.extend(write_idr_slice()); + + // AU1 marks itself long-term: MMCO 4 admits long-term index 0, MMCO 6 assigns + // it to the current picture. + let au1 = write_p_slice(1, 2, 1, 1, Some(&[(4, 1), (6, 0)])); + let au2 = write_p_slice(2, 4, 1, 2, None); + // AU3 evicts it again: MMCO 2 unmarks long_term_pic_num 0. Its own list is + // 3 deep so the long-term picture's presence (or wrongful absence) is visible. + let au3 = write_p_slice(3, 6, 1, 3, Some(&[(2, 0)])); + let au4 = write_p_slice(4, 8, 0, 3, None); + + let mut planner = H264Planner::new(); + let p0 = planner.plan_au(&au0).unwrap(); + let p1 = planner.plan_au(&au1).unwrap(); + let p2 = planner.plan_au(&au2).unwrap(); + let p3 = planner.plan_au(&au3).unwrap(); + let p4 = planner.plan_au(&au4).unwrap(); + for plan in [&p0, &p1, &p2, &p3, &p4] { + assert!( + plan.warnings.is_empty(), + "authored stream must plan clean: {plan:?}" + ); + } + assert!(p0.picture.is_idr); + assert_eq!(p2.picture.pic_order_cnt, 4); + + let idr_id = p0.dpb.stored.unwrap(); + let lt_id = p1.dpb.stored.unwrap(); + + // After AU1's marking, AU2's list must be [short-term IDR, long-term AU1] — + // 8.2.4.2.1 puts long-term references after the short-term ones. + let list0 = &p2.slices[0].ref_list0; + assert_eq!(list0.len(), 2); + assert!(!list0[0].is_long_term); + assert_eq!(list0[0].id, idr_id); + assert!(list0[1].is_long_term); + assert_eq!(list0[1].id, lt_id); + assert_eq!( + list0[1].frame_num_or_lt_idx, 0, + "LongTermFrameIdx, not frame_num" + ); + + // AU3 carries the MMCO 2, but marking is an end-of-picture process (8.2.5): + // its OWN list is built before the op applies and must still hold the + // long-term picture, after the short-terms in descending-PicNum order. An + // applied-marking-before-list-build ordering bug surfaces exactly here. + let p2_id = p2.dpb.stored.unwrap(); + let p3_id = p3.dpb.stored.unwrap(); + assert_eq!( + p3.slices[0] + .ref_list0 + .iter() + .map(|r| (r.id, r.is_long_term)) + .collect::>(), + vec![(p2_id, false), (idr_id, false), (lt_id, true)], + "AU3 still sees the long-term ref; its own MMCO 2 applies only at finish" + ); + + // AU4 must no longer see it: exactly the three short-terms, in descending + // PicNum order (8.2.4.2.1). + assert_eq!( + p4.slices[0] + .ref_list0 + .iter() + .map(|r| (r.id, r.is_long_term)) + .collect::>(), + vec![(p3_id, false), (p2_id, false), (idr_id, false)], + "the unmarked picture must have left, short-terms sorted by PicNum" + ); + + // Unmarked and displayed, the picture leaves the DPB for good. + let flush = planner.flush(); + assert!(flush.outputs.contains(<_id)); + assert!(flush.removed.contains(<_id)); + } + + #[test] + fn the_dpb_snapshot_holds_a_marked_long_term_reference_no_slice_of_the_au_names() { + // The shape a DXVA `RefFrameList` built from the derived lists loses: an + // anchor pinned long-term, still marked in the DPB, that this picture's own + // reference list is too short to reach. A driver told it is gone is entitled + // to discard it — and RFI recovery then decodes against nothing. + let (sps, pps) = authored_sps_pps(); + let mut au0 = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au0, true).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(3, &pps, &mut au0, true).unwrap(); + au0.extend(write_idr_slice()); + // AU1 pins ITSELF long-term (MMCO 4 admits index 0, MMCO 6 assigns it). + let au1 = write_p_slice(1, 2, 1, 1, Some(&[(4, 1), (6, 0)])); + // AU2 activates ONE reference: 8.2.4.2.1 puts the short-term IDR first, so + // the truncated list never names the long-term picture. + let au2 = write_p_slice(2, 4, 1, 1, None); + + let mut planner = H264Planner::new(); + let p0 = planner.plan_au(&au0).unwrap(); + let p1 = planner.plan_au(&au1).unwrap(); + let p2 = planner.plan_au(&au2).unwrap(); + for plan in [&p0, &p1, &p2] { + assert!(plan.warnings.is_empty(), "must plan clean: {plan:?}"); + } + let idr_id = p0.dpb.stored.unwrap(); + let lt_id = p1.dpb.stored.unwrap(); + + assert_eq!( + p2.slices[0] + .ref_list0 + .iter() + .map(|r| r.id) + .collect::>(), + vec![idr_id], + "the AU's own list reaches only the short-term picture" + ); + // …and the snapshot still reports both, the long-term one marked as such and + // keyed by its LongTermFrameIdx rather than its frame_num. + assert_eq!( + p2.dpb_refs + .iter() + .map(|r| (r.id, r.is_long_term, r.frame_num_or_lt_idx)) + .collect::>(), + vec![(idr_id, false, 0), (lt_id, true, 0)] + ); + + // The opening IDR has an empty DPB behind it; AU1 sees only the IDR, still + // short-term (its own marking is an end-of-picture process). + assert!(p0.dpb_refs.is_empty()); + assert_eq!( + p1.dpb_refs + .iter() + .map(|r| (r.id, r.is_long_term)) + .collect::>(), + vec![(idr_id, false)] + ); + } + + #[test] + fn the_dpb_snapshot_matches_the_planners_own_live_ids_across_the_whole_vector() { + // Every picture the snapshot names must be one the planner considers live — + // a snapshot entry with no live id is a surface a backend cannot resolve. + // (The converse does not hold: the DPB holds unmarked pictures for output.) + let mut planner = H264Planner::new(); + let mut plans = Vec::new(); + for au in split_into_aus(TEST_25FPS) { + let plan = planner.plan_au(au).expect("plan"); + plans.push(plan); + } + let mut live: BTreeSet = BTreeSet::new(); + for plan in &plans { + for r in &plan.dpb_refs { + assert!( + live.contains(&r.id), + "snapshot names {} which no earlier plan stored", + r.id + ); + } + // Uniqueness: one picture, one surface, one marking. + let mut ids: Vec = plan.dpb_refs.iter().map(|r| r.id).collect(); + let count = ids.len(); + ids.sort_unstable(); + ids.dedup(); + assert_eq!(ids.len(), count, "the snapshot must not repeat a picture"); + // The current picture is stored AFTER the snapshot is taken. + assert!(!ids.contains(&plan.dpb.stored.unwrap())); + live.insert(plan.dpb.stored.unwrap()); + for id in &plan.dpb.removed { + live.remove(id); + } + } + // The IPPP… vector keeps every reference until the DPB is full, so the + // snapshot must be non-empty for the great majority of its AUs. + let non_empty = plans.iter().filter(|p| !p.dpb_refs.is_empty()).count(); + assert!(non_empty >= 240, "only {non_empty} AUs carried a snapshot"); + } + + #[test] + fn a_dropped_reference_au_degrades_to_gap_warnings_and_planning_continues() { + let aus = split_into_aus(TEST_25FPS); + + // Pass 1: find a droppable AU — a non-IDR reference picture not followed by an + // IDR (an IDR right after would reset the state and hide the gap). + let mut planner = H264Planner::new(); + let mut plans = Vec::new(); + for au in &aus { + plans.push(planner.plan_au(au).unwrap()); + } + let dropped = plans + .iter() + .enumerate() + .position(|(i, p)| { + p.picture.is_reference + && !p.picture.is_idr + && plans.get(i + 1).is_some_and(|next| !next.picture.is_idr) + }) + .expect("the vector contains a droppable reference picture"); + + // Pass 2: the same stream minus that AU must warn, not error — and every ref + // list entry it emits must still resolve to a picture the backend was told to + // store (substitution never leaks a placeholder). + let mut planner = H264Planner::new(); + let mut gap_seen = false; + let mut missing_seen = false; + let mut planned = 0usize; + let mut stored_so_far: BTreeSet = BTreeSet::new(); + for (i, au) in aus.iter().enumerate() { + if i == dropped { + continue; + } + let plan = planner + .plan_au(au) + .expect("a lost reference AU must degrade to warnings, not errors"); + planned += 1; + gap_seen |= plan + .warnings + .iter() + .any(|w| matches!(w, PlanWarning::FrameNumGap { .. })); + missing_seen |= plan + .warnings + .iter() + .any(|w| matches!(w, PlanWarning::MissingReference { .. })); + stored_so_far.insert(plan.dpb.stored.unwrap()); + for slice in &plan.slices { + for entry in slice.ref_list0.iter().chain(&slice.ref_list1) { + assert!( + stored_so_far.contains(&entry.id), + "every emitted reference must be a real stored PicId" + ); + } + } + } + + assert_eq!(planned, 249); + assert!( + gap_seen, + "the AU after the drop must report the frame_num gap" + ); + // The 8.2.5.2 placeholder is un-resolvable for backends, so planning around + // it must also have flagged it. + assert!(missing_seen); + } + + #[test] + fn a_gap_placeholder_inside_a_ref_list_is_substituted_in_place_not_compacted() { + let (sps, pps) = authored_sps_pps(); + let mut au0 = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au0, true).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(3, &pps, &mut au0, true).unwrap(); + au0.extend(write_idr_slice()); + let au1 = write_p_slice(1, 2, 1, 1, None); + // The reference picture with frame_num 2 is never fed (lost on the wire); the + // next AU's 3-deep list then holds the 8.2.5.2 placeholder at its HEAD. + let au3 = write_p_slice(3, 6, 1, 3, None); + + let mut planner = H264Planner::new(); + let p0 = planner.plan_au(&au0).unwrap(); + let p1 = planner.plan_au(&au1).unwrap(); + let p3 = planner.plan_au(&au3).unwrap(); + + assert!(p3 + .warnings + .iter() + .any(|w| matches!(w, PlanWarning::FrameNumGap { .. }))); + assert!(p3 + .warnings + .iter() + .any(|w| matches!(w, PlanWarning::MissingReference { .. }))); + + // Initial list by descending PicNum: [placeholder(2), P1(1), IDR(0)]. The + // placeholder heads the list, so its substitute is the first existing entry + // (P1) — and crucially the two real entries keep their ref_idx positions. + let id0 = p0.dpb.stored.unwrap(); + let id1 = p1.dpb.stored.unwrap(); + let list0 = &p3.slices[0].ref_list0; + assert_eq!( + list0.iter().map(|r| r.id).collect::>(), + vec![id1, id1, id0], + "substitution must preserve list length and positions" + ); + assert!(list0.iter().all(|r| !r.is_long_term)); + } + + #[test] + fn a_recovery_point_sei_in_the_au_lands_on_the_picture_plan() { + let (sps, pps) = authored_sps_pps(); + let mut au0 = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au0, true).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(3, &pps, &mut au0, true).unwrap(); + // SEI NALU: recovery point, recovery_frame_cnt = 5, exact_match = 1. + au0.extend([0x00, 0x00, 0x00, 0x01, 0x06, 0x06, 0x02, 0x34, 0x40, 0x80]); + au0.extend(write_idr_slice()); + + let mut planner = H264Planner::new(); + let plan = planner.plan_au(&au0).unwrap(); + assert_eq!( + plan.picture.recovery_point, + Some(RecoveryPoint { + recovery_frame_cnt: 5, + exact_match: true, + broken_link: false + }) + ); + + // The following AU carries no SEI: the field must not stick. + let au1 = write_p_slice(1, 2, 1, 1, None); + let plan = planner.plan_au(&au1).unwrap(); + assert_eq!(plan.picture.recovery_point, None); + } + + #[test] + fn an_interlaced_sps_is_rejected_as_outside_the_envelope() { + let sps = SpsBuilder::new() + .seq_parameter_set_id(0) + .profile_idc(Profile::Main) + .level_idc(Level::L4) + .resolution(64, 64) + .frame_mbs_only_flag(false) + .mb_adaptive_frame_field_flag(false) + .direct_8x8_inference_flag(true) + .max_num_ref_frames(4) + .log2_max_frame_num_minus4(0) + .pic_order_cnt_type(0) + .log2_max_pic_order_cnt_lsb_minus4(0) + .build(); + let mut au = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au, true).unwrap(); + + let mut planner = H264Planner::new(); + assert!(matches!( + planner.plan_au(&au), + Err(PlanError::OutsideEnvelope(_)) + )); + } + + #[test] + fn a_dpb_deeper_than_16_frames_is_rejected_as_outside_the_envelope() { + // The one route past the A.3.1 16-frame cap: the VUI bitstream restriction's + // max_dec_frame_buffering, an unbounded ue(v) that overrides the level-derived + // size in `Sps::max_dpb_frames`. The builder has no VUI-restriction setter, so + // the Sps is constructed directly (its fields are public). + let sps = Sps { + profile_idc: Profile::Main as u8, + level_idc: Level::L4, + frame_mbs_only_flag: true, + direct_8x8_inference_flag: true, + max_num_ref_frames: 4, + vui_parameters_present_flag: true, + vui_parameters: VuiParams { + bitstream_restriction_flag: true, + max_dec_frame_buffering: 17, + ..Default::default() + }, + ..Default::default() + }; + let mut au = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au, true).unwrap(); + + let err = H264Planner::new().plan_au(&au).unwrap_err(); + assert!( + matches!(err, PlanError::OutsideEnvelope(what) if what.contains("DPB")), + "{err:?}" + ); + } + + /// MMCO 5 writer: op 5 takes NO argument (Table 7-9), so the generic + /// [`write_p_slice`] — whose supported ops all take exactly one — cannot author + /// it. + fn write_p_slice_mmco5(frame_num: u32, poc_lsb: u32) -> Vec { + let mut buf = Vec::new(); + { + let mut w = NaluWriter::new(&mut buf, true); + w.write_header(1, NaluType::Slice as u8).unwrap(); + w.write_ue(0u32).unwrap(); // first_mb_in_slice + w.write_ue(0u32).unwrap(); // slice_type: P + w.write_ue(0u32).unwrap(); // pic_parameter_set_id + w.write_f(4, frame_num).unwrap(); // frame_num, u(4) + w.write_f(4, poc_lsb).unwrap(); // pic_order_cnt_lsb, u(4) + w.write_f(1, 1u32).unwrap(); // num_ref_idx_active_override_flag + w.write_ue(0u32).unwrap(); // num_ref_idx_l0_active_minus1 + w.write_f(1, 0u32).unwrap(); // ref_pic_list_modification_flag_l0 + w.write_f(1, 1u32).unwrap(); // adaptive_ref_pic_marking_mode_flag + w.write_ue(5u32).unwrap(); // memory_management_control_operation 5 + w.write_ue(0u32).unwrap(); // memory_management_control_operation end + w.write_se(0i32).unwrap(); // slice_qp_delta + w.write_f(1, 1u32).unwrap(); // rbsp stop bit + while !w.aligned() { + w.write_f(1, 0u32).unwrap(); + } + } + buf + } + + #[test] + fn an_mmco_5_is_planned_with_a_rebase_warning_not_rejected() { + let (sps, pps) = authored_sps_pps(); + let mut au0 = param_set_au(&sps, &pps); + au0.extend(write_idr_slice()); + let au1 = write_p_slice_mmco5(1, 2); + + let mut planner = H264Planner::new(); + let p0 = planner.plan_au(&au0).unwrap(); + let p1 = planner.plan_au(&au1).unwrap(); + + assert!(p1.warnings.contains(&PlanWarning::Mmco5Rebase)); + // The plan carries the pre-rebase 8.2.1 values a decoder submits with; the + // zeroed frame_num/POC exist only in the STORED picture later AUs reference. + assert_eq!(p1.picture.frame_num, 1); + assert_eq!(p1.picture.pic_order_cnt, 2); + // And the op's C.4.5.3 clause-3 drain ran: the IDR is display-ready. + assert!(p1.dpb.outputs.contains(&p0.dpb.stored.unwrap())); + } + + #[test] + fn a_separate_colour_plane_sps_is_rejected_as_outside_the_envelope() { + // SpsBuilder has no separate_colour_plane setter; construct the Sps directly + // (its fields are public) — the synthesizer writes the flag for High profile + // with chroma_format_idc 3. + let sps = Sps { + profile_idc: Profile::High as u8, + chroma_format_idc: 3, + separate_colour_plane_flag: true, + frame_mbs_only_flag: true, + ..Default::default() + }; + let mut au = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au, true).unwrap(); + + let err = H264Planner::new().plan_au(&au).unwrap_err(); + assert!( + matches!(err, PlanError::OutsideEnvelope(what) if what.contains("separate")), + "{err:?}" + ); + } + + #[test] + fn display_crop_reports_the_conformance_window_offset_and_size() { + // chroma_format_idc 1 (inferred for Main) puts CropUnitX/Y at 2 with + // frame_mbs_only: offsets top 2 / bottom 2 / left 4 / right 2 are 4/4/8/4 in + // luma samples. + let sps = base_sps() + .resolution(64, 64) + .frame_crop_offsets(2, 2, 4, 2) + .build(); + let pps = PpsBuilder::new(Rc::clone(&sps)) + .pic_parameter_set_id(0) + .pic_init_qp(26) + .build(); + let mut au = param_set_au(&sps, &pps); + au.extend(write_idr_slice()); + let plan = H264Planner::new().plan_au(&au).unwrap(); + assert_eq!( + plan.picture.display_crop, + DisplayCrop { + x: 8, + y: 4, + width: 52, + height: 56 + } + ); + + // The review's underflow shape: crop_left 100 (200 luma samples) on a + // 320-wide picture passes SPS validation; a max-minus-min derivation + // underflows on it. + let sps = base_sps() + .resolution(320, 240) + .frame_crop_offsets(0, 0, 100, 0) + .build(); + let pps = PpsBuilder::new(Rc::clone(&sps)) + .pic_parameter_set_id(0) + .pic_init_qp(26) + .build(); + let mut au = param_set_au(&sps, &pps); + au.extend(write_idr_slice()); + let plan = H264Planner::new().plan_au(&au).unwrap(); + assert_eq!( + plan.picture.display_crop, + DisplayCrop { + x: 200, + y: 0, + width: 120, + height: 240 + } + ); + } + + /// A 64x64 SPS with the VUI colour fields set as given. SpsBuilder has no + /// colour setters, so the built Sps is unwrapped and mutated directly (the + /// separate_colour_plane test's idiom); the synthesizer writes the whole + /// `video_signal_type` block from the struct. + fn sps_with_vui_colour( + signal_type: bool, + full_range: bool, + description: Option<(u8, u8, u8)>, + ) -> Rc { + let mut sps = Rc::try_unwrap(base_sps().resolution(64, 64).build()).expect("freshly built"); + sps.vui_parameters_present_flag = true; + sps.vui_parameters.video_signal_type_present_flag = signal_type; + sps.vui_parameters.video_full_range_flag = full_range; + if let Some((primaries, transfer, matrix)) = description { + sps.vui_parameters.colour_description_present_flag = true; + sps.vui_parameters.colour_primaries = primaries; + sps.vui_parameters.transfer_characteristics = transfer; + sps.vui_parameters.matrix_coefficients = matrix; + } + Rc::new(sps) + } + + fn plan_one_idr(sps: &Rc) -> AuPlan { + let pps = PpsBuilder::new(Rc::clone(sps)) + .pic_parameter_set_id(0) + .pic_init_qp(26) + .build(); + let mut au = param_set_au(sps, &pps); + au.extend(write_idr_slice()); + H264Planner::new().plan_au(&au).unwrap() + } + + #[test] + fn an_sps_without_vui_plans_the_e211_unspecified_colour() { + let (sps, pps) = authored_sps_pps(); + assert!( + !sps.vui_parameters_present_flag, + "the base SPS carries no VUI" + ); + let mut au = param_set_au(&sps, &pps); + au.extend(write_idr_slice()); + let plan = H264Planner::new().plan_au(&au).unwrap(); + assert_eq!( + plan.picture.colour, + ColourDescription { + colour_primaries: 2, + transfer_characteristics: 2, + matrix_coefficients: 2, + video_full_range: false, + }, + "E.2.1 inference: 'unspecified' code points + limited range, never a raw 0" + ); + } + + #[test] + fn an_explicit_colour_description_rides_the_plan_and_follows_a_new_sps() { + // BT.2020/PQ HDR signalling — the in-band switch the Windows host emits. + let hdr = sps_with_vui_colour(true, false, Some((9, 16, 9))); + let plan = plan_one_idr(&hdr); + assert_eq!( + plan.picture.colour, + ColourDescription { + colour_primaries: 9, + transfer_characteristics: 16, + matrix_coefficients: 9, + video_full_range: false, + } + ); + + // The colour must track the SPS active for EACH picture, not the + // session's first: an SDR stream renegotiated to HDR mid-stream (same + // SPS id, new content, SPS+PPS in-band at the IDR — the parser's Pps + // snapshots its SPS at PPS-parse time, and hosts re-send both exactly + // so the new content activates) flips at the very next planned picture. + let (sdr_sps, sdr_pps) = authored_sps_pps(); + let mut au0 = param_set_au(&sdr_sps, &sdr_pps); + au0.extend(write_idr_slice()); + let mut planner = H264Planner::new(); + let plan0 = planner.plan_au(&au0).unwrap(); + assert_eq!(plan0.picture.colour.matrix_coefficients, 2); + + let hdr_pps = PpsBuilder::new(Rc::clone(&hdr)) + .pic_parameter_set_id(0) + .pic_init_qp(26) + .build(); + let mut au1 = param_set_au(&hdr, &hdr_pps); + au1.extend(write_idr_slice()); + let plan1 = planner.plan_au(&au1).unwrap(); + assert_eq!( + plan1.picture.colour.matrix_coefficients, 9, + "the replacing SPS's colour lands on its own picture, not latched" + ); + } + + #[test] + fn a_vui_without_colour_description_keeps_unspecified_but_honours_the_range_flag() { + // video_signal_type present, full-range set, but NO colour description: + // the code points stay E.2.1's "unspecified" while the range flag rides. + let plan = plan_one_idr(&sps_with_vui_colour(true, true, None)); + assert_eq!( + plan.picture.colour, + ColourDescription { + colour_primaries: 2, + transfer_characteristics: 2, + matrix_coefficients: 2, + video_full_range: true, + } + ); + } + + #[test] + fn a_malformed_nalu_mid_au_truncates_with_a_warning_keeping_prior_slices() { + let (sps, pps) = authored_sps_pps(); + let mut au = param_set_au(&sps, &pps); + au.extend(write_idr_slice()); + // Reserved NAL type 24: the vendored header parser rejects it. + au.extend([0x00, 0x00, 0x00, 0x01, 0x18, 0xAA, 0xBB]); + au.extend(write_idr_slice()); // real data behind the cut, never reached + + let plan = H264Planner::new().plan_au(&au).unwrap(); + assert_eq!( + plan.slices.len(), + 1, + "only the slice before the cut is planned" + ); + assert!(plan + .warnings + .iter() + .any(|w| matches!(w, PlanWarning::TruncatedAu { .. }))); + } + + #[test] + fn a_foreign_slice_in_the_au_is_dropped_with_a_truncated_au_warning() { + let (sps, pps) = authored_sps_pps(); + let mut au = param_set_au(&sps, &pps); + au.extend(write_idr_slice()); + // A mis-split AU: continuation slices belonging to ANOTHER picture (non-IDR, + // frame_num 1). They and everything after them must be ignored. + au.extend(write_p_slice_at(8, 0, 1, 2, 1, 1, None)); + au.extend(write_p_slice_at(9, 0, 1, 2, 1, 1, None)); + + let plan = H264Planner::new().plan_au(&au).unwrap(); + assert!(plan.picture.is_idr); + assert_eq!(plan.slices.len(), 1, "the foreign slices are not planned"); + assert!(plan + .warnings + .iter() + .any(|w| matches!(w, PlanWarning::TruncatedAu { .. }))); + } + + #[test] + fn outputs_queued_during_a_failed_au_surface_in_the_next_successful_plan() { + let (sps, pps) = authored_sps_pps(); + let mut au0 = param_set_au(&sps, &pps); + au0.extend(write_idr_slice()); + + let mut planner = H264Planner::new(); + let id0 = planner.plan_au(&au0).unwrap().dpb.stored.unwrap(); + + // This AU errors AFTER its IDR begin drained the DPB (queueing id0 for + // output): the continuation slice references PPS 1, which was never sent. + let mut bad_au = write_idr_slice(); + bad_au.extend(write_p_slice_at(8, 1, 0, 0, 1, 1, None)); + assert!(matches!( + planner.plan_au(&bad_au), + Err(PlanError::NoActiveParamSet { pps_id: 1 }) + )); + + // The queued output and the eviction must surface here, not vanish. + let plan = planner.plan_au(&write_idr_slice()).unwrap(); + assert!(plan.dpb.outputs.contains(&id0)); + assert!(plan.dpb.removed.contains(&id0)); + } + + #[test] + fn flush_resets_decoding_state_and_refuses_non_idr_until_an_idr_arrives() { + let (sps, pps) = authored_sps_pps(); + let mut au0 = param_set_au(&sps, &pps); + au0.extend(write_idr_slice()); + + let mut planner = H264Planner::new(); + let id0 = planner.plan_au(&au0).unwrap().dpb.stored.unwrap(); + let id1 = planner + .plan_au(&write_p_slice(1, 2, 1, 1, None)) + .unwrap() + .dpb + .stored + .unwrap(); + + let flushed = planner.flush(); + assert!(flushed.outputs.contains(&id0) && flushed.outputs.contains(&id1)); + assert_eq!(flushed.removed, vec![id0, id1]); + + // A non-IDR AU is refused until the next IDR. + assert!(matches!( + planner.plan_au(&write_p_slice(2, 4, 1, 1, None)), + Err(PlanError::AwaitingIdr) + )); + + // The IDR restarts planning; parameter sets survived the flush (7.4.1.2). + let plan = planner.plan_au(&write_idr_slice()).unwrap(); + assert!(plan.picture.is_idr); + assert_eq!(plan.picture.pic_order_cnt, 0); + assert!(plan.warnings.is_empty()); + + // And the stream continues cleanly on the reset state. + let plan = planner.plan_au(&write_p_slice(1, 2, 1, 1, None)).unwrap(); + assert!(plan.warnings.is_empty()); + assert_eq!(plan.slices[0].ref_list0.len(), 1); + } + + #[test] + fn picture_plan_parameters_come_from_the_first_slices_pps() { + // Two SPSes with identical negotiation parameters but different conformance + // windows; PPS 1 references the cropped one. + let sps0 = base_sps().resolution(64, 64).build(); + let sps1 = base_sps() + .seq_parameter_set_id(1) + .resolution(64, 64) + .frame_crop_offsets(2, 2, 4, 2) + .build(); + let pps0 = PpsBuilder::new(Rc::clone(&sps0)) + .pic_parameter_set_id(0) + .pic_init_qp(26) + .build(); + let pps1 = PpsBuilder::new(Rc::clone(&sps1)) + .pic_parameter_set_id(1) + .pic_init_qp(26) + .build(); + + let mut au = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps0, &mut au, true).unwrap(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps1, &mut au, true).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(3, &pps0, &mut au, true).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(3, &pps1, &mut au, true).unwrap(); + au.extend(write_idr_slice_at(0, 0)); + // Legal but perverse: a continuation slice may reference another PPS. + au.extend(write_idr_slice_at(8, 1)); + + let plan = H264Planner::new().plan_au(&au).unwrap(); + assert!(plan.warnings.is_empty()); + assert_eq!(plan.slices.len(), 2); + // The picture parameters come from the FIRST slice's PPS (the uncropped + // SPS 0); they must not drift to the last slice's. + assert_eq!( + plan.picture.display_crop, + DisplayCrop { + x: 0, + y: 0, + width: 64, + height: 64 + } + ); + // The accessor pair follows the same first-slice rule: backends build + // their parameter objects from these, so drifting to PPS 1 here would + // desynchronize them from `picture`. + assert_eq!(plan.pps.pic_parameter_set_id, 0); + assert_eq!(plan.sps.seq_parameter_set_id, 0); + assert!( + Rc::ptr_eq(&plan.sps, &plan.pps.sps), + "the SPS accessor is the PPS's own SPS, not a second copy" + ); + assert!( + !plan.sps.frame_cropping_flag, + "SPS 0, not the cropped SPS 1" + ); + } + + #[test] + fn the_plans_parameter_set_accessors_carry_the_activated_content() { + let (sps, pps) = authored_sps_pps(); + let mut au0 = param_set_au(&sps, &pps); + au0.extend(write_idr_slice()); + + let plan = H264Planner::new().plan_au(&au0).unwrap(); + // The parser re-parses the in-band parameter sets, so pointer identity + // with the authored `sps`/`pps` is not expected (and whole-struct + // equality would compare parser-side normalizations like the flat + // scaling-list fill); the contract is that the ACTIVATED content rides + // out. Spot-check the fields backends build parameter objects from. + assert_eq!(plan.sps.seq_parameter_set_id, sps.seq_parameter_set_id); + assert_eq!(plan.sps.profile_idc, sps.profile_idc); + assert_eq!(plan.sps.level_idc, sps.level_idc); + assert_eq!(plan.sps.max_num_ref_frames, sps.max_num_ref_frames); + assert_eq!(plan.sps.width(), sps.width()); + assert_eq!(plan.sps.height(), sps.height()); + assert_eq!(plan.pps.pic_parameter_set_id, pps.pic_parameter_set_id); + assert_eq!(plan.pps.seq_parameter_set_id, pps.seq_parameter_set_id); + assert_eq!(plan.pps.pic_init_qp_minus26, pps.pic_init_qp_minus26); + assert!(Rc::ptr_eq(&plan.sps, &plan.pps.sps)); + } +} diff --git a/crates/pf-bitstream/src/h265.rs b/crates/pf-bitstream/src/h265.rs new file mode 100644 index 00000000..289c73bf --- /dev/null +++ b/crates/pf-bitstream/src/h265.rs @@ -0,0 +1,3320 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file (vendor/cros-codecs/LICENSE). +// +// Adapted from cros-codecs `decoder/stateless/h265.rs` (see +// vendor/cros-codecs/PROVENANCE.md for the snapshot pin). The spec machinery — POC +// computation (8.3.1, in the vendored `PictureData`), reference picture set derivation +// and marking (8.3.2), reference list construction (8.3.3/8.3.4), DPB update and +// bumping (C.5.2) — is ported faithfully and keeps upstream's structure and +// spec-section comments so future upstream diffs stay legible. Stripped: the +// StatelessDecoder/backend trait plumbing, fd/event machinery, pooled-buffer handling, +// and the SCC current-picture-reference paths (the envelope gate below rejects SCC +// self-referencing outright, so a reference list can never contain the current +// picture). + +//! Per-AU H.265 planning: [`H265Planner::plan_au`] turns one access unit exactly as +//! the pump hands it to a decoder (Annex-B, parameter sets + the slice segments of one +//! picture) into an [`AuPlan`] — everything a stateless hardware decoder needs before +//! submission and nothing it has to re-derive: parsed headers, POC, the derived +//! reference picture sets (including the long-term entries host RFI recovery leans +//! on — HEVC's analogue of H.264 LTR/MMCO), per-slice reference lists and the DPB +//! delta. +//! +//! Concealment posture, same as the H.264 layer: an RPS entry that names a picture the +//! DPB does not hold is a [`PlanWarning`], never an error — the reference is +//! substituted in place, the session layer sees the warning and requests recovery +//! while planning continues. [`PlanError`] is reserved for AUs that cannot (or, for +//! RASL pictures behind a skipped CRA, must not) be planned at all. +//! +//! WP-2 contract note (the pf-vkdecode/client wiring): the existing H.264 session +//! layer maps every planner `Err` to release-the-frame-unshown + request-reanchor. +//! [`PlanError::RaslSkipped`] must NOT inherit that mapping in the future H.265 +//! backend — it is the spec's own skip (8.1.3 NOTE: decode nothing, show nothing, +//! the stream is healthy), so the backend treats it as an Ok-skip of the AU, never +//! as a recovery trigger. Dead in the field today (punktfunk hosts emit IDR-only +//! re-entry points), recorded here so WP-2 does not copy the H.264 error path +//! blindly. + +use std::cell::RefCell; +use std::collections::BTreeSet; +use std::io::Cursor; +use std::mem; +use std::ops::Range; +use std::rc::Rc; + +use cros_codecs::codec::h265::dpb::Dpb; +use cros_codecs::codec::h265::dpb::DpbEntry; +use cros_codecs::codec::h265::parser::Nalu; +use cros_codecs::codec::h265::parser::Parser; +use cros_codecs::codec::h265::parser::Pps; +use cros_codecs::codec::h265::parser::ShortTermRefPicSet; +use cros_codecs::codec::h265::parser::Slice; +use cros_codecs::codec::h265::parser::Sps; +use cros_codecs::codec::h265::picture::PictureData; +use cros_codecs::codec::h265::picture::Reference; +use cros_codecs::Resolution; +use tracing::trace; + +pub use cros_codecs::codec::h265::parser::Level; +pub use cros_codecs::codec::h265::parser::NaluType; +pub use cros_codecs::codec::h265::parser::SliceHeader; + +// Codec-neutral plan vocabulary, shared with (and canonically owned by) the H.264 +// layer: pf-vkdecode already imports these from `pf_bitstream::h264`, so they stay +// defined there and are re-exported here rather than lifted to a third module. +pub use crate::h264::ColourDescription; +pub use crate::h264::DisplayCrop; +pub use crate::h264::DpbUpdate; +pub use crate::h264::PicId; + +use crate::sei; +pub use crate::sei::RecoveryPointHevc; + +/// Everything a backend needs to submit one access unit. +#[derive(Debug, Clone)] +pub struct AuPlan { + pub picture: PicturePlan, + /// The picture's 8.3.2 reference picture sets, resolved to stored pictures — + /// the DPB snapshot DXVA picparams and Vulkan `StdVideoDecodeH265PictureInfo` + /// both key their reference arrays by. + pub rps: RpsPlan, + pub slices: Vec, + pub dpb: DpbUpdate, + /// Every picture the DPB holds marked "used for reference" at the moment this AU + /// decodes — the MARKED DPB, which is a SUPERSET of [`Self::rps`]'s three current + /// sets. + /// + /// The difference is 8.3.2's *Foll* sets: `RefPicSetStFoll` and `RefPicSetLtFoll` + /// are pictures this AU keeps marked for FUTURE pictures to reference while + /// naming none of them itself. They belong in `DXVA_PicParams_HEVC::RefPicList` + /// all the same — that array is spec-defined as the pictures currently marked + /// used for reference, and libavcodec's DXVA HEVC path fills it by walking its + /// whole DPB for `HEVC_FRAME_FLAG_{LONG,SHORT}_REF`, then points the + /// `RefPicSet*Curr` INDEX arrays into it. A driver keeping per-reference state is + /// entitled to read a picture's absence from `RefPicList` as "no longer a + /// reference" and discard it; a long-term anchor held across an RFI recovery + /// window is exactly the picture that disappears and comes back. + /// + /// Vulkan asks the opposite question — `pReferenceSlots` is spec-defined as the + /// slots THIS decode operation uses — so the native Vulkan rung binds + /// [`Self::rps`] and is right to. + /// + /// Captured at BEGIN-picture time: after 8.3.2 has derived and MARKED this + /// picture's reference picture sets and after C.5.2.2's pre-decode DPB update, + /// before the current picture itself is stored. It therefore never contains the + /// current picture, and — like libavcodec's walk — never contains a picture the + /// RPS just unmarked. + /// + /// Order is the DPB's own (oldest stored first), which is also libavcodec's. + /// Entries are unique: one picture, one surface, one marking. + pub dpb_refs: Vec, + pub warnings: Vec, + /// The SPS the planner activated for this AU — the one [`Self::picture`]'s + /// parameters derive from (the FIRST slice's PPS's SPS; a later slice segment may + /// legally reference another PPS, and that drift deliberately does not reach + /// here). Cloned out of the parser's table so backends build their parameter + /// objects from exactly what was activated, never by re-parsing the AU. + pub sps: Rc, + /// The PPS the picture was BEGUN with (the first slice's), same contract as + /// [`Self::sps`]. Its `sps` field is the same `Rc` as [`Self::sps`]; the VPS, if + /// the stream carried one, hangs off `sps.vps`. + pub pps: Rc, +} + +/// Per-picture parameters, captured after 8.3.1 POC derivation and 8.3.2 RPS marking +/// (the values a hardware picture-parameters struct wants). +#[derive(Debug, Clone)] +pub struct PicturePlan { + /// The picture's NALU type — HEVC encodes the picture taxonomy (IDR/BLA/CRA, + /// RADL/RASL, sub-layer non-reference) here rather than in header flags. + pub nalu_type: NaluType, + pub is_idr: bool, + pub is_irap: bool, + /// `NoRaslOutputFlag` (8.1.3): set on every IDR/BLA and on a CRA that opens the + /// bitstream or follows an EOS — the signal that RASL pictures leading this IRAP + /// are undecodable. + pub no_rasl_output_flag: bool, + /// Whether later pictures may reference this one. Every planned picture is stored + /// in the DPB (C.3.4 marks it "used for short-term reference" wholesale); this is + /// false only for sub-layer non-reference NALU types, which nothing at the same + /// temporal layer may reference. + pub is_reference: bool, + /// `PicOrderCntVal` per 8.3.1. + pub pic_order_cnt: i32, + pub coded_width: u32, + pub coded_height: u32, + /// Conformance-window crop (7.4.3.2.1: `conf_win_*` offsets scale by + /// SubWidthC/SubHeightC), in luma samples of the coded picture. + pub display_crop: DisplayCrop, + /// Colour signalling from the ACTIVE SPS's VUI (E.3.1 inference where absent — + /// the vendored parser defaults the colour code points to 2/"unspecified" with + /// limited range, so reading unconditionally IS the inference). Per picture, + /// never latched at session start: the Windows host switches an HDR desktop to + /// PQ/BT.2020 IN-BAND with a new SPS mid-stream. + pub colour: ColourDescription, + pub general_profile_idc: u8, + pub level_idc: Level, + pub bit_depth_luma_minus8: u8, + pub bit_depth_chroma_minus8: u8, + pub chroma_format_idc: u8, + /// DPB size in frames per A.4 (equation A-2, capped at 16) — backends size their + /// slot pool from this. + pub max_dpb_frames: usize, + /// Bits of the `st_ref_pic_set()` the FIRST slice carried inline (0 when the RPS + /// came from the SPS by index) — Vulkan's `NumBitsForSTRefPicSetInSlice`. + pub short_term_ref_pic_set_size_bits: u32, + pub recovery_point: Option, +} + +/// A reference list / RPS entry: the minimum every backend picparams format needs. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RefPic { + pub id: PicId, + /// The stored picture's `PicOrderCntVal` — the key HEVC hardware formats identify + /// references by (there is no `frame_num` in this codec). + pub pic_order_cnt: i32, + pub is_long_term: bool, +} + +/// The three "current" reference picture sets of 8.3.2, resolved to stored pictures. +/// +/// Entries the DPB could not resolve are ABSENT here (each one was flagged via +/// [`PlanWarning::MissingReference`] when the RPS was derived); the per-slice +/// reference lists — where positional stability matters because `ref_idx` indexes +/// them — conceal by substitution instead. +#[derive(Debug, Clone, Default)] +pub struct RpsPlan { + /// `RefPicSetStCurrBefore`: short-term references with POC below the current + /// picture's, nearest first. + pub st_curr_before: Vec, + /// `RefPicSetStCurrAfter`: short-term references with POC above the current + /// picture's, nearest first. + pub st_curr_after: Vec, + /// `RefPicSetLtCurr`: the long-term references — the entries punktfunk hosts' + /// RFI recovery rides. + pub lt_curr: Vec, +} + +/// One slice segment NALU of the picture, with its reference lists fully derived. +#[derive(Debug, Clone)] +pub struct SlicePlan { + /// Byte range of the slice NALU in the input AU, start code included — hardware + /// decoders take the raw bitstream, so the plan points instead of copying. + pub data: Range, + /// The parsed slice segment header. For a dependent slice segment this is the + /// COMPLETED header — the inherited fields already copied from the picture's last + /// independent slice segment (7.4.7.1), so backends never see a partial header. + pub header: SliceHeader, + pub ref_list0: Vec, + pub ref_list1: Vec, +} + +/// Concealment signals: planning continues, the session layer requests recovery. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanWarning { + /// An RPS entry (or a reference list built from one) named a picture the DPB does + /// not hold — at least one reference AU was lost upstream. The corresponding + /// reference-list positions carry an in-place substitute. + MissingReference { + context: &'static str, + detail: String, + }, + /// The AU's NALU walk stopped early — a truncated NALU with real data behind it, + /// or a slice belonging to another picture (mis-split AU). The plan covers only + /// the slices before the cut; `offset` is the byte position of the cut in the AU. + TruncatedAu { offset: usize }, + /// The activated SPS signals output reordering (`sps_max_num_reorder_pics > 0`). + /// Spec-legal and fully planned — the C.5.2 bumping honours it — but punktfunk + /// hosts emit zero-reorder low-delay streams only, so this warning is the field + /// signal if that assumption ever breaks (the H.264 layer's `Mmco5Rebase` idiom). + NonZeroReorder { max_num_reorder_pics: u8 }, +} + +/// The AU cannot be planned at all. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanError { + Parse(String), + /// Legal H.265, but outside what punktfunk hosts emit (clients only decode + /// punktfunk hosts, so this is a stream-integrity failure, not a feature gap). + OutsideEnvelope(&'static str), + NoActiveParamSet { + pps_id: u8, + }, + /// [`H265Planner::flush`] discarded the decoding state; planning resumes only at + /// an IRAP (the port of upstream's `Reset` gating — HEVC's CRA/BLA are full + /// re-entry points here because a flush marks the next picture "first after EOS", + /// which gives any IRAP `NoRaslOutputFlag = 1`). + AwaitingIdr, + /// A RASL picture whose associated CRA/BLA had `NoRaslOutputFlag = 1` (an + /// open-GOP join): the spec says it may reference pictures from before the join + /// and must not be decoded or output (8.1.3 NOTE). The AU is deliberately not + /// planned — planner state is untouched and the next AU plans normally, mirroring + /// how the H.264 layer refuses pre-anchor pictures without wedging. + RaslSkipped { + poc: i32, + }, +} + +impl std::fmt::Display for PlanError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlanError::Parse(msg) => write!(f, "parse error: {msg}"), + PlanError::OutsideEnvelope(what) => { + write!(f, "outside the punktfunk decode envelope: {what}") + } + PlanError::NoActiveParamSet { pps_id } => { + write!(f, "slice references PPS {pps_id}, which has not been seen") + } + PlanError::AwaitingIdr => { + write!(f, "flushed: waiting for an IRAP to resume planning") + } + PlanError::RaslSkipped { poc } => { + write!( + f, + "RASL picture (poc {poc}) after a CRA join is not decodable" + ) + } + } + } +} + +impl std::error::Error for PlanError {} + +/// Keeps track of the last values seen for negotiation purposes. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct NegotiationInfo { + coded_resolution: Resolution, + general_profile_idc: u8, + bit_depth_luma_minus8: u8, + bit_depth_chroma_minus8: u8, + chroma_format_idc: u8, + max_dpb_frames: usize, + /// Output latency is a negotiation fact too: an SPS that raises the reorder + /// depth mid-stream (same geometry) changes what the session must expect from + /// [`DpbUpdate::outputs`], and it is what [`PlanWarning::NonZeroReorder`] keys + /// on — omitting it would let a 0→N reorder switch stall outputs silently. + max_num_reorder_pics: u8, +} + +impl From<&Sps> for NegotiationInfo { + fn from(sps: &Sps) -> Self { + NegotiationInfo { + coded_resolution: Resolution::from((u32::from(sps.width()), u32::from(sps.height()))), + general_profile_idc: sps.profile_tier_level.general_profile_idc, + bit_depth_luma_minus8: sps.bit_depth_luma_minus8, + bit_depth_chroma_minus8: sps.bit_depth_chroma_minus8, + chroma_format_idc: sps.chroma_format_idc, + max_dpb_frames: dpb_limit(sps), + max_num_reorder_pics: sps.max_num_reorder_pics[usize::from(sps.max_sub_layers_minus1)], + } + } +} + +/// The DPB size the planner enforces: equation A-2 (via the vendored +/// `Sps::max_dpb_size`), never above 16 — and never below the stream's +/// `sps_max_dec_pic_buffering_minus1 + 1`, which C.5.2.2's bumping uses as its +/// fullness bound. A conforming stream keeps the latter within A-2 (its constraint +/// clause), so the `max` only widens the pool for streams that already violate A.4 — +/// storing their pictures beats erroring the AU. +fn dpb_limit(sps: &Sps) -> usize { + let buffering = + usize::from(sps.max_dec_pic_buffering_minus1[usize::from(sps.max_sub_layers_minus1)]) + 1; + sps.max_dpb_size().max(buffering).min(16) +} + +/// The RefPicSet data (8.3.2), derived once per picture. +/// +/// Upstream keeps fixed `[_; 16]` arrays plus counts; `Vec`s here carry the same +/// derivation without the out-of-bounds panics a hostile header could otherwise +/// reach (a slice may signal more RPS entries than any conforming DPB holds). +#[derive(Default)] +struct RefPicSet { + /// `PocStCurrBefore` / `PocStCurrAfter` / `PocStFoll` (equation 8-5). + poc_st_curr_before: Vec, + poc_st_curr_after: Vec, + poc_st_foll: Vec, + /// `PocLtCurr` / `PocLtFoll` with their `delta_poc_msb_present_flag` (8-5). + poc_lt_curr: Vec<(i32, bool)>, + poc_lt_foll: Vec<(i32, bool)>, + + /// The resolved sets (equations 8-6/8-7). `None` = the DPB does not hold the + /// picture (lost upstream); kept positional so the reference-list construction + /// can substitute in place. + ref_pic_set_st_curr_before: Vec>>, + ref_pic_set_st_curr_after: Vec>>, + ref_pic_set_lt_curr: Vec>>, + ref_pic_set_st_foll: Vec>>, + ref_pic_set_lt_foll: Vec>>, +} + +impl RefPicSet { + /// Whether the current picture has any usable reference at all. + fn curr_is_empty(&self) -> bool { + self.poc_st_curr_before.is_empty() + && self.poc_st_curr_after.is_empty() + && self.poc_lt_curr.is_empty() + } +} + +/// State of the picture being planned, spanning the slice segments of one AU. +struct CurrentPicState { + /// Data for the current picture as extracted from the stream. + pic: PictureData, + /// PPS at the time of the current slice segment. Follows the slices — a later + /// segment may reference another PPS — and feeds end-of-picture bumping, as + /// upstream does. + pps: Rc, + /// The PPS the picture was BEGUN with. [`H265Planner::picture_plan`] reads this + /// snapshot so per-picture parameters cannot drift to a later segment's PPS. + first_slice_pps: Rc, + /// The id backends will know this picture by (upstream: the backend picture). + id: PicId, + /// The picture's RPS, resolved at begin-picture time and captured for the plan. + rps_plan: RpsPlan, + /// The marked DPB as it stands for THIS picture's decode, captured beside the + /// RPS that marked it — see [`AuPlan::dpb_refs`]. + dpb_refs: Vec, + /// 7.4.7.1: `slice_segment_address` must increase across a picture's segments. + prev_segment_address: Option, +} + +/// Plans H.265 access units for stateless hardware decoders. +/// +/// Owns the vendored parser and DPB plus the POC/RPS state that upstream keeps in +/// `H265DecoderState`. One instance per elementary stream; feed AUs in decode order. +pub struct H265Planner { + parser: Parser, + negotiation_info: NegotiationInfo, + dpb: Dpb, + rps: RefPicSet, + /// Same as `PrevTid0Pic` in the specification (8.3.1). + prev_tid0_pic: Option, + /// `MaxPicOrderCntLsb` of the ACTIVE SPS. Upstream latches this at SPS parse + /// time — the last SPS PARSED, not the one the picture activates; taken from the + /// activating PPS's SPS here instead (deliberate divergence, in favour of 8.3.1 + /// which reads the active SPS). + max_pic_order_cnt_lsb: i32, + /// The value of `NoRaslOutputFlag` for the last IRAP picture. + irap_no_rasl_output_flag: bool, + /// Whether the next picture is the first in the bitstream / follows an EOS NALU + /// (both feed `NoRaslOutputFlag`, 8.1.3). + first_picture_in_bitstream: bool, + first_picture_after_eos: bool, + /// The last independent slice segment header, copied into dependent segments + /// (7.4.7.1). + last_independent_header: Option, + /// Next [`PicId`] to hand out (upstream: the backend allocates here). + next_pic_id: PicId, + /// Display-ready pictures accumulated while planning (upstream: the decoder's + /// ready queue). Not cleared on a failed AU — the next emitted [`DpbUpdate`] + /// carries them, so an error can never swallow a frame. + pending_outputs: Vec, + /// Ids the last emitted [`DpbUpdate`] left alive: the baseline for `removed`. + /// Kept across failed AUs so interim evictions are reported, never dropped. + reported_live: BTreeSet, + /// Set by [`Self::flush`]: planning resumes only at an IRAP (upstream: `Reset`). + awaiting_idr: bool, +} + +impl Default for H265Planner { + fn default() -> Self { + Self { + parser: Default::default(), + negotiation_info: Default::default(), + dpb: Default::default(), + rps: Default::default(), + prev_tid0_pic: None, + max_pic_order_cnt_lsb: 0, + irap_no_rasl_output_flag: false, + first_picture_in_bitstream: true, + first_picture_after_eos: true, + last_independent_header: None, + next_pic_id: 0, + pending_outputs: Vec::new(), + reported_live: BTreeSet::new(), + awaiting_idr: false, + } + } +} + +impl H265Planner { + pub fn new() -> Self { + Default::default() + } + + /// Plan one access unit: Annex-B bytes containing VPS/SPS/PPS/SEI/AUD NALUs plus + /// the 1..N slice segment NALUs of exactly one picture. + /// + /// After a [`PlanError`] the planner state is best-effort ([`PlanError::RaslSkipped`] + /// excepted — that one leaves the state fully intact); the session should request + /// an IDR before feeding more AUs. Outputs and removals queued by a failed AU are + /// retained and emitted with the next successful plan (or [`Self::flush`]) — never + /// discarded. + pub fn plan_au(&mut self, au: &[u8]) -> Result { + let mut warnings = Vec::new(); + let mut slices = Vec::new(); + let mut recovery_point = None; + let mut current: Option = None; + let mut saw_nalu = false; + + // Byte position just past the last fully consumed NALU: the truncation + // detector's anchor. (The cursor is useless for this — after a successful + // `Nalu::next` it sits at the CURRENT NALU's header, and a failed one leaves + // it mid-scan, so a cursor-based "start code behind the cursor" test can + // never fire.) + let mut consumed_end = 0usize; + let mut cursor = Cursor::new(au); + loop { + let nalu = match Nalu::next(&mut cursor) { + Ok(nalu) => nalu, + Err(_) => { + // End of the AU — or a NALU cut so short its two header bytes are + // missing (unlike H.264, every 6-bit HEVC type code is a valid + // header, so this is the only header-level failure). Anything + // after the last consumed NALU other than zero bytes + // (trailing_zero_8bits padding, B.2.2) is cut-off data: degrade + // to a concealment signal covering the slices already planned. + let tail = &au[consumed_end.min(au.len())..]; + if tail.iter().any(|&b| b != 0) { + warnings.push(PlanWarning::TruncatedAu { + offset: consumed_end, + }); + } + break; + } + }; + saw_nalu = true; + // After `Nalu::next` the cursor sits on the NAL header bytes; `offset` is + // the start-code length and `size` the NALU payload length, which pins the + // NALU's absolute byte range in the AU without copying. + let nalu_offset = cursor.position() as usize; + let range = (nalu_offset - nalu.offset)..(nalu_offset + nalu.size); + debug_assert_eq!(&au[range.clone()], nalu.data.as_ref()); + consumed_end = range.end; + + // Multilayer/scalable streams put enhancement layers at nuh_layer_id > 0; + // punktfunk hosts emit single-layer only, and half-decoding the base layer + // of a stream we do not understand is exactly the kind of silent + // degradation the envelope gates exist to prevent. + if nalu.header.nuh_layer_id != 0 { + return Err(PlanError::OutsideEnvelope( + "multilayer stream (nuh_layer_id != 0)", + )); + } + + match nalu.header.type_ { + NaluType::VpsNut => { + self.parser.parse_vps(&nalu).map_err(PlanError::Parse)?; + } + NaluType::SpsNut => { + let sps = self.parser.parse_sps(&nalu).map_err(PlanError::Parse)?; + Self::check_envelope(sps)?; + } + NaluType::PpsNut => { + self.parser.parse_pps(&nalu).map_err(PlanError::Parse)?; + } + NaluType::PrefixSeiNut => { + // The HEVC NAL header is two bytes; the recovery point is a + // prefix-only payload (D.2.1), so suffix SEI NALUs never carry it + // and fall through to the skip arm below. + match sei::parse_recovery_point_hevc(nalu.as_ref().get(2..).unwrap_or(&[])) { + Ok(Some(rp)) => recovery_point = Some(rp), + Ok(None) => {} + // A broken SEI must not cost the picture it decorates. + Err(err) => trace!("ignoring unparseable SEI NALU: {err}"), + } + } + NaluType::EosNut => { + // 8.1.3: the first picture after an end-of-sequence NALU gets + // NoRaslOutputFlag = 1. + self.first_picture_after_eos = true; + } + NaluType::EobNut => { + self.first_picture_in_bitstream = true; + } + NaluType::TrailN + | NaluType::TrailR + | NaluType::TsaN + | NaluType::TsaR + | NaluType::StsaN + | NaluType::StsaR + | NaluType::RadlN + | NaluType::RadlR + | NaluType::RaslN + | NaluType::RaslR + | NaluType::BlaWLp + | NaluType::BlaWRadl + | NaluType::BlaNLp + | NaluType::IdrWRadl + | NaluType::IdrNLp + | NaluType::CraNut => { + // An AU that OPENS with a continuation segment (the first payload + // bit — first_slice_segment_in_pic_flag — is 0) is the tail of a + // previous picture, mis-split onto this AU. Beginning a picture + // from it would fabricate a duplicate (a dependent segment would + // even inherit a PREVIOUS AU's independent header wholesale), so + // it is skipped behind a concealment signal instead. + let first_segment = nalu.as_ref().get(2).is_some_and(|byte| byte & 0x80 != 0); + if current.is_none() && !first_segment { + warnings.push(PlanWarning::TruncatedAu { + offset: range.start, + }); + continue; + } + // Upstream's `Reset` gating: after a flush, only an IRAP restarts + // the decoding process (any IRAP works here, not just IDR: the + // flush set `first_picture_after_eos`, which hands a CRA/BLA + // NoRaslOutputFlag = 1 and with it full re-entry semantics). + // The gate is CLEARED only once the IRAP's picture actually + // begins — an IRAP AU that fails before that must not unlatch it. + if current.is_none() && self.awaiting_idr && !nalu.header.type_.is_irap() { + return Err(PlanError::AwaitingIdr); + } + let mut slice = match self.parser.parse_slice_header(nalu) { + Ok(slice) => slice, + Err(err) => { + // A continuation segment that fails to parse is a cut mid- + // AU: keep the slices already planned behind a concealment + // signal. (The H.264 layer's equivalent cut fires one + // level up, at the NALU header — HEVC's 2-byte header + // accepts every type code, so the failure surfaces here.) + if current.is_some() { + warnings.push(PlanWarning::TruncatedAu { + offset: range.start, + }); + break; + } + return Err(Self::slice_parse_error(err)); + } + }; + + // 7.4.7.1: a dependent slice segment inherits everything but its + // address from the preceding independent one. Completing the + // header HERE means every SlicePlan carries a full header and the + // picture-continuity checks below see real values. + if slice.header.dependent_slice_segment_flag { + let independent = + self.last_independent_header.clone().ok_or_else(|| { + PlanError::Parse( + "dependent slice segment without a preceding \ + independent slice segment header" + .into(), + ) + })?; + slice + .replace_header(independent) + .map_err(PlanError::Parse)?; + } + + match ¤t { + None => { + current = Some(self.begin_picture(&slice, &mut warnings)?); + self.awaiting_idr = false; + } + // Upstream would finish the picture and begin another; our + // contract is one picture per AU, so a second first-segment + // means the pump upstream of us is broken. + Some(_) if slice.header.first_slice_segment_in_pic_flag => { + return Err(PlanError::OutsideEnvelope( + "more than one coded picture in one access unit", + )); + } + Some(cur) => { + // Mis-split-AU guard: a continuation segment must belong + // to the picture the first segment began (7.4.2.4.4: same + // NALU type; 7.4.7.1: same POC lsb). A foreign slice and + // everything after it are dropped behind a concealment + // signal. + if slice.nalu.header.type_ != cur.pic.nalu_type + || i32::from(slice.header.pic_order_cnt_lsb) + != cur.pic.slice_pic_order_cnt_lsb + { + warnings.push(PlanWarning::TruncatedAu { + offset: range.start, + }); + break; + } + } + } + if !slice.header.dependent_slice_segment_flag { + self.last_independent_header = Some(slice.header.clone()); + } + let cur = current.as_mut().expect("a picture was begun above"); + slices.push(self.plan_slice(cur, slice, range, &mut warnings)?); + } + other => trace!("skipping NAL unit type {other:?}"), + } + } + + if !saw_nalu { + return Err(PlanError::Parse("no NAL units in access unit".into())); + } + let cur = current + .ok_or_else(|| PlanError::Parse("access unit contains no coded picture".into()))?; + + let picture = Self::picture_plan(&cur, recovery_point); + let rps = cur.rps_plan.clone(); + let dpb_refs = cur.dpb_refs.clone(); + // The activated parameter sets ride out with the plan (AuPlan field docs); + // cloned before finish_picture consumes `cur`. + let pps = Rc::clone(&cur.first_slice_pps); + let sps = Rc::clone(&pps.sps); + let stored = self.finish_picture(cur)?; + + // `removed` is the delta against what the backend last SAW alive, not against + // this call's start — a failed AU in between may have evicted pictures, and + // those removals must still be reported here. + let live_after = self.live_ids(); + let mut previously_live = mem::take(&mut self.reported_live); + previously_live.insert(stored); + let removed = previously_live.difference(&live_after).copied().collect(); + self.reported_live = live_after; + + Ok(AuPlan { + picture, + rps, + slices, + dpb: DpbUpdate { + stored: Some(stored), + outputs: mem::take(&mut self.pending_outputs), + removed, + }, + dpb_refs, + warnings, + sps, + pps, + }) + } + + /// Drain the DPB: every still-buffered picture becomes display-ready and every id + /// is released. The session calls this at teardown or a stream discontinuity. + /// + /// The 8.3 decoding state is discarded with the pictures; planning resumes only at + /// an IRAP ([`PlanError::AwaitingIdr`] until then). Parameter sets survive — per + /// 7.4.2.4 they persist until replaced. + pub fn flush(&mut self) -> DpbUpdate { + let mut removed = mem::take(&mut self.reported_live); + removed.extend(self.live_ids()); + self.drain_dpb(); + + self.rps = Default::default(); + self.prev_tid0_pic = None; + self.negotiation_info = Default::default(); + self.last_independent_header = None; + self.irap_no_rasl_output_flag = false; + // The resuming picture behaves as the first after an EOS: an IRAP of any + // flavour gets NoRaslOutputFlag = 1 (8.1.3), which is what makes non-IDR + // re-entry sound. + self.first_picture_after_eos = true; + self.awaiting_idr = true; + + DpbUpdate { + stored: None, + outputs: mem::take(&mut self.pending_outputs), + removed: removed.into_iter().collect(), + } + } + + /// The envelope gate: punktfunk clients only decode punktfunk hosts, and no host + /// emits interlaced video, separate-colour-plane coding, SCC self-referencing or + /// an oversized DPB. + fn check_envelope(sps: &Sps) -> Result<(), PlanError> { + if sps.separate_colour_plane_flag { + return Err(PlanError::OutsideEnvelope( + "separate colour plane coding (separate_colour_plane_flag == 1)", + )); + } + // HEVC has no frame_mbs_only_flag; field coding is signalled through the + // VUI's field_seq_flag (and pic_struct SEI). The vendored parser defaults the + // flag to 0 when the VUI is absent, so the read is safe unconditionally. + if sps.vui_parameters.field_seq_flag { + return Err(PlanError::OutsideEnvelope( + "field-coded stream (vui field_seq_flag == 1)", + )); + } + let ptl = &sps.profile_tier_level; + if ptl.general_interlaced_source_flag && !ptl.general_progressive_source_flag { + return Err(PlanError::OutsideEnvelope( + "interlaced source (general_interlaced_source_flag)", + )); + } + // A.4 caps the DPB at 16 frames. sps_max_dec_pic_buffering_minus1 is what + // C.5.2.2's fullness clause trusts, the vendored parser reads it up to 16 + // (17 frames), and no hardware decoder implements a deeper DPB — a larger + // value is a corrupt (or hostile) SPS, not a feature request. Backends size + // real slot pools from this, so it is gated here, at SPS activation. + let buffering = + usize::from(sps.max_dec_pic_buffering_minus1[usize::from(sps.max_sub_layers_minus1)]) + + 1; + if buffering > 16 { + return Err(PlanError::OutsideEnvelope( + "DPB deeper than 16 frames (sps_max_dec_pic_buffering_minus1)", + )); + } + if sps.scc_extension.curr_pic_ref_enabled_flag { + return Err(PlanError::OutsideEnvelope( + "SCC current-picture referencing (sps_curr_pic_ref_enabled_flag)", + )); + } + // 7.4.3.2.1 bounds the conformance window inside the coded size. The vendored + // `visible_rectangle()` subtracts in u32 and would panic on an offset sum + // beyond the picture; validated here (in u64 — the offsets are unbounded + // ue(v)) so a hostile SPS is an error, not a crash. + const SUB_WIDTH_C: [u64; 4] = [1, 2, 2, 1]; + const SUB_HEIGHT_C: [u64; 4] = [1, 2, 1, 1]; + if sps.conformance_window_flag { + let idx = usize::from(sps.chroma_array_type.min(3)); + let horizontal = SUB_WIDTH_C[idx] + * (u64::from(sps.conf_win_left_offset) + u64::from(sps.conf_win_right_offset)); + let vertical = SUB_HEIGHT_C[idx] + * (u64::from(sps.conf_win_top_offset) + u64::from(sps.conf_win_bottom_offset)); + if horizontal >= u64::from(sps.width()) || vertical >= u64::from(sps.height()) { + return Err(PlanError::Parse( + "conformance window exceeds the coded picture".into(), + )); + } + } + Ok(()) + } + + /// Map a vendored slice-header parse failure, sniffing the missing-PPS message so + /// it surfaces as [`PlanError::NoActiveParamSet`]. The prefix match is + /// best-effort: if an upstream re-sync rewords it, the error degrades to `Parse`, + /// not silence. + fn slice_parse_error(err: String) -> PlanError { + match err.strip_prefix("Could not get PPS for pic_parameter_set_id ") { + Some(id) => PlanError::NoActiveParamSet { + pps_id: id.trim().parse().unwrap_or(0), + }, + None => PlanError::Parse(err), + } + } + + /// Ids of every picture the DPB currently holds. + fn live_ids(&self) -> BTreeSet { + self.dpb.entries().iter().map(|entry| entry.1).collect() + } + + /// Queue the pictures the C.5.2 bumping process declares ready for output. + /// `additional` selects C.5.2.3 (after decoding the picture) over C.5.2.2 + /// (before), exactly upstream's `BumpingType`. + fn bump_as_needed(&mut self, sps: &Sps, additional: bool) { + loop { + let needs = if additional { + self.dpb.needs_additional_bumping(sps) + } else { + self.dpb.needs_bumping(sps) + }; + if !needs { + break; + } + match self.dpb.bump(false) { + Some(entry) => self.pending_outputs.push(entry.1), + None => break, + } + } + } + + /// Queue all frames still pending output and empty the DPB. + fn drain_dpb(&mut self) { + let pics = self.dpb.drain(); + self.pending_outputs.extend(pics.into_iter().map(|e| e.1)); + self.dpb.clear(); + } + + // See 8.3.2, Note 2. + fn st_ref_pic_set<'a>( + hdr: &'a SliceHeader, + sps: &'a Sps, + ) -> Result<&'a ShortTermRefPicSet, PlanError> { + if hdr.curr_rps_idx == sps.num_short_term_ref_pic_sets { + Ok(&hdr.short_term_ref_pic_set) + } else { + sps.short_term_ref_pic_set + .get(usize::from(hdr.curr_rps_idx)) + .ok_or_else(|| PlanError::Parse("invalid short_term_ref_pic_set_idx".into())) + } + } + + // See 8.3.2: derivation of the five POC lists. + fn decode_rps( + &mut self, + slice: &Slice, + sps: &Sps, + cur_pic: &PictureData, + warnings: &mut Vec, + ) -> Result<(), PlanError> { + let hdr = &slice.header; + + if cur_pic.nalu_type.is_irap() && cur_pic.no_rasl_output_flag { + self.dpb.mark_all_as_unused_for_ref(); + } + + self.rps = RefPicSet::default(); + + if !slice.nalu.header.type_.is_idr() { + let curr_st_rps = Self::st_ref_pic_set(hdr, sps)?; + // Equation 8-5, short-term half. Saturating adds: the deltas are + // stream-controlled and a POC outside i32 is a corrupt header, which must + // degrade to a missing-reference warning downstream, not overflow here + // (upstream adds unchecked). + for i in 0..usize::from(curr_st_rps.num_negative_pics) { + let poc = cur_pic + .pic_order_cnt_val + .saturating_add(curr_st_rps.delta_poc_s0[i]); + if curr_st_rps.used_by_curr_pic_s0[i] { + self.rps.poc_st_curr_before.push(poc); + } else { + self.rps.poc_st_foll.push(poc); + } + } + for i in 0..usize::from(curr_st_rps.num_positive_pics) { + let poc = cur_pic + .pic_order_cnt_val + .saturating_add(curr_st_rps.delta_poc_s1[i]); + if curr_st_rps.used_by_curr_pic_s1[i] { + self.rps.poc_st_curr_after.push(poc); + } else { + self.rps.poc_st_foll.push(poc); + } + } + + // Equation 8-5, long-term half: PocLtCurr/PocLtFoll from PocLsbLt plus the + // optional MSB cycle. This is the path punktfunk RFI recovery rides — a + // host pins a picture long-term and the recovery slice names it here. + let num_lt = usize::from(hdr.num_long_term_sps) + usize::from(hdr.num_long_term_pics); + for i in 0..num_lt.min(hdr.poc_lsb_lt.len()) { + let mut poc_lt = i64::from(hdr.poc_lsb_lt[i]); + if hdr.delta_poc_msb_present_flag[i] { + poc_lt += i64::from(cur_pic.pic_order_cnt_val); + poc_lt -= i64::from(hdr.delta_poc_msb_cycle_lt[i]) + * i64::from(self.max_pic_order_cnt_lsb); + poc_lt -= + i64::from(cur_pic.pic_order_cnt_val & (self.max_pic_order_cnt_lsb - 1)); + } + let poc_lt = poc_lt.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32; + if hdr.used_by_curr_pic_lt[i] { + self.rps + .poc_lt_curr + .push((poc_lt, hdr.delta_poc_msb_present_flag[i])); + } else { + self.rps + .poc_lt_foll + .push((poc_lt, hdr.delta_poc_msb_present_flag[i])); + } + } + } + + self.derive_and_mark_rps(warnings); + Ok(()) + } + + // See the derivation process in the second half of 8.3.2 (equations 8-6/8-7 plus + // the marking step). Upstream logs unresolvable entries and stores `None`; the + // `None` is kept (positional stability for the list construction) and each miss + // is a [`PlanWarning::MissingReference`] when the current picture needs it. + fn derive_and_mark_rps(&mut self, warnings: &mut Vec) { + let mask = self.max_pic_order_cnt_lsb.wrapping_sub(1); + + // Equation 8-6. + for &(poc, msb_present) in &self.rps.poc_lt_curr { + let reference = if msb_present { + self.dpb.find_ref_by_poc(poc) + } else { + // The lsb-masked lookup: 7.4.7.1 requires at most ONE reference per + // poc_lsb when delta_poc_msb is absent — this is the RFI anchor path, + // and silently taking the oldest of several matches (as the vendored + // find does) could hand the decoder the wrong anchor. Still picked + // (concealment posture), but flagged. + let candidates = self + .dpb + .pictures() + .filter(|p| p.is_ref() && (p.pic_order_cnt_val & mask) == poc) + .count(); + if candidates > 1 { + warnings.push(PlanWarning::MissingReference { + context: "ambiguous long-term poc_lsb (7.4.7.1 requires a unique match)", + detail: format!("poc lsb {poc}: {candidates} candidates"), + }); + } + self.dpb.find_ref_by_poc_masked(poc, mask) + }; + if reference.is_none() { + warnings.push(PlanWarning::MissingReference { + context: "long-term RPS entry (RefPicSetLtCurr)", + detail: format!("poc {poc}"), + }); + } + self.rps.ref_pic_set_lt_curr.push(reference); + } + for &(poc, msb_present) in &self.rps.poc_lt_foll { + let reference = if msb_present { + self.dpb.find_ref_by_poc(poc) + } else { + self.dpb.find_ref_by_poc_masked(poc, mask) + }; + if reference.is_none() { + // Not referenced by THIS picture — a future one names it, and its own + // RPS will warn then. A trace keeps join scenarios from spamming. + trace!("RefPicSetLtFoll entry poc {poc} not in the DPB"); + } + self.rps.ref_pic_set_lt_foll.push(reference); + } + + for pic in self.rps.ref_pic_set_lt_curr.iter().flatten() { + pic.0.borrow_mut().set_reference(Reference::LongTerm); + } + for pic in self.rps.ref_pic_set_lt_foll.iter().flatten() { + pic.0.borrow_mut().set_reference(Reference::LongTerm); + } + + // Equation 8-7. + for &poc in &self.rps.poc_st_curr_before { + let reference = self.dpb.find_short_term_ref_by_poc(poc); + if reference.is_none() { + warnings.push(PlanWarning::MissingReference { + context: "short-term RPS entry (RefPicSetStCurrBefore)", + detail: format!("poc {poc}"), + }); + } + self.rps.ref_pic_set_st_curr_before.push(reference); + } + for &poc in &self.rps.poc_st_curr_after { + let reference = self.dpb.find_short_term_ref_by_poc(poc); + if reference.is_none() { + warnings.push(PlanWarning::MissingReference { + context: "short-term RPS entry (RefPicSetStCurrAfter)", + detail: format!("poc {poc}"), + }); + } + self.rps.ref_pic_set_st_curr_after.push(reference); + } + for &poc in &self.rps.poc_st_foll { + let reference = self.dpb.find_short_term_ref_by_poc(poc); + if reference.is_none() { + trace!("RefPicSetStFoll entry poc {poc} not in the DPB"); + } + self.rps.ref_pic_set_st_foll.push(reference); + } + + // 8.3.2 step 4: every DPB picture in none of the five sets is marked "unused + // for reference" (identity by Rc, not POC — POC collisions across a corrupt + // stream must not keep the wrong picture alive). + let in_any_set = |pic: &Rc>| { + self.rps + .ref_pic_set_lt_curr + .iter() + .chain(&self.rps.ref_pic_set_lt_foll) + .chain(&self.rps.ref_pic_set_st_curr_before) + .chain(&self.rps.ref_pic_set_st_curr_after) + .chain(&self.rps.ref_pic_set_st_foll) + .flatten() + .any(|entry| Rc::ptr_eq(&entry.0, pic)) + }; + for entry in self.dpb.entries() { + if !in_any_set(&entry.0) { + entry.0.borrow_mut().set_reference(Reference::None); + } + } + } + + // See C.5.2.2: the DPB update that runs before decoding the current picture. The + // exemption is for "picture 0" — the first picture of the BITSTREAM, whose DPB is + // empty by definition — and for nothing else: an IRAP that merely follows an + // in-band EOS still drains (or, under no_output_of_prior_pics_flag, discards) the + // previous sequence's pictures, so two sequences' outputs never interleave. + // `was_first_in_bitstream` is the flag value AT this picture (upstream clears its + // planner field before reading it, which makes its check vacuous; observable + // behavior only differs when the DPB is already empty). + fn update_dpb_before_decoding( + &mut self, + cur_pic: &PictureData, + was_first_in_bitstream: bool, + sps: &Sps, + ) { + if cur_pic.nalu_type.is_irap() && cur_pic.no_rasl_output_flag && !was_first_in_bitstream { + if cur_pic.no_output_of_prior_pics_flag { + // C.3.2: prior pictures are discarded without output. + self.dpb.clear(); + } else { + self.drain_dpb(); + } + } else { + self.dpb.remove_unused(); + self.bump_as_needed(sps, false); + } + } + + /// Called once per picture, on its first slice segment. + fn begin_picture( + &mut self, + slice: &Slice, + warnings: &mut Vec, + ) -> Result { + let hdr = &slice.header; + let pps = Rc::clone(self.parser.get_pps(hdr.pic_parameter_set_id).ok_or( + PlanError::NoActiveParamSet { + pps_id: hdr.pic_parameter_set_id, + }, + )?); + // The SPS-level SCC gate cannot catch a PPS that enables self-referencing on + // its own; a reference list containing the current picture is a backend + // contract violation, so it fails closed here, at activation. + if pps.scc_extension.curr_pic_ref_enabled_flag { + return Err(PlanError::OutsideEnvelope( + "SCC current-picture referencing (pps_curr_pic_ref_enabled_flag)", + )); + } + + // The envelope gate runs at EVERY activation, not only when NegotiationInfo + // changes: the parser's table keeps an SPS whose parse-time gate rejected its + // AU, and NegotiationInfo deliberately omits envelope-only facts (conformance + // window, field_seq_flag, SCC flags) — a PPS-only AU rebinding to such an SPS + // must not smuggle it past the gate (the conformance-window leg would reach + // `visible_rectangle()`'s unchecked u32 subtraction). + Self::check_envelope(&pps.sps)?; + + // 8.3.1 reads MaxPicOrderCntLsb off the ACTIVE SPS (see the field doc for the + // upstream divergence). Kept local until the picture is accepted below. + let max_pic_order_cnt_lsb = 1i32 << (pps.sps.log2_max_pic_order_cnt_lsb_minus4 + 4); + + // The vendored PictureData runs the 8.3.1 POC process and the 8.1.3 output + // flags in its constructor — a pure computation, no planner state touched. + let pic = PictureData::new_from_slice( + slice, + self.first_picture_in_bitstream, + self.first_picture_after_eos, + self.prev_tid0_pic.as_ref(), + max_pic_order_cnt_lsb, + ); + + if pic.nalu_type.is_rasl() && self.irap_no_rasl_output_flag { + // 8.1.3 NOTE: RASL pictures of an IRAP with NoRaslOutputFlag = 1 may + // reference pictures from before the join and are neither decoded nor + // output. Refused BEFORE any state change — including renegotiation: a + // RASL AU carrying a renegotiating SPS must not drain the DPB on its way + // out (upstream also drops here, but after consuming its firstness + // flags). + return Err(PlanError::RaslSkipped { + poc: pic.pic_order_cnt_val, + }); + } + + // A picture's SPS may require renegotiation. From here on the picture is + // accepted and state changes begin. + self.renegotiate_if_needed(&pps.sps, warnings); + self.max_pic_order_cnt_lsb = max_pic_order_cnt_lsb; + + if pic.nalu_type.is_irap() { + self.irap_no_rasl_output_flag = pic.no_rasl_output_flag; + } + + let was_first_in_bitstream = self.first_picture_in_bitstream; + self.first_picture_after_eos = false; + self.first_picture_in_bitstream = false; + + // Upstream secures the backend picture here; the plan's equivalent is the id + // backends will allocate against. + let id = self.next_pic_id; + self.next_pic_id += 1; + + self.decode_rps(slice, &pps.sps, &pic, warnings)?; + self.update_dpb_before_decoding(&pic, was_first_in_bitstream, &pps.sps); + + let rps_plan = self.rps_plan(); + // Taken here, beside the RPS that marked it, and never later: `finish_picture` + // stores the current picture, which belongs to the NEXT AU's snapshot. + let dpb_refs = self.dpb_snapshot(); + + Ok(CurrentPicState { + pic, + first_slice_pps: Rc::clone(&pps), + pps, + id, + rps_plan, + dpb_refs, + prev_segment_address: None, + }) + } + + /// The marked DPB as [`AuPlan::dpb_refs`] reports it — every picture 8.3.2 left + /// marked "used for short-term reference" or "used for long-term reference", + /// whether or not THIS picture names it. + fn dpb_snapshot(&self) -> Vec { + self.dpb + .get_all_references() + .iter() + .map(Self::to_ref_pic) + .collect() + } + + /// Infallible by design: the caller ([`Self::begin_picture`]) has already run the + /// envelope gate on this SPS, and everything here is bookkeeping. + fn renegotiate_if_needed(&mut self, sps: &Sps, warnings: &mut Vec) { + if NegotiationInfo::from(sps) == self.negotiation_info { + return; + } + // Make sure all the frames planned so far are display-ready before the + // stream parameters change under them. + self.drain_dpb(); + self.negotiation_info = NegotiationInfo::from(sps); + self.dpb.set_max_num_pics(dpb_limit(sps)); + + let reorder = sps.max_num_reorder_pics[usize::from(sps.max_sub_layers_minus1)]; + if reorder > 0 { + warnings.push(PlanWarning::NonZeroReorder { + max_num_reorder_pics: reorder, + }); + } + } + + /// Handle one slice segment of the current picture (upstream: `handle_slice`). + fn plan_slice( + &self, + cur: &mut CurrentPicState, + slice: Slice, + data: Range, + warnings: &mut Vec, + ) -> Result { + // 7.4.7.1: slice_segment_address increases across a picture's segments. + if let Some(prev) = cur.prev_segment_address { + if slice.header.segment_address <= prev && !slice.header.first_slice_segment_in_pic_flag + { + trace!("slice_segment_address does not increase monotonically, expect corrupted output"); + } + } + cur.prev_segment_address = Some(slice.header.segment_address); + + // A slice segment can technically refer to another PPS. + let pps = self + .parser + .get_pps(slice.header.pic_parameter_set_id) + .ok_or(PlanError::NoActiveParamSet { + pps_id: slice.header.pic_parameter_set_id, + })?; + cur.pps = Rc::clone(pps); + + // Make sure that no negotiation is possible mid-picture. How could it? + // We'd lose the context of the previous slices. + if NegotiationInfo::from(&*cur.pps.sps) != self.negotiation_info { + return Err(PlanError::Parse( + "invalid stream: mid-picture renegotiation requested".into(), + )); + } + + let (ref_list0, ref_list1) = self.build_ref_pic_lists(&slice.header, warnings); + + // An inter slice shall have at least one usable reference (8.3.4 requires + // num_ref_idx_l0_active entries). Ending up empty — every candidate lost — + // is undecodable-as-intended: flag it so the session requests recovery. + let slice_type = slice.header.type_; + if (slice_type.is_p() || slice_type.is_b()) && ref_list0.is_empty() { + warnings.push(PlanWarning::MissingReference { + context: "inter slice with no usable RefPicList0", + detail: format!("slice_type {slice_type:?}"), + }); + } + if slice_type.is_b() && ref_list1.is_empty() { + warnings.push(PlanWarning::MissingReference { + context: "B slice with no usable RefPicList1", + detail: format!("slice_type {slice_type:?}"), + }); + } + + Ok(SlicePlan { + data, + header: slice.header, + ref_list0, + ref_list1, + }) + } + + // See 8.3.4: reference picture list construction for P and B slices, already + // converted to backend-facing [`RefPic`]s with in-place concealment. + fn build_ref_pic_lists( + &self, + hdr: &SliceHeader, + warnings: &mut Vec, + ) -> (Vec, Vec) { + // I slices do not use inter prediction. + if !hdr.type_.is_p() && !hdr.type_.is_b() { + return (Vec::new(), Vec::new()); + } + + // The 8-8/8-10 temporal-list loops cycle the three current sets until the + // list is full; with all three empty they would never terminate. Upstream + // only guards this behind its SCC flags — a bare broken P slice loops + // forever there. Bail out with empty lists; plan_slice flags them. + if self.rps.curr_is_empty() { + return (Vec::new(), Vec::new()); + } + + let list0 = self.build_one_list( + hdr, + usize::from(hdr.num_ref_idx_l0_active_minus1) + 1, + hdr.ref_pic_list_modification + .ref_pic_list_modification_flag_l0, + &hdr.ref_pic_list_modification.list_entry_l0, + // Equation 8-8: list 0 leads with the past (StCurrBefore first). + [ + &self.rps.ref_pic_set_st_curr_before, + &self.rps.ref_pic_set_st_curr_after, + &self.rps.ref_pic_set_lt_curr, + ], + warnings, + ); + + let list1 = if hdr.type_.is_b() { + self.build_one_list( + hdr, + usize::from(hdr.num_ref_idx_l1_active_minus1) + 1, + hdr.ref_pic_list_modification + .ref_pic_list_modification_flag_l1, + &hdr.ref_pic_list_modification.list_entry_l1, + // Equation 8-10: list 1 leads with the future (StCurrAfter first). + [ + &self.rps.ref_pic_set_st_curr_after, + &self.rps.ref_pic_set_st_curr_before, + &self.rps.ref_pic_set_lt_curr, + ], + warnings, + ) + } else { + Vec::new() + }; + + (list0, list1) + } + + #[allow(clippy::type_complexity)] + fn build_one_list( + &self, + hdr: &SliceHeader, + num_active: usize, + modification_flag: bool, + list_entries: &[u32], + set_order: [&Vec>>; 3], + warnings: &mut Vec, + ) -> Vec { + // Equations 8-8/8-10: RefPicListTempX cycles the current sets until + // NumRpsCurrTempListX entries exist. + let temp_len = num_active.max(hdr.num_pic_total_curr as usize); + let mut temp: Vec> = Vec::with_capacity(temp_len); + 'fill: while temp.len() < temp_len { + for set in set_order { + for entry in set { + if temp.len() == temp_len { + break 'fill; + } + temp.push(entry.as_ref().map(Self::to_ref_pic)); + } + } + } + + // Equations 8-9/8-11: the final list is the temporal list, reordered through + // list_entry_lX when the modification flag is set. + let mut list: Vec> = Vec::with_capacity(num_active); + for r_idx in 0..num_active { + let entry = if modification_flag { + match list_entries + .get(r_idx) + .and_then(|&idx| temp.get(idx as usize)) + { + Some(entry) => *entry, + None => { + // The parser bounds list_entry below NumPicTotalCurr, so this + // is unreachable on its output — belt over suspenders for a + // future parser re-sync. + warnings.push(PlanWarning::MissingReference { + context: "ref_pic_list_modification entry out of range", + detail: format!("ref_idx {r_idx}"), + }); + None + } + } + } else { + temp.get(r_idx).copied().flatten() + }; + list.push(entry); + } + + Self::substitute_in_place(list) + } + + /// Fill the holes a lost reference leaves, preserving list positions: every + /// `ref_idx` in the slice syntax indexes the returned Vec 1:1. A hole is + /// substituted by the nearest existing reference in list order (the previous + /// existing entry, else the first existing one) — stable-but-wrong concealment, + /// already flagged via [`PlanWarning::MissingReference`] when the RPS was derived. + /// Compacting instead would shift every subsequent ref_idx and make the decoder + /// predict from the wrong pictures. Only a list with no existing reference at all + /// collapses to empty (the caller warns on that separately). + fn substitute_in_place(list: Vec>) -> Vec { + let first_existing = list.iter().flatten().next().copied(); + let mut out = Vec::with_capacity(list.len()); + let mut prev_existing: Option = None; + for slot in &list { + match slot { + Some(real) => { + prev_existing = Some(*real); + out.push(*real); + } + None => { + if let Some(substitute) = prev_existing.or(first_existing) { + out.push(substitute); + } + } + } + } + out + } + + fn to_ref_pic(entry: &DpbEntry) -> RefPic { + let pic = entry.0.borrow(); + RefPic { + id: entry.1, + pic_order_cnt: pic.pic_order_cnt_val, + is_long_term: matches!(pic.reference(), Reference::LongTerm), + } + } + + /// Snapshot the resolved current sets for the plan (backends rebuild their + /// reference arrays from these). + fn rps_plan(&self) -> RpsPlan { + let convert = |set: &Vec>>| -> Vec { + set.iter().flatten().map(Self::to_ref_pic).collect() + }; + RpsPlan { + st_curr_before: convert(&self.rps.ref_pic_set_st_curr_before), + st_curr_after: convert(&self.rps.ref_pic_set_st_curr_after), + lt_curr: convert(&self.rps.ref_pic_set_lt_curr), + } + } + + fn finish_picture(&mut self, cur: CurrentPicState) -> Result { + let CurrentPicState { pic, pps, id, .. } = cur; + + // 8.3.1: this picture becomes PrevTid0Pic for the next one if eligible. + if pic.valid_for_prev_tid0_pic { + self.prev_tid0_pic = Some(pic.clone()); + } + + let sps = Rc::clone(&pps.sps); + + // First store the current picture in the DPB, only then decide whether to + // bump (C.3.4 marks it short-term inside store_picture). + self.dpb + .store_picture(Rc::new(RefCell::new(pic)), id) + .map_err(PlanError::Parse)?; + self.bump_as_needed(&sps, true); + + Ok(id) + } + + fn picture_plan( + cur: &CurrentPicState, + recovery_point: Option, + ) -> PicturePlan { + let pic = &cur.pic; + // The first slice's PPS defines the picture's parameters; `cur.pps` may have + // drifted to a later segment's. + let sps = &cur.first_slice_pps.sps; + let rect = sps.visible_rectangle(); + + PicturePlan { + nalu_type: pic.nalu_type, + is_idr: pic.nalu_type.is_idr(), + is_irap: pic.nalu_type.is_irap(), + no_rasl_output_flag: pic.no_rasl_output_flag, + is_reference: !pic.nalu_type.is_slnr(), + pic_order_cnt: pic.pic_order_cnt_val, + coded_width: u32::from(sps.width()), + coded_height: u32::from(sps.height()), + // The vendored `visible_rectangle()` returns the crop OFFSET in `min` and + // the visible SIZE in `max` (not an edge coordinate), with the + // SubWidthC/SubHeightC scaling already applied — same convention the + // H.264 layer verified. + display_crop: DisplayCrop { + x: rect.min.x, + y: rect.min.y, + width: rect.max.x, + height: rect.max.y, + }, + // Read unconditionally: the vendored parser builds every SPS from + // `Default`, whose `VuiParams` already holds E.3.1's inferred values + // (2/2/2, limited range), and parsing only overwrites them under the + // present flags — so this IS the spec inference whether or not the + // stream carried a VUI. (The vendored fields are u32; E.2 reads them as + // u(8), so the casts cannot truncate.) + colour: ColourDescription { + colour_primaries: sps.vui_parameters.colour_primaries as u8, + transfer_characteristics: sps.vui_parameters.transfer_characteristics as u8, + matrix_coefficients: sps.vui_parameters.matrix_coeffs as u8, + video_full_range: sps.vui_parameters.video_full_range_flag, + }, + general_profile_idc: sps.profile_tier_level.general_profile_idc, + level_idc: sps.profile_tier_level.general_level_idc, + bit_depth_luma_minus8: sps.bit_depth_luma_minus8, + bit_depth_chroma_minus8: sps.bit_depth_chroma_minus8, + chroma_format_idc: sps.chroma_format_idc, + max_dpb_frames: dpb_limit(sps), + short_term_ref_pic_set_size_bits: pic.short_term_ref_pic_set_size_bits, + recovery_point, + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use super::*; + + const TEST_25FPS: &[u8] = + include_bytes!("../vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265"); + const TEST_BEAR: &[u8] = + include_bytes!("../vendor/cros-codecs/src/codec/h265/test_data/bear.h265"); + const TEST_BBB: &[u8] = + include_bytes!("../vendor/cros-codecs/src/codec/h265/test_data/bbb.h265"); + const TEST_64X64_I_P_B_P: &[u8] = + include_bytes!("../vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P-B-P.h265"); + + /// Test-only AU splitter: the vendored vectors are raw Annex-B streams, while + /// `plan_au` takes the pre-split AUs punktfunk's pump produces. A new AU starts + /// at a non-VCL NALU following slices, or at a slice segment with + /// `first_slice_segment_in_pic_flag == 1` (the first bit of the payload, i.e. of + /// the byte after the 2-byte NAL header) when the current AU already has slices. + fn split_into_aus(stream: &[u8]) -> Vec<&[u8]> { + let mut aus = Vec::new(); + let mut cursor = Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + + while let Ok(nalu) = Nalu::next(&mut cursor) { + let header_start = cursor.position() as usize; + let start = header_start - nalu.offset; + let is_slice = (nalu.header.type_ as u32) < 32; + let first_slice_flag = + is_slice && stream.get(header_start + 2).is_some_and(|b| b & 0x80 != 0); + + if au_has_slice && (!is_slice || first_slice_flag) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus + } + + /// The concealment/envelope warnings a clean stream must not produce. + /// `NonZeroReorder` is excluded: the vendored conformance clips are general + /// (reordering) encodes, and the planner deliberately plans them while flagging + /// the envelope fact. + fn is_integrity_warning(w: &PlanWarning) -> bool { + matches!( + w, + PlanWarning::MissingReference { .. } | PlanWarning::TruncatedAu { .. } + ) + } + + /// Plan a whole vendored clip and assert the global invariants: every AU plans, + /// no integrity warnings, every stored id reaches output exactly once, and + /// outputs emerge in ascending POC order within each IRAP period. + fn plan_whole_clip(stream: &[u8]) -> (H265Planner, Vec) { + let aus = split_into_aus(stream); + let mut planner = H265Planner::new(); + let mut plans = Vec::new(); + for au in &aus { + plans.push( + planner + .plan_au(au) + .expect("the clean vector must plan without errors"), + ); + } + + for plan in &plans { + assert!( + !plan.warnings.iter().any(is_integrity_warning), + "clean vector produced an integrity warning: {:?}", + plan.warnings + ); + } + + let stored: BTreeSet = plans.iter().filter_map(|p| p.dpb.stored).collect(); + assert_eq!(stored.len(), plans.len()); + let mut emitted: Vec = plans + .iter() + .flat_map(|p| p.dpb.outputs.iter().copied()) + .collect(); + emitted.extend(planner.flush().outputs); + let output: BTreeSet = emitted.iter().copied().collect(); + assert_eq!( + output.len(), + emitted.len(), + "no picture may be output twice" + ); + assert_eq!( + output, stored, + "bumping plus the final flush must output every picture" + ); + + // Output ORDER, not just coverage: within each IRAP period, ids must emerge + // in ascending POC — the invariant the C.5.2 bumping process exists to + // provide. (An IRAP with NoRaslOutputFlag resets POC continuity, hence the + // period key.) + let mut period = 0usize; + let mut order_key: BTreeMap = BTreeMap::new(); + for plan in &plans { + if plan.picture.is_irap && plan.picture.no_rasl_output_flag { + period += 1; + } + order_key.insert( + plan.dpb.stored.unwrap(), + (period, plan.picture.pic_order_cnt), + ); + } + let mut last: Option<(usize, i32)> = None; + for id in &emitted { + let key = order_key[id]; + if let Some(last) = last { + assert!( + key > last, + "outputs must emerge in ascending POC order per IRAP period: \ + {key:?} emitted after {last:?}" + ); + } + last = Some(key); + } + + (planner, plans) + } + + #[test] + fn the_full_25fps_vector_plans_every_picture_and_every_pic_id_reaches_output() { + let aus = split_into_aus(TEST_25FPS); + assert_eq!(aus.len(), 250, "the vendored golden: 250 pictures"); + let (_, plans) = plan_whole_clip(TEST_25FPS); + assert_eq!(plans.len(), 250); + assert_eq!(plans.iter().map(|p| p.slices.len()).sum::(), 250); + assert!(plans[0].picture.is_idr); + assert_eq!( + plans[0].picture.pic_order_cnt, 0, + "POC 0 at the opening IRAP" + ); + for plan in &plans { + for slice in &plan.slices { + if slice.header.type_.is_p() || slice.header.type_.is_b() { + assert!(!slice.ref_list0.is_empty()); + } + } + } + } + + #[test] + fn the_bear_and_bbb_vectors_plan_clean_end_to_end() { + let (_, bear) = plan_whole_clip(TEST_BEAR); + assert!(!bear.is_empty()); + let (_, bbb) = plan_whole_clip(TEST_BBB); + assert!(!bbb.is_empty()); + } + + #[test] + fn b_slices_get_a_future_led_list1_distinct_from_list0() { + let aus = split_into_aus(TEST_64X64_I_P_B_P); + let mut planner = H265Planner::new(); + let mut b_slices_seen = 0usize; + + for au in &aus { + let plan = planner + .plan_au(au) + .expect("the clean vector must plan without errors"); + for slice in &plan.slices { + if !slice.header.type_.is_b() { + continue; + } + b_slices_seen += 1; + assert!(!slice.ref_list0.is_empty()); + assert!(!slice.ref_list1.is_empty()); + // 8.3.4: list0 leads with the past (StCurrBefore), list1 with the + // future (StCurrAfter). + assert!(slice.ref_list0[0].pic_order_cnt < plan.picture.pic_order_cnt); + assert!(slice.ref_list1[0].pic_order_cnt > plan.picture.pic_order_cnt); + assert!(!plan.rps.st_curr_before.is_empty()); + assert!(!plan.rps.st_curr_after.is_empty()); + } + } + + assert!(b_slices_seen > 0, "the vector must contain B slices"); + } + + /// Byte-level authoring: the vendored crate has no H.265 builders or synthesizer + /// (its encoder is H.264-only), so the tests carry a minimal bit writer plus + /// SPS/PPS/slice-segment writers for exactly the syntax the planner reads — + /// the h264 tests' `write_idr_slice` idiom, one codec over. The planner only + /// reads headers, so no slice data follows the alignment bit. + struct BitSink { + bytes: Vec, + acc: u8, + nbits: u8, + } + + impl BitSink { + fn new() -> Self { + BitSink { + bytes: Vec::new(), + acc: 0, + nbits: 0, + } + } + + fn bit(&mut self, b: u32) { + self.acc = (self.acc << 1) | (b as u8 & 1); + self.nbits += 1; + if self.nbits == 8 { + self.bytes.push(self.acc); + self.acc = 0; + self.nbits = 0; + } + } + + fn bits(&mut self, count: usize, value: u32) { + for i in (0..count).rev() { + self.bit((value >> i) & 1); + } + } + + fn ue(&mut self, value: u32) { + let x = value + 1; + let len = 32 - x.leading_zeros() as usize; + self.bits(len - 1, 0); + self.bits(len, x); + } + + fn se(&mut self, value: i32) { + let k = if value > 0 { + 2 * value as u32 - 1 + } else { + (-2 * (value as i64)) as u32 + }; + self.ue(k); + } + + /// rbsp_trailing_bits(): the stop bit plus zero padding to a byte boundary. + fn finish(mut self) -> Vec { + self.bit(1); + while self.nbits != 0 { + self.bit(0); + } + self.bytes + } + } + + /// Wrap an RBSP in start code + 2-byte HEVC NAL header + emulation prevention. + fn h265_nalu_with_layer(nalu_type: u8, layer_id: u8, rbsp: &[u8]) -> Vec { + let mut out = vec![ + 0x00, + 0x00, + 0x00, + 0x01, + (nalu_type << 1) | (layer_id >> 5), + ((layer_id & 0x1f) << 3) | 0x01, // nuh_temporal_id_plus1 = 1 + ]; + let mut zeros = 0usize; + for &byte in rbsp { + if zeros >= 2 && byte <= 0x03 { + out.push(0x03); + zeros = 0; + } + out.push(byte); + zeros = if byte == 0 { zeros + 1 } else { 0 }; + } + out + } + + fn h265_nalu(nalu_type: u8, rbsp: &[u8]) -> Vec { + h265_nalu_with_layer(nalu_type, 0, rbsp) + } + + #[derive(Clone)] + enum VuiOpt { + Absent, + SignalType { + full_range: bool, + colour: Option<(u8, u8, u8)>, + }, + FieldSeq, + } + + #[derive(Clone)] + struct SpsOpts { + profile_idc: u8, + chroma_format_idc: u32, + width: u32, + height: u32, + bit_depth_minus8: u32, + /// (left, right, top, bottom) conf_win offsets, in chroma units. + conf_win: Option<(u32, u32, u32, u32)>, + max_dec_pic_buffering_minus1: u32, + max_num_reorder_pics: u32, + long_term: bool, + vui: VuiOpt, + } + + impl Default for SpsOpts { + fn default() -> Self { + SpsOpts { + profile_idc: 1, // Main + chroma_format_idc: 1, + width: 64, + height: 64, + bit_depth_minus8: 0, + conf_win: None, + max_dec_pic_buffering_minus1: 4, + max_num_reorder_pics: 0, + long_term: false, + vui: VuiOpt::Absent, + } + } + } + + fn synth_sps(o: &SpsOpts) -> Vec { + let mut s = BitSink::new(); + s.bits(4, 0); // sps_video_parameter_set_id + s.bits(3, 0); // sps_max_sub_layers_minus1 + s.bit(1); // sps_temporal_id_nesting_flag + + // profile_tier_level(1, 0): general_profile_space u(2), tier u(1), + // profile_idc u(5), 32 compatibility flags, progressive/interlaced/ + // non-packed/frame-only, 43 constraint/reserved bits (all zero for every + // profile branch the parser takes), inbld/reserved bit, level u(8). + s.bits(2, 0); + s.bit(0); + s.bits(5, u32::from(o.profile_idc)); + s.bits(32, 0); + s.bit(1); // general_progressive_source_flag + s.bit(0); // general_interlaced_source_flag + s.bit(0); // general_non_packed_constraint_flag + s.bit(1); // general_frame_only_constraint_flag + s.bits(31, 0); + s.bits(12, 0); // 43 zero bits total + s.bit(0); // general_inbld_flag / reserved + s.bits(8, 120); // general_level_idc: level 4 + + s.ue(0); // sps_seq_parameter_set_id + s.ue(o.chroma_format_idc); + if o.chroma_format_idc == 3 { + s.bit(0); // separate_colour_plane_flag + } + s.ue(o.width); + s.ue(o.height); + match o.conf_win { + Some((left, right, top, bottom)) => { + s.bit(1); + s.ue(left); + s.ue(right); + s.ue(top); + s.ue(bottom); + } + None => s.bit(0), + } + s.ue(o.bit_depth_minus8); // bit_depth_luma_minus8 + s.ue(o.bit_depth_minus8); // bit_depth_chroma_minus8 + s.ue(0); // log2_max_pic_order_cnt_lsb_minus4: 4-bit POC lsb + s.bit(1); // sps_sub_layer_ordering_info_present_flag + s.ue(o.max_dec_pic_buffering_minus1); + s.ue(o.max_num_reorder_pics); + s.ue(0); // sps_max_latency_increase_plus1 + s.ue(0); // log2_min_luma_coding_block_size_minus3: 8 + s.ue(3); // log2_diff_max_min_luma_coding_block_size: CTB 64 + s.ue(0); // log2_min_luma_transform_block_size_minus2: 4 + s.ue(3); // log2_diff_max_min_luma_transform_block_size: 32 + s.ue(0); // max_transform_hierarchy_depth_inter + s.ue(0); // max_transform_hierarchy_depth_intra + s.bit(0); // scaling_list_enabled_flag + s.bit(0); // amp_enabled_flag + s.bit(0); // sample_adaptive_offset_enabled_flag + s.bit(0); // pcm_enabled_flag + s.ue(0); // num_short_term_ref_pic_sets + if o.long_term { + s.bit(1); // long_term_ref_pics_present_flag + s.ue(0); // num_long_term_ref_pics_sps + } else { + s.bit(0); + } + s.bit(0); // sps_temporal_mvp_enabled_flag + s.bit(0); // strong_intra_smoothing_enabled_flag + match &o.vui { + VuiOpt::Absent => s.bit(0), + vui => { + s.bit(1); // vui_parameters_present_flag + s.bit(0); // aspect_ratio_info_present_flag + s.bit(0); // overscan_info_present_flag + match vui { + VuiOpt::SignalType { full_range, colour } => { + s.bit(1); // video_signal_type_present_flag + s.bits(3, 5); // video_format: unspecified + s.bit(u32::from(*full_range)); + match colour { + Some((primaries, transfer, matrix)) => { + s.bit(1); // colour_description_present_flag + s.bits(8, u32::from(*primaries)); + s.bits(8, u32::from(*transfer)); + s.bits(8, u32::from(*matrix)); + } + None => s.bit(0), + } + } + _ => s.bit(0), // video_signal_type_present_flag + } + s.bit(0); // chroma_loc_info_present_flag + s.bit(0); // neutral_chroma_indication_flag + s.bit(u32::from(matches!(vui, VuiOpt::FieldSeq))); // field_seq_flag + s.bit(0); // frame_field_info_present_flag + s.bit(0); // default_display_window_flag + s.bit(0); // vui_timing_info_present_flag + s.bit(0); // bitstream_restriction_flag + } + } + s.bit(0); // sps_extension_present_flag + h265_nalu(33, &s.finish()) + } + + fn synth_pps(dependent_slice_segments: bool) -> Vec { + let mut s = BitSink::new(); + s.ue(0); // pps_pic_parameter_set_id + s.ue(0); // pps_seq_parameter_set_id + s.bit(u32::from(dependent_slice_segments)); + s.bit(0); // output_flag_present_flag + s.bits(3, 0); // num_extra_slice_header_bits + s.bit(0); // sign_data_hiding_enabled_flag + s.bit(0); // cabac_init_present_flag + s.ue(0); // num_ref_idx_l0_default_active_minus1 + s.ue(0); // num_ref_idx_l1_default_active_minus1 + s.se(0); // init_qp_minus26 + s.bit(0); // constrained_intra_pred_flag + s.bit(0); // transform_skip_enabled_flag + s.bit(0); // cu_qp_delta_enabled_flag + s.se(0); // pps_cb_qp_offset + s.se(0); // pps_cr_qp_offset + s.bit(0); // pps_slice_chroma_qp_offsets_present_flag + s.bit(0); // weighted_pred_flag + s.bit(0); // weighted_bipred_flag + s.bit(0); // transquant_bypass_enabled_flag + s.bit(0); // tiles_enabled_flag + s.bit(0); // entropy_coding_sync_enabled_flag + s.bit(0); // pps_loop_filter_across_slices_enabled_flag + s.bit(0); // deblocking_filter_control_present_flag + s.bit(0); // pps_scaling_list_data_present_flag + s.bit(0); // lists_modification_present_flag + s.ue(0); // log2_parallel_merge_level_minus2 + s.bit(0); // slice_segment_header_extension_present_flag + s.bit(0); // pps_extension_present_flag + h265_nalu(34, &s.finish()) + } + + const IDR_W_RADL: u8 = 19; + const TRAIL_R: u8 = 1; + const CRA_NUT: u8 = 21; + const RASL_N: u8 = 8; + + #[derive(Clone)] + struct SliceOpts { + nalu_type: u8, + layer_id: u8, + /// Continuation segments: `Some((address, address_bits, dependent))`. + segment: Option<(u32, usize, bool)>, + /// PPS 0 has dependent_slice_segments_enabled: the flag bit is only written + /// when the PPS enables it, so the writer must know. + pps_dependent_enabled: bool, + slice_type: u32, // 2 = I, 1 = P, 0 = B + poc_lsb: u32, + /// Short-term RPS: (delta_poc_sX_minus1, used_by_curr_pic) pairs. + neg: Vec<(u32, bool)>, + pos: Vec<(u32, bool)>, + /// Present only when the SPS set long_term_ref_pics_present_flag: + /// (poc_lsb_lt, used_by_curr_pic_lt, delta_poc_msb_cycle_lt). + lt: Vec<(u32, bool, Option)>, + sps_long_term: bool, + num_ref_idx_l0: u32, + num_ref_idx_l1: u32, + no_output_of_prior_pics: bool, + } + + impl Default for SliceOpts { + fn default() -> Self { + SliceOpts { + nalu_type: TRAIL_R, + layer_id: 0, + segment: None, + pps_dependent_enabled: false, + slice_type: 1, + poc_lsb: 0, + neg: Vec::new(), + pos: Vec::new(), + lt: Vec::new(), + sps_long_term: false, + num_ref_idx_l0: 1, + num_ref_idx_l1: 1, + no_output_of_prior_pics: false, + } + } + } + + fn synth_slice(o: &SliceOpts) -> Vec { + let is_irap = (16..=23).contains(&o.nalu_type); + let is_idr = o.nalu_type == IDR_W_RADL || o.nalu_type == 20; + let mut s = BitSink::new(); + s.bit(u32::from(o.segment.is_none())); // first_slice_segment_in_pic_flag + if is_irap { + s.bit(u32::from(o.no_output_of_prior_pics)); + } + s.ue(0); // slice_pic_parameter_set_id + let mut dependent = false; + if let Some((address, bits, dep)) = o.segment { + if o.pps_dependent_enabled { + s.bit(u32::from(dep)); + } + s.bits(bits, address); + dependent = dep; + } + if !dependent { + s.ue(o.slice_type); + if !is_idr { + s.bits(4, o.poc_lsb); + s.bit(0); // short_term_ref_pic_set_sps_flag + // st_ref_pic_set(stRpsIdx = 0): no inter-RPS prediction flag. + s.ue(o.neg.len() as u32); + s.ue(o.pos.len() as u32); + for &(delta_minus1, used) in &o.neg { + s.ue(delta_minus1); + s.bit(u32::from(used)); + } + for &(delta_minus1, used) in &o.pos { + s.ue(delta_minus1); + s.bit(u32::from(used)); + } + if o.sps_long_term { + s.ue(o.lt.len() as u32); // num_long_term_pics + for &(poc_lsb_lt, used, msb) in &o.lt { + s.bits(4, poc_lsb_lt); + s.bit(u32::from(used)); + match msb { + Some(cycle) => { + s.bit(1); + s.ue(cycle); + } + None => s.bit(0), + } + } + } + } + if o.slice_type != 2 { + s.bit(1); // num_ref_idx_active_override_flag + s.ue(o.num_ref_idx_l0 - 1); + if o.slice_type == 0 { + s.ue(o.num_ref_idx_l1 - 1); + s.bit(0); // mvd_l1_zero_flag + } + s.ue(0); // five_minus_max_num_merge_cand + } + s.se(0); // slice_qp_delta + } + h265_nalu_with_layer(o.nalu_type, o.layer_id, &s.finish()) + } + + fn idr_slice() -> Vec { + synth_slice(&SliceOpts { + nalu_type: IDR_W_RADL, + slice_type: 2, + ..Default::default() + }) + } + + fn trail_p(poc_lsb: u32, neg: &[(u32, bool)], num_ref_idx_l0: u32) -> Vec { + synth_slice(&SliceOpts { + poc_lsb, + neg: neg.to_vec(), + num_ref_idx_l0, + ..Default::default() + }) + } + + fn param_sets(sps: &SpsOpts) -> Vec { + let mut au = synth_sps(sps); + au.extend(synth_pps(false)); + au + } + + fn opening_idr_au(sps: &SpsOpts) -> Vec { + let mut au = param_sets(sps); + au.extend(idr_slice()); + au + } + + #[test] + fn an_idr_opens_planning_with_poc_zero_full_crop_and_inferred_colour() { + let mut planner = H265Planner::new(); + let plan = planner + .plan_au(&opening_idr_au(&SpsOpts::default())) + .unwrap(); + + assert!(plan.warnings.is_empty(), "{:?}", plan.warnings); + assert!(plan.picture.is_idr && plan.picture.is_irap); + assert!(plan.picture.no_rasl_output_flag); + assert!(plan.picture.is_reference); + assert_eq!(plan.picture.pic_order_cnt, 0); + assert_eq!(plan.picture.nalu_type, NaluType::IdrWRadl); + assert_eq!( + (plan.picture.coded_width, plan.picture.coded_height), + (64, 64) + ); + assert_eq!( + plan.picture.display_crop, + DisplayCrop { + x: 0, + y: 0, + width: 64, + height: 64 + } + ); + assert_eq!( + plan.picture.colour, + ColourDescription { + colour_primaries: 2, + transfer_characteristics: 2, + matrix_coefficients: 2, + video_full_range: false, + }, + "E.3.1 inference: 'unspecified' code points + limited range, never a raw 0" + ); + assert_eq!(plan.picture.general_profile_idc, 1); + assert_eq!(plan.picture.level_idc, Level::L4); + assert_eq!(plan.picture.chroma_format_idc, 1); + assert_eq!(plan.picture.max_dpb_frames, 16, "A-2 for a 64x64 L4 stream"); + // Zero-reorder low-delay: the picture is display-ready in its own plan. + assert_eq!(plan.dpb.outputs, vec![plan.dpb.stored.unwrap()]); + // An IDR carries no RPS. + assert!(plan.rps.st_curr_before.is_empty()); + assert!(plan.rps.lt_curr.is_empty()); + assert_eq!(plan.picture.short_term_ref_pic_set_size_bits, 0); + } + + #[test] + fn a_p_slice_references_the_idr_short_term_and_outputs_immediately() { + let mut planner = H265Planner::new(); + let p0 = planner + .plan_au(&opening_idr_au(&SpsOpts::default())) + .unwrap(); + let idr_id = p0.dpb.stored.unwrap(); + + let p1 = planner.plan_au(&trail_p(1, &[(0, true)], 1)).unwrap(); + assert!(p1.warnings.is_empty(), "{:?}", p1.warnings); + assert_eq!(p1.picture.pic_order_cnt, 1); + assert_eq!( + p1.slices[0].ref_list0, + vec![RefPic { + id: idr_id, + pic_order_cnt: 0, + is_long_term: false + }] + ); + assert_eq!(p1.rps.st_curr_before.len(), 1); + assert_eq!(p1.rps.st_curr_before[0].id, idr_id); + assert!(p1.rps.st_curr_after.is_empty()); + // The slice carried its RPS inline, so the bit count must be nonzero + // (Vulkan's NumBitsForSTRefPicSetInSlice). + assert!(p1.picture.short_term_ref_pic_set_size_bits > 0); + // Zero-reorder: p1 is display-ready immediately. + assert!(p1.dpb.outputs.contains(&p1.dpb.stored.unwrap())); + } + + #[test] + fn long_term_rps_entries_carry_the_rfi_reference_shape() { + let sps = SpsOpts { + long_term: true, + ..Default::default() + }; + let mut planner = H265Planner::new(); + let mut au0 = param_sets(&sps); + au0.extend(idr_slice()); + let p0 = planner.plan_au(&au0).unwrap(); + let idr_id = p0.dpb.stored.unwrap(); + + let p1 = planner + .plan_au(&synth_slice(&SliceOpts { + poc_lsb: 1, + neg: vec![(0, true)], + sps_long_term: true, + num_ref_idx_l0: 1, + ..Default::default() + })) + .unwrap(); + assert!(p1.warnings.is_empty(), "{:?}", p1.warnings); + let p1_id = p1.dpb.stored.unwrap(); + + // The RFI shape: the recovery slice keeps the previous picture short-term + // AND pins the anchor (the IDR, poc 0) through the long-term RPS. + let p2 = planner + .plan_au(&synth_slice(&SliceOpts { + poc_lsb: 2, + neg: vec![(0, true)], + lt: vec![(0, true, None)], + sps_long_term: true, + num_ref_idx_l0: 2, + ..Default::default() + })) + .unwrap(); + assert!(p2.warnings.is_empty(), "{:?}", p2.warnings); + + // 8.3.4: short-term current entries lead the list, long-term follow. + assert_eq!( + p2.slices[0].ref_list0, + vec![ + RefPic { + id: p1_id, + pic_order_cnt: 1, + is_long_term: false + }, + RefPic { + id: idr_id, + pic_order_cnt: 0, + is_long_term: true + }, + ] + ); + assert_eq!(p2.rps.lt_curr.len(), 1); + assert_eq!(p2.rps.lt_curr[0].id, idr_id); + assert!(p2.rps.lt_curr[0].is_long_term); + + // And with delta_poc_msb_present the same anchor resolves by FULL POC + // (8.3.2's other lookup path). + let p3 = planner + .plan_au(&synth_slice(&SliceOpts { + poc_lsb: 3, + neg: vec![(0, true)], + lt: vec![(0, true, Some(0))], + sps_long_term: true, + num_ref_idx_l0: 2, + ..Default::default() + })) + .unwrap(); + assert!(p3.warnings.is_empty(), "{:?}", p3.warnings); + assert_eq!(p3.rps.lt_curr.len(), 1); + assert_eq!(p3.rps.lt_curr[0].id, idr_id); + } + + #[test] + fn the_dpb_snapshot_holds_a_foll_long_term_anchor_the_current_rps_never_names() { + // 8.3.2's *Foll* sets are the whole point of the snapshot: an anchor pinned + // long-term for LATER pictures, marked in the DPB, in none of this picture's + // three current sets. `RefPicSetLtCurr` is empty here — a DXVA `RefPicList` + // built from the current sets alone drops the anchor for exactly the + // pictures between the pin and its use. + let sps = SpsOpts { + long_term: true, + ..Default::default() + }; + let mut planner = H265Planner::new(); + let mut au0 = param_sets(&sps); + au0.extend(idr_slice()); + let p0 = planner.plan_au(&au0).unwrap(); + let idr_id = p0.dpb.stored.unwrap(); + assert!( + p0.dpb_refs.is_empty(), + "an opening IDR has no DPB behind it" + ); + + let p1 = planner + .plan_au(&synth_slice(&SliceOpts { + poc_lsb: 1, + neg: vec![(0, true)], + sps_long_term: true, + num_ref_idx_l0: 1, + ..Default::default() + })) + .unwrap(); + let p1_id = p1.dpb.stored.unwrap(); + assert!(p1.warnings.is_empty(), "{:?}", p1.warnings); + + // used_by_curr_pic_lt_flag = 0: the IDR lands in RefPicSetLtFoll — kept + // marked long-term, referenced by nothing in this picture. + let p2 = planner + .plan_au(&synth_slice(&SliceOpts { + poc_lsb: 2, + neg: vec![(0, true)], + lt: vec![(0, false, None)], + sps_long_term: true, + num_ref_idx_l0: 1, + ..Default::default() + })) + .unwrap(); + assert!(p2.warnings.is_empty(), "{:?}", p2.warnings); + assert!( + p2.rps.lt_curr.is_empty(), + "the anchor is Foll, not Curr, for this picture" + ); + assert_eq!( + p2.slices[0] + .ref_list0 + .iter() + .map(|r| r.id) + .collect::>(), + vec![p1_id] + ); + // …and it is still in the DPB, still marked long-term. + assert_eq!( + p2.dpb_refs + .iter() + .map(|r| (r.id, r.is_long_term)) + .collect::>(), + vec![(idr_id, true), (p1_id, false)] + ); + assert!( + !p2.dpb.removed.contains(&idr_id), + "a Foll entry must not be retired" + ); + } + + #[test] + fn the_dpb_snapshot_is_a_superset_of_the_current_rps_across_the_whole_vector() { + let (_, plans) = plan_whole_clip(TEST_25FPS); + assert_eq!(plans.len(), 250); + for (i, plan) in plans.iter().enumerate() { + let mut ids: Vec = plan.dpb_refs.iter().map(|r| r.id).collect(); + let count = ids.len(); + ids.sort_unstable(); + ids.dedup(); + assert_eq!(ids.len(), count, "AU {i}: the snapshot repeats a picture"); + // Every current-set entry is a marked DPB picture, with the SAME marking + // — the snapshot is the authority a backend keys its arrays by. + for rp in plan + .rps + .st_curr_before + .iter() + .chain(&plan.rps.st_curr_after) + .chain(&plan.rps.lt_curr) + { + let found = plan + .dpb_refs + .iter() + .find(|d| d.id == rp.id) + .unwrap_or_else(|| panic!("AU {i}: RPS entry {} is not marked", rp.id)); + assert_eq!(found.is_long_term, rp.is_long_term); + assert_eq!(found.pic_order_cnt, rp.pic_order_cnt); + } + // The current picture is stored AFTER the snapshot is taken. + assert!(!ids.contains(&plan.dpb.stored.unwrap())); + } + } + + #[test] + fn an_rps_that_drops_a_reference_retires_it_from_the_dpb() { + let mut planner = H265Planner::new(); + let p0 = planner + .plan_au(&opening_idr_au(&SpsOpts::default())) + .unwrap(); + let idr_id = p0.dpb.stored.unwrap(); + let p1 = planner.plan_au(&trail_p(1, &[(0, true)], 1)).unwrap(); + let p1_id = p1.dpb.stored.unwrap(); + + // p2's RPS names only poc 1: the IDR leaves every set, is marked unused and + // — already output — must be reported removed. + let p2 = planner.plan_au(&trail_p(2, &[(0, true)], 1)).unwrap(); + assert!(p2.warnings.is_empty(), "{:?}", p2.warnings); + assert_eq!(p2.slices[0].ref_list0[0].id, p1_id); + assert!(p2.dpb.removed.contains(&idr_id)); + assert!(!p2.dpb.removed.contains(&p1_id)); + } + + #[test] + fn a_missing_reference_is_substituted_in_place_not_compacted() { + let mut planner = H265Planner::new(); + let p0 = planner + .plan_au(&opening_idr_au(&SpsOpts::default())) + .unwrap(); + let idr_id = p0.dpb.stored.unwrap(); + let p1 = planner.plan_au(&trail_p(1, &[(0, true)], 1)).unwrap(); + let p1_id = p1.dpb.stored.unwrap(); + + // The picture at poc 2 was lost on the wire. p3's RPS still names poc 2, 1 + // and 0; the list0 head (poc 2) is unresolvable and must be substituted in + // place — the two real entries keep their ref_idx positions. + let p3 = planner + .plan_au(&trail_p(3, &[(0, true), (0, true), (0, true)], 3)) + .unwrap(); + assert!( + p3.warnings + .iter() + .any(|w| matches!(w, PlanWarning::MissingReference { .. })), + "{:?}", + p3.warnings + ); + let ids: Vec = p3.slices[0].ref_list0.iter().map(|r| r.id).collect(); + assert_eq!( + ids, + vec![p1_id, p1_id, idr_id], + "substitution must preserve list length and positions" + ); + // The plan's RPS omits the unresolvable entry rather than fabricating one. + assert_eq!(p3.rps.st_curr_before.len(), 2); + } + + #[test] + fn a_rasl_behind_a_join_cra_is_skipped_and_the_trailing_picture_plans() { + // A CRA opening the stream (an open-GOP join): NoRaslOutputFlag = 1. + let mut au0 = param_sets(&SpsOpts::default()); + au0.extend(synth_slice(&SliceOpts { + nalu_type: CRA_NUT, + slice_type: 2, + poc_lsb: 0, + ..Default::default() + })); + let mut planner = H265Planner::new(); + let p0 = planner.plan_au(&au0).unwrap(); + assert!(p0.picture.is_irap && !p0.picture.is_idr); + assert!(p0.picture.no_rasl_output_flag); + assert_eq!(p0.picture.pic_order_cnt, 0); + let cra_id = p0.dpb.stored.unwrap(); + + // Its RASL leading picture (poc -1, referencing a pre-join picture) must be + // refused without wedging the planner. + let rasl = synth_slice(&SliceOpts { + nalu_type: RASL_N, + poc_lsb: 15, + neg: vec![(0, true)], + ..Default::default() + }); + assert!(matches!( + planner.plan_au(&rasl), + Err(PlanError::RaslSkipped { poc: -1 }) + )); + + // The trailing picture referencing the CRA plans clean. + let p1 = planner.plan_au(&trail_p(1, &[(0, true)], 1)).unwrap(); + assert!(p1.warnings.is_empty(), "{:?}", p1.warnings); + assert_eq!(p1.slices[0].ref_list0[0].id, cra_id); + } + + #[test] + fn a_rasl_behind_a_mid_stream_cra_plans_normally() { + let mut planner = H265Planner::new(); + let p0 = planner + .plan_au(&opening_idr_au(&SpsOpts::default())) + .unwrap(); + let _idr_id = p0.dpb.stored.unwrap(); + let p1 = planner.plan_au(&trail_p(1, &[(0, true)], 1)).unwrap(); + let p1_id = p1.dpb.stored.unwrap(); + + // A CRA reached by continuous decoding keeps NoRaslOutputFlag = 0; its RPS + // may keep pre-CRA pictures around for its RASLs (used = false → StFoll). + let mut cra = synth_slice(&SliceOpts { + nalu_type: CRA_NUT, + slice_type: 2, + poc_lsb: 4, + neg: vec![(2, false)], + ..Default::default() + }); + // In-band parameter re-send at the IRAP, as hosts do. + let mut au = param_sets(&SpsOpts::default()); + au.append(&mut cra); + let p2 = planner.plan_au(&au).unwrap(); + assert!(p2.warnings.is_empty(), "{:?}", p2.warnings); + assert!(!p2.picture.no_rasl_output_flag); + let cra_id = p2.dpb.stored.unwrap(); + + // The RASL at poc 2 references both sides of the CRA — decodable here. + let p3 = planner + .plan_au(&synth_slice(&SliceOpts { + nalu_type: RASL_N, + poc_lsb: 2, + neg: vec![(0, true)], + pos: vec![(1, true)], + num_ref_idx_l0: 2, + ..Default::default() + })) + .unwrap(); + assert!(p3.warnings.is_empty(), "{:?}", p3.warnings); + assert_eq!(p3.picture.pic_order_cnt, 2); + assert_eq!( + p3.slices[0] + .ref_list0 + .iter() + .map(|r| r.id) + .collect::>(), + vec![p1_id, cra_id], + "list0: the past (StCurrBefore) then the future (StCurrAfter)" + ); + assert!( + !p3.picture.is_reference, + "RASL_N is a sub-layer non-reference type" + ); + } + + #[test] + fn a_recovery_point_sei_lands_on_the_picture_plan_and_does_not_stick() { + let mut au0 = param_sets(&SpsOpts::default()); + // Prefix SEI (type 39): recovery point, recovery_poc_cnt = 0, exact = 0, + // broken = 0 (payload bits: se(0) '1', two flag zeros, alignment). + au0.extend(h265_nalu(39, &[0x06, 0x01, 0x90, 0x80])); + au0.extend(idr_slice()); + + let mut planner = H265Planner::new(); + let plan = planner.plan_au(&au0).unwrap(); + assert_eq!( + plan.picture.recovery_point, + Some(RecoveryPointHevc { + recovery_poc_cnt: 0, + exact_match: false, + broken_link: false + }) + ); + + // The following AU carries no SEI: the field must not stick. + let plan = planner.plan_au(&trail_p(1, &[(0, true)], 1)).unwrap(); + assert_eq!(plan.picture.recovery_point, None); + } + + #[test] + fn the_conformance_window_scales_by_the_chroma_format() { + // 4:2:0: SubWidthC = SubHeightC = 2. + let plan = H265Planner::new() + .plan_au(&opening_idr_au(&SpsOpts { + conf_win: Some((2, 1, 1, 2)), + ..Default::default() + })) + .unwrap(); + assert_eq!( + plan.picture.display_crop, + DisplayCrop { + x: 4, + y: 2, + width: 58, + height: 58 + } + ); + + // 4:4:4 (chroma_format_idc 3, RExt profile): offsets are luma samples. + let plan = H265Planner::new() + .plan_au(&opening_idr_au(&SpsOpts { + profile_idc: 4, + chroma_format_idc: 3, + conf_win: Some((2, 1, 1, 2)), + ..Default::default() + })) + .unwrap(); + assert_eq!(plan.picture.chroma_format_idc, 3); + assert_eq!( + plan.picture.display_crop, + DisplayCrop { + x: 2, + y: 1, + width: 61, + height: 61 + } + ); + + // 4:2:2: SubWidthC = 2, SubHeightC = 1. + let plan = H265Planner::new() + .plan_au(&opening_idr_au(&SpsOpts { + profile_idc: 4, + chroma_format_idc: 2, + conf_win: Some((1, 1, 1, 1)), + ..Default::default() + })) + .unwrap(); + assert_eq!( + plan.picture.display_crop, + DisplayCrop { + x: 2, + y: 1, + width: 60, + height: 62 + } + ); + } + + #[test] + fn main10_depths_ride_the_plan() { + let plan = H265Planner::new() + .plan_au(&opening_idr_au(&SpsOpts { + profile_idc: 2, + bit_depth_minus8: 2, + ..Default::default() + })) + .unwrap(); + assert_eq!(plan.picture.general_profile_idc, 2); + assert_eq!(plan.picture.bit_depth_luma_minus8, 2); + assert_eq!(plan.picture.bit_depth_chroma_minus8, 2); + assert_eq!(plan.picture.chroma_format_idc, 1); + } + + #[test] + fn explicit_vui_colour_rides_the_plan_and_the_range_flag_stands_alone() { + // BT.2020/PQ HDR signalling — the in-band switch the Windows host emits. + let plan = H265Planner::new() + .plan_au(&opening_idr_au(&SpsOpts { + vui: VuiOpt::SignalType { + full_range: false, + colour: Some((9, 16, 9)), + }, + ..Default::default() + })) + .unwrap(); + assert_eq!( + plan.picture.colour, + ColourDescription { + colour_primaries: 9, + transfer_characteristics: 16, + matrix_coefficients: 9, + video_full_range: false, + } + ); + + // video_signal_type present, full-range set, but NO colour description: the + // code points stay E.3.1's "unspecified" while the range flag rides. + let plan = H265Planner::new() + .plan_au(&opening_idr_au(&SpsOpts { + vui: VuiOpt::SignalType { + full_range: true, + colour: None, + }, + ..Default::default() + })) + .unwrap(); + assert_eq!( + plan.picture.colour, + ColourDescription { + colour_primaries: 2, + transfer_characteristics: 2, + matrix_coefficients: 2, + video_full_range: true, + } + ); + } + + #[test] + fn a_field_coded_stream_is_rejected_as_outside_the_envelope() { + let err = H265Planner::new() + .plan_au(&synth_sps(&SpsOpts { + vui: VuiOpt::FieldSeq, + ..Default::default() + })) + .unwrap_err(); + assert!( + matches!(err, PlanError::OutsideEnvelope(what) if what.contains("field")), + "{err:?}" + ); + } + + #[test] + fn a_dpb_deeper_than_16_frames_is_rejected_as_outside_the_envelope() { + // The vendored parser reads sps_max_dec_pic_buffering_minus1 up to 16 — a + // 17-frame DPB no hardware implements. Gated at SPS activation. + let err = H265Planner::new() + .plan_au(&synth_sps(&SpsOpts { + max_dec_pic_buffering_minus1: 16, + ..Default::default() + })) + .unwrap_err(); + assert!( + matches!(err, PlanError::OutsideEnvelope(what) if what.contains("DPB")), + "{err:?}" + ); + } + + #[test] + fn a_multilayer_nalu_is_rejected_as_outside_the_envelope() { + let mut au = param_sets(&SpsOpts::default()); + au.extend(synth_slice(&SliceOpts { + nalu_type: IDR_W_RADL, + slice_type: 2, + layer_id: 1, + ..Default::default() + })); + let err = H265Planner::new().plan_au(&au).unwrap_err(); + assert!( + matches!(err, PlanError::OutsideEnvelope(what) if what.contains("nuh_layer_id")), + "{err:?}" + ); + } + + #[test] + fn flush_resets_decoding_state_and_refuses_non_irap_until_one_arrives() { + let mut planner = H265Planner::new(); + let id0 = planner + .plan_au(&opening_idr_au(&SpsOpts::default())) + .unwrap() + .dpb + .stored + .unwrap(); + let id1 = planner + .plan_au(&trail_p(1, &[(0, true)], 1)) + .unwrap() + .dpb + .stored + .unwrap(); + + let flushed = planner.flush(); + assert!(flushed.outputs.is_empty(), "both pictures already output"); + assert_eq!(flushed.removed, vec![id0, id1]); + + // A non-IRAP AU is refused until the next IRAP. + assert!(matches!( + planner.plan_au(&trail_p(2, &[(0, true)], 1)), + Err(PlanError::AwaitingIdr) + )); + + // A CRA restarts planning — the flush gave it NoRaslOutputFlag = 1, making + // it as good a re-entry point as an IDR. Parameter sets survived (7.4.2.4). + let plan = planner + .plan_au(&synth_slice(&SliceOpts { + nalu_type: CRA_NUT, + slice_type: 2, + poc_lsb: 0, + ..Default::default() + })) + .unwrap(); + assert!(plan.picture.is_irap); + assert!(plan.picture.no_rasl_output_flag); + assert_eq!(plan.picture.pic_order_cnt, 0); + assert!(plan.warnings.is_empty(), "{:?}", plan.warnings); + + // And the stream continues cleanly on the reset state. + let plan = planner.plan_au(&trail_p(1, &[(0, true)], 1)).unwrap(); + assert!(plan.warnings.is_empty(), "{:?}", plan.warnings); + assert_eq!(plan.slices[0].ref_list0.len(), 1); + } + + #[test] + fn a_foreign_slice_in_the_au_is_dropped_with_a_truncated_au_warning() { + let mut au = param_sets(&SpsOpts::default()); + au.extend(idr_slice()); + // A mis-split AU: a continuation segment belonging to ANOTHER picture (a + // TRAIL slice after an IDR — 7.4.2.4.4 requires one NALU type per picture). + au.extend(synth_slice(&SliceOpts { + segment: Some((0, 0, false)), + poc_lsb: 1, + neg: vec![(0, true)], + ..Default::default() + })); + + let plan = H265Planner::new().plan_au(&au).unwrap(); + assert!(plan.picture.is_idr); + assert_eq!(plan.slices.len(), 1, "the foreign slice is not planned"); + assert!(plan + .warnings + .iter() + .any(|w| matches!(w, PlanWarning::TruncatedAu { .. }))); + } + + #[test] + fn a_truncated_continuation_slice_degrades_to_a_warning() { + let mut au = param_sets(&SpsOpts::default()); + au.extend(idr_slice()); + // A second IDR segment, cut mid-header: its parse fails, the planned slice + // before the cut survives. + let mut cont = synth_slice(&SliceOpts { + nalu_type: IDR_W_RADL, + slice_type: 2, + segment: Some((0, 0, false)), + ..Default::default() + }); + cont.truncate(cont.len() - 1); + // Drop the alignment content so the header read runs out of bits. + let cut_at = au.len(); + au.extend(&cont[..7.min(cont.len())]); + + let plan = H265Planner::new().plan_au(&au).unwrap(); + assert_eq!(plan.slices.len(), 1); + assert!( + plan.warnings + .iter() + .any(|w| matches!(w, PlanWarning::TruncatedAu { offset } if *offset == cut_at)), + "{:?}", + plan.warnings + ); + } + + #[test] + fn two_pictures_in_one_au_is_outside_the_envelope() { + let mut au = param_sets(&SpsOpts::default()); + au.extend(idr_slice()); + au.extend(idr_slice()); + let err = H265Planner::new().plan_au(&au).unwrap_err(); + assert!( + matches!(err, PlanError::OutsideEnvelope(what) if what.contains("one access unit")), + "{err:?}" + ); + } + + #[test] + fn dependent_slice_segments_plan_with_the_completed_header() { + // 128x64 with a 64-sample CTB: two CTBs, so segment addresses exist (1 bit). + let sps = SpsOpts { + width: 128, + ..Default::default() + }; + let mut au = synth_sps(&sps); + au.extend(synth_pps(true)); + au.extend(idr_slice()); + au.extend(synth_slice(&SliceOpts { + nalu_type: IDR_W_RADL, + slice_type: 2, + segment: Some((1, 1, true)), + pps_dependent_enabled: true, + ..Default::default() + })); + + let plan = H265Planner::new().plan_au(&au).unwrap(); + assert!(plan.warnings.is_empty(), "{:?}", plan.warnings); + assert_eq!(plan.slices.len(), 2); + let dependent = &plan.slices[1].header; + assert!(dependent.dependent_slice_segment_flag); + assert_eq!(dependent.segment_address, 1); + assert!( + dependent.type_.is_i(), + "the dependent header inherited the independent slice's type" + ); + } + + #[test] + fn reordering_streams_plan_with_the_envelope_fact_flagged() { + let sps = SpsOpts { + max_num_reorder_pics: 2, + ..Default::default() + }; + let mut planner = H265Planner::new(); + let p0 = planner.plan_au(&opening_idr_au(&sps)).unwrap(); + assert!( + p0.warnings.contains(&PlanWarning::NonZeroReorder { + max_num_reorder_pics: 2 + }), + "{:?}", + p0.warnings + ); + // With reorder depth 2 nothing is display-ready yet. + assert!(p0.dpb.outputs.is_empty()); + let p1 = planner.plan_au(&trail_p(1, &[(0, true)], 1)).unwrap(); + assert!(p1.dpb.outputs.is_empty()); + + // The flush releases everything, in POC order. + let flushed = planner.flush(); + assert_eq!( + flushed.outputs, + vec![p0.dpb.stored.unwrap(), p1.dpb.stored.unwrap()] + ); + } + + #[test] + fn poc_msb_wraps_across_the_lsb_boundary() { + // 4-bit POC lsb (MaxPicOrderCntLsb 16): 0 → 7 → 14 → 2, the last step + // crossing the lsb boundary, must decode as POC 18 (8.3.1 msb increment). + let mut planner = H265Planner::new(); + planner + .plan_au(&opening_idr_au(&SpsOpts::default())) + .unwrap(); + let p1 = planner.plan_au(&trail_p(7, &[(6, true)], 1)).unwrap(); + assert_eq!(p1.picture.pic_order_cnt, 7); + let p2 = planner.plan_au(&trail_p(14, &[(6, true)], 1)).unwrap(); + assert_eq!(p2.picture.pic_order_cnt, 14); + let p3 = planner.plan_au(&trail_p(2, &[(3, true)], 1)).unwrap(); + assert!(p3.warnings.is_empty(), "{:?}", p3.warnings); + assert_eq!(p3.picture.pic_order_cnt, 18); + assert_eq!(p3.slices[0].ref_list0[0].pic_order_cnt, 14); + } + + #[test] + fn the_plans_parameter_set_accessors_carry_the_activated_content() { + let plan = H265Planner::new() + .plan_au(&opening_idr_au(&SpsOpts::default())) + .unwrap(); + assert_eq!(plan.sps.seq_parameter_set_id, 0); + assert_eq!(plan.sps.width(), 64); + assert_eq!(plan.sps.height(), 64); + assert_eq!(plan.pps.pic_parameter_set_id, 0); + assert_eq!(plan.pps.seq_parameter_set_id, 0); + assert!( + Rc::ptr_eq(&plan.sps, &plan.pps.sps), + "the SPS accessor is the PPS's own SPS, not a second copy" + ); + } + + #[test] + fn outputs_queued_during_a_failed_au_surface_in_the_next_successful_plan() { + let sps = SpsOpts { + max_num_reorder_pics: 1, + ..Default::default() + }; + let mut planner = H265Planner::new(); + let p0 = planner.plan_au(&opening_idr_au(&sps)).unwrap(); + let id0 = p0.dpb.stored.unwrap(); + assert!(p0.dpb.outputs.is_empty(), "held back by the reorder depth"); + let p1 = planner.plan_au(&trail_p(1, &[(0, true)], 1)).unwrap(); + let id1 = p1.dpb.stored.unwrap(); + assert_eq!(p1.dpb.outputs, vec![id0]); + + // This AU errors AFTER its mid-stream IDR begin drained the DPB (queueing + // p1 for output): a second first-segment makes it a two-picture AU. + let mut bad_au = idr_slice(); + bad_au.extend(idr_slice()); + assert!(matches!( + planner.plan_au(&bad_au), + Err(PlanError::OutsideEnvelope(_)) + )); + + // The queued output and the eviction must surface here, not vanish. + let plan = planner.plan_au(&idr_slice()).unwrap(); + assert!(plan.dpb.outputs.contains(&id1)); + assert!(plan.dpb.removed.contains(&id1)); + } + + #[test] + fn a_dropped_reference_au_degrades_to_warnings_and_planning_continues() { + let aus = split_into_aus(TEST_25FPS); + + // Pass 1: find a droppable AU — a non-IRAP reference picture not followed + // by an IRAP (an IRAP right after would reset the state and hide the loss). + let mut planner = H265Planner::new(); + let mut plans = Vec::new(); + for au in &aus { + plans.push(planner.plan_au(au).unwrap()); + } + let dropped = plans + .iter() + .enumerate() + .position(|(i, p)| { + p.picture.is_reference + && !p.picture.is_irap + && plans.get(i + 1).is_some_and(|next| !next.picture.is_irap) + }) + .expect("the vector contains a droppable reference picture"); + + // Pass 2: the same stream minus that AU must warn, not error — and every + // ref list entry it emits must still resolve to a picture the backend was + // told to store (substitution never leaks a hole). + let mut planner = H265Planner::new(); + let mut missing_seen = false; + let mut planned = 0usize; + let mut stored_so_far: BTreeSet = BTreeSet::new(); + for (i, au) in aus.iter().enumerate() { + if i == dropped { + continue; + } + let plan = planner + .plan_au(au) + .expect("a lost reference AU must degrade to warnings, not errors"); + planned += 1; + missing_seen |= plan + .warnings + .iter() + .any(|w| matches!(w, PlanWarning::MissingReference { .. })); + stored_so_far.insert(plan.dpb.stored.unwrap()); + for slice in &plan.slices { + for entry in slice.ref_list0.iter().chain(&slice.ref_list1) { + assert!( + stored_so_far.contains(&entry.id), + "every emitted reference must be a real stored PicId" + ); + } + } + for entry in plan + .rps + .st_curr_before + .iter() + .chain(&plan.rps.st_curr_after) + .chain(&plan.rps.lt_curr) + { + assert!(stored_so_far.contains(&entry.id)); + } + } + + assert_eq!(planned, aus.len() - 1); + assert!( + missing_seen, + "an AU after the drop must report the missing reference" + ); + } + + #[test] + fn a_first_slice_naming_an_unseen_pps_is_a_no_active_param_set_error() { + let mut planner = H265Planner::new(); + planner + .plan_au(&opening_idr_au(&SpsOpts::default())) + .unwrap(); + // A first slice segment referencing PPS 1, which was never sent: the AU has + // no picture to conceal around, so this is an error, not a warning. + let mut s = BitSink::new(); + s.bit(1); // first_slice_segment_in_pic_flag + s.ue(1); // slice_pic_parameter_set_id — never seen + let au = h265_nalu(TRAIL_R, &s.finish()); + assert!(matches!( + planner.plan_au(&au), + Err(PlanError::NoActiveParamSet { pps_id: 1 }) + )); + } + + // ------- review-round regressions (findings 1-9) ------- + + /// Finding 1: the vendored parser indexed its 16-deep long-term arrays with a + /// count it read up to 32 — a hostile header was a production panic. Now a parse + /// error (vendor deviation 7). + #[test] + fn a_hostile_long_term_count_is_a_parse_error_not_a_panic() { + let sps = SpsOpts { + long_term: true, + ..Default::default() + }; + let mut planner = H265Planner::new(); + let mut au0 = param_sets(&sps); + au0.extend(idr_slice()); + planner.plan_au(&au0).unwrap(); + + let hostile = synth_slice(&SliceOpts { + poc_lsb: 1, + neg: vec![(0, true)], + lt: vec![(0, true, None); 17], // num_long_term_pics = 17 > the 16 slots + sps_long_term: true, + ..Default::default() + }); + assert!(matches!( + planner.plan_au(&hostile), + Err(PlanError::Parse(_)) + )); + // And the planner survives to plan the next clean AU. + let plan = planner.plan_au(&trail_p(1, &[(0, true)], 1)).unwrap(); + assert!(plan.warnings.is_empty(), "{:?}", plan.warnings); + } + + fn eos_nalu() -> Vec { + h265_nalu(36, &[]) + } + + /// Finding 2: C.5.2.2 exempts only picture 0 of the BITSTREAM — an IRAP behind an + /// in-band EOS must drain the previous sequence's outputs before the new one + /// starts, and must honour no_output_of_prior_pics_flag there. + #[test] + fn an_eos_then_idr_drains_the_previous_sequence_before_the_new_one() { + let sps = SpsOpts { + max_num_reorder_pics: 2, + ..Default::default() + }; + let mut planner = H265Planner::new(); + let p0 = planner.plan_au(&opening_idr_au(&sps)).unwrap(); + let id0 = p0.dpb.stored.unwrap(); + let p1 = planner.plan_au(&trail_p(1, &[(0, true)], 1)).unwrap(); + let id1 = p1.dpb.stored.unwrap(); + assert!(p1.dpb.outputs.is_empty(), "held back by the reorder depth"); + let p2 = planner + .plan_au(&trail_p(2, &[(0, true), (0, true)], 1)) + .unwrap(); + let id2 = p2.dpb.stored.unwrap(); + assert_eq!(p2.dpb.outputs, vec![id0], "depth 2 releases poc 0 here"); + + // EOS + IDR in one AU: pocs 1 and 2 must ALL come out here, in POC order, + // before the new sequence emits anything — never interleaved with it. + let mut au = eos_nalu(); + au.extend(idr_slice()); + let p3 = planner.plan_au(&au).unwrap(); + assert!(p3.picture.no_rasl_output_flag, "EOS gave the IDR the flag"); + assert_eq!(p3.dpb.outputs, vec![id1, id2]); + for id in [id0, id1, id2] { + assert!(p3.dpb.removed.contains(&id)); + } + + // Same join with no_output_of_prior_pics_flag = 1: the leftovers are + // DISCARDED — removed without ever being output (C.3.2). + let mut planner = H265Planner::new(); + planner.plan_au(&opening_idr_au(&sps)).unwrap(); + let q1 = planner.plan_au(&trail_p(1, &[(0, true)], 1)).unwrap(); + let q1_id = q1.dpb.stored.unwrap(); + let mut au = eos_nalu(); + au.extend(synth_slice(&SliceOpts { + nalu_type: IDR_W_RADL, + slice_type: 2, + no_output_of_prior_pics: true, + ..Default::default() + })); + let q2 = planner.plan_au(&au).unwrap(); + assert!( + !q2.dpb.outputs.contains(&q1_id), + "no_output_of_prior_pics discards without output" + ); + assert!(q2.dpb.removed.contains(&q1_id)); + } + + /// Finding 3: parse_sps stores an SPS even when its AU is rejected, and + /// NegotiationInfo deliberately omits envelope-only facts — so a later PPS-only + /// rebind must re-run the envelope gate at activation. Both bypass legs. + #[test] + fn a_rejected_sps_cannot_be_activated_through_a_pps_only_rebind() { + // Leg A: hostile conformance window on otherwise-identical geometry. Without + // the activation-time gate this reaches visible_rectangle()'s unchecked u32 + // subtraction (panic in debug, silent wrap in release). + let mut planner = H265Planner::new(); + planner + .plan_au(&opening_idr_au(&SpsOpts::default())) + .unwrap(); + let hostile = synth_sps(&SpsOpts { + conf_win: Some((100, 0, 0, 0)), // 200 luma samples of a 64-wide picture + ..Default::default() + }); + assert!(matches!( + planner.plan_au(&hostile), + Err(PlanError::Parse(_)) + )); + let mut rebind = synth_pps(false); + rebind.extend(idr_slice()); + match planner.plan_au(&rebind) { + Err(PlanError::Parse(msg)) => assert!(msg.contains("conformance window"), "{msg}"), + other => panic!("the rebind must not activate the rejected SPS: {other:?}"), + } + + // Leg B: a 17-frame DPB, same geometry (dpb_limit caps both sides at 16, so + // NegotiationInfo alone cannot catch the rebind). + let mut planner = H265Planner::new(); + planner + .plan_au(&opening_idr_au(&SpsOpts::default())) + .unwrap(); + let hostile = synth_sps(&SpsOpts { + max_dec_pic_buffering_minus1: 16, + ..Default::default() + }); + assert!(matches!( + planner.plan_au(&hostile), + Err(PlanError::OutsideEnvelope(_)) + )); + let mut rebind = synth_pps(false); + rebind.extend(idr_slice()); + assert!(matches!( + planner.plan_au(&rebind), + Err(PlanError::OutsideEnvelope(what)) if what.contains("DPB") + )); + } + + /// Finding 4: the RASL refusal must run BEFORE renegotiation — a RASL AU carrying + /// a renegotiating SPS must not drain the DPB on its way out. + #[test] + fn a_skipped_rasl_carrying_a_renegotiating_sps_leaves_the_dpb_intact() { + // A CRA join: RASLs behind it are skipped. + let mut au0 = param_sets(&SpsOpts::default()); + au0.extend(synth_slice(&SliceOpts { + nalu_type: CRA_NUT, + slice_type: 2, + ..Default::default() + })); + let mut planner = H265Planner::new(); + let cra_id = planner.plan_au(&au0).unwrap().dpb.stored.unwrap(); + + // The RASL AU re-sends parameter sets with NEW geometry (128x64) — a + // renegotiation trigger — and must still be refused state-free. + let mut rasl_au = param_sets(&SpsOpts { + width: 128, + ..Default::default() + }); + rasl_au.extend(synth_slice(&SliceOpts { + nalu_type: RASL_N, + poc_lsb: 15, + neg: vec![(0, true)], + ..Default::default() + })); + assert!(matches!( + planner.plan_au(&rasl_au), + Err(PlanError::RaslSkipped { .. }) + )); + + // With the original parameter sets re-sent, the trailing picture still finds + // the CRA in the DPB: nothing was drained or renegotiated by the skip. + let mut au = param_sets(&SpsOpts::default()); + au.extend(trail_p(1, &[(0, true)], 1)); + let plan = planner.plan_au(&au).unwrap(); + assert!(plan.warnings.is_empty(), "{:?}", plan.warnings); + assert_eq!(plan.slices[0].ref_list0[0].id, cra_id); + } + + /// Finding 5: an AU that OPENS with a continuation segment is the mis-split tail + /// of a previous picture; beginning a picture from it would fabricate a duplicate + /// (a dependent one would even wear a previous AU's independent header). + #[test] + fn a_leading_continuation_segment_is_skipped_not_fabricated_into_a_picture() { + let sps = SpsOpts { + width: 128, // two CTBs, so continuation addresses exist + ..Default::default() + }; + let mut au0 = synth_sps(&sps); + au0.extend(synth_pps(true)); + au0.extend(idr_slice()); + let mut planner = H265Planner::new(); + planner.plan_au(&au0).unwrap(); + + // A dependent segment leads the AU; the real picture follows. + let mut au = synth_slice(&SliceOpts { + nalu_type: IDR_W_RADL, + slice_type: 2, + segment: Some((1, 1, true)), + pps_dependent_enabled: true, + ..Default::default() + }); + au.extend(trail_p(1, &[(0, true)], 1)); + let plan = planner.plan_au(&au).unwrap(); + assert_eq!(plan.slices.len(), 1, "only the real picture is planned"); + assert_eq!(plan.picture.pic_order_cnt, 1); + assert!(plan + .warnings + .iter() + .any(|w| matches!(w, PlanWarning::TruncatedAu { .. }))); + + // Same for a leading INDEPENDENT continuation segment. + let mut au = synth_slice(&SliceOpts { + segment: Some((1, 1, false)), + pps_dependent_enabled: true, + poc_lsb: 1, + neg: vec![(0, true)], + ..Default::default() + }); + au.extend(trail_p(2, &[(0, true)], 1)); + let plan = planner.plan_au(&au).unwrap(); + assert_eq!(plan.slices.len(), 1); + assert_eq!(plan.picture.pic_order_cnt, 2); + assert!(plan + .warnings + .iter() + .any(|w| matches!(w, PlanWarning::TruncatedAu { .. }))); + } + + /// Finding 6: the old truncation detector keyed on the cursor, which never has a + /// start code behind it — cut-off data at the AU tail went unreported. + #[test] + fn cut_off_data_at_the_au_tail_warns_while_zero_padding_does_not() { + let mut au = opening_idr_au(&SpsOpts::default()); + let cut_at = au.len(); + au.extend([0x00, 0x00, 0x01, 0x02]); // a start code + half a NAL header + let plan = H265Planner::new().plan_au(&au).unwrap(); + assert!( + plan.warnings + .iter() + .any(|w| matches!(w, PlanWarning::TruncatedAu { offset } if *offset == cut_at)), + "{:?}", + plan.warnings + ); + + // trailing_zero_8bits padding (B.2.2) is legal and stays silent. + let mut au = opening_idr_au(&SpsOpts::default()); + au.extend([0x00, 0x00, 0x00, 0x00]); + let plan = H265Planner::new().plan_au(&au).unwrap(); + assert!(plan.warnings.is_empty(), "{:?}", plan.warnings); + } + + /// Finding 7: a mid-stream SPS that raises the reorder depth on unchanged + /// geometry must still renegotiate and flag the envelope fact. + #[test] + fn a_mid_stream_reorder_increase_flags_the_envelope_fact() { + let mut planner = H265Planner::new(); + let p0 = planner + .plan_au(&opening_idr_au(&SpsOpts::default())) + .unwrap(); + assert!(p0.warnings.is_empty(), "{:?}", p0.warnings); + planner.plan_au(&trail_p(1, &[(0, true)], 1)).unwrap(); + + let mut au = param_sets(&SpsOpts { + max_num_reorder_pics: 3, + ..Default::default() + }); + au.extend(idr_slice()); + let plan = planner.plan_au(&au).unwrap(); + assert!( + plan.warnings.contains(&PlanWarning::NonZeroReorder { + max_num_reorder_pics: 3 + }), + "{:?}", + plan.warnings + ); + } + + /// Finding 8: two references sharing a poc_lsb make the MSB-less long-term + /// lookup ambiguous (a 7.4.7.1 violation) — the RFI anchor path must say so + /// rather than silently picking one. + #[test] + fn an_ambiguous_long_term_poc_lsb_warns_instead_of_silently_picking() { + let sps = SpsOpts { + long_term: true, + ..Default::default() + }; + let mut planner = H265Planner::new(); + let mut au0 = param_sets(&sps); + au0.extend(idr_slice()); + planner.plan_au(&au0).unwrap(); // poc 0 + + let lt_slice = |poc_lsb: u32, neg: Vec<(u32, bool)>, lt| { + synth_slice(&SliceOpts { + poc_lsb, + neg, + lt, + sps_long_term: true, + ..Default::default() + }) + }; + // Build to POC 16 while keeping POC 0 referenced: 0, 7, 14, 16 — the msb + // wrap makes 0 and 16 share poc_lsb 0. + planner + .plan_au(<_slice(7, vec![(6, true)], vec![])) + .unwrap(); + planner + .plan_au(<_slice(14, vec![(6, true), (6, true)], vec![])) + .unwrap(); + let p3 = planner + .plan_au(<_slice(0, vec![(1, true), (13, true)], vec![])) + .unwrap(); + assert_eq!(p3.picture.pic_order_cnt, 16, "msb wrap"); + + // POC 0 and POC 16 are both referenced and share poc_lsb 0: an MSB-less + // long-term entry naming lsb 0 is ambiguous. + let p4 = planner + .plan_au(<_slice(1, vec![(0, true)], vec![(0, true, None)])) + .unwrap(); + assert!( + p4.warnings.iter().any(|w| matches!( + w, + PlanWarning::MissingReference { context, .. } if context.contains("ambiguous") + )), + "{:?}", + p4.warnings + ); + assert_eq!(p4.rps.lt_curr.len(), 1, "still resolved (concealment)"); + } + + /// Finding 9: an IRAP AU that fails before its picture begins must not unlatch + /// the AwaitingIdr gate. + #[test] + fn a_failed_resume_irap_keeps_the_awaiting_gate_latched() { + let mut planner = H265Planner::new(); + planner + .plan_au(&opening_idr_au(&SpsOpts::default())) + .unwrap(); + planner.flush(); + + // A CRA naming an unseen PPS: the resume attempt fails before begin_picture. + let mut s = BitSink::new(); + s.bit(1); // first_slice_segment_in_pic_flag + s.bit(0); // no_output_of_prior_pics_flag + s.ue(1); // slice_pic_parameter_set_id — never seen + let bad_cra = h265_nalu(CRA_NUT, &s.finish()); + assert!(matches!( + planner.plan_au(&bad_cra), + Err(PlanError::NoActiveParamSet { pps_id: 1 }) + )); + + // The gate must still hold against non-IRAP pictures... + assert!(matches!( + planner.plan_au(&trail_p(1, &[(0, true)], 1)), + Err(PlanError::AwaitingIdr) + )); + + // ...and a valid IRAP still resumes (parameter sets survived the flush). + let plan = planner.plan_au(&idr_slice()).unwrap(); + assert!(plan.picture.is_idr); + assert!(plan.warnings.is_empty(), "{:?}", plan.warnings); + } +} diff --git a/crates/pf-bitstream/src/lib.rs b/crates/pf-bitstream/src/lib.rs new file mode 100644 index 00000000..ee6936c3 --- /dev/null +++ b/crates/pf-bitstream/src/lib.rs @@ -0,0 +1,157 @@ +//! The client's bitstream layer for native decode (design/client-native-decode.md §3.1): +//! everything a stateless hardware decoder needs to know about an AU before submission — +//! parsed headers, POC, DPB state, reference lists (including MMCO/LTR, which the hosts' +//! RFI recovery actively uses), recovery-point SEI — derived once here and consumed by +//! every backend (Vulkan `StdVideo*`, DXVA picparams, libva buffers). +//! +//! Parsing primitives come from the vendored cros-codecs parser layer +//! (`vendor/cros-codecs`, see its PROVENANCE.md); this crate owns what upstream keeps in +//! its Linux-only `decoder::stateless` half — the per-AU orchestration — plus the pieces +//! upstream lacks (SEI payload parsing: their parsers classify SEI NALUs but never read +//! them). +//! +//! Scope discipline: punktfunk clients decode punktfunk hosts — zero-reorder, no +//! B-frames, progressive, parameter sets from encoders we control. Implement to spec +//! where cheap; reject-with-log outside that envelope rather than half-decode. +//! +//! Nothing in this crate may touch a GPU API, an OS handle, or the network: CPU-only by +//! construction, so its tests run on every CI leg including macOS. And no `unsafe`, +//! compiler-enforced — this layer exists to replace C parsers; it does not get to +//! reintroduce their failure mode. +#![forbid(unsafe_code)] + +pub mod av1; +pub mod h264; +pub mod h265; +pub mod sei; + +// The vendor-pinning smoke tests below assert against byte counts and golden values from +// the vendored snapshot's own test vectors; a cros-codecs re-sync that shifts parser +// behavior must trip HERE, in our tree, not in a decode session. +#[cfg(test)] +mod vendor_smoke { + use std::io::Cursor; + + use cros_codecs::bitstream_utils::IvfIterator; + use cros_codecs::codec::av1::parser::ObuAction; + use cros_codecs::codec::av1::parser::ParsedObu; + use cros_codecs::codec::h264::parser::Nalu as H264Nalu; + use cros_codecs::codec::h264::parser::Parser as H264Parser; + use cros_codecs::codec::h265::parser::Nalu as H265Nalu; + use cros_codecs::codec::h265::parser::Parser as H265Parser; + + const H264_25FPS: &[u8] = + include_bytes!("../vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264"); + const H265_25FPS: &[u8] = + include_bytes!("../vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265"); + const AV1_25FPS: &[u8] = + include_bytes!("../vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1"); + const VP9_25FPS: &[u8] = + include_bytes!("../vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9"); + + #[test] + fn h264_parses_the_vendored_vector_to_its_goldens() { + let mut cursor = Cursor::new(H264_25FPS); + let mut parser = H264Parser::default(); + let (mut nalus, mut sps, mut slices) = (0u32, 0u32, 0u32); + let mut coded = (0u32, 0u32); + while let Ok(nalu) = H264Nalu::next(&mut cursor) { + nalus += 1; + if let Ok(s) = parser.parse_sps(&nalu) { + sps += 1; + coded = ( + (s.pic_width_in_mbs_minus1 as u32 + 1) * 16, + (s.pic_height_in_map_units_minus1 as u32 + 1) * 16, + ); + continue; + } + if parser.parse_pps(&nalu).is_ok() { + continue; + } + if parser.parse_slice_header(nalu).is_ok() { + slices += 1; + } + } + // 759 is upstream's own golden for this stream (chromium h264_parser_unittest lineage). + assert_eq!(nalus, 759); + assert_eq!(sps, 4); + assert_eq!(slices, 500); + assert_eq!(coded, (320, 240)); + } + + #[test] + fn h265_parses_the_vendored_vector() { + let mut cursor = Cursor::new(H265_25FPS); + let mut parser = H265Parser::default(); + let (mut nalus, mut sps, mut slices) = (0u32, 0u32, 0u32); + while let Ok(nalu) = H265Nalu::next(&mut cursor) { + nalus += 1; + if parser.parse_sps(&nalu).is_ok() { + sps += 1; + continue; + } + if parser.parse_pps(&nalu).is_ok() { + continue; + } + if parser.parse_slice_header(nalu).is_ok() { + slices += 1; + } + } + assert_eq!(nalus, 254); + assert_eq!(sps, 1); + assert_eq!(slices, 250); + } + + #[test] + fn av1_walks_obus_and_maintains_ref_slots_across_the_stream() { + let mut parser = cros_codecs::codec::av1::parser::Parser::default(); + let (mut obus, mut frames) = (0u32, 0u32); + for packet in IvfIterator::new(AV1_25FPS) { + let mut consumed = 0; + while let Ok(action) = parser.read_obu(&packet[consumed..]) { + let obu = match action { + ObuAction::Process(obu) => obu, + ObuAction::Drop(n) => { + consumed += n as usize; + continue; + } + }; + consumed += obu.bytes_used; + obus += 1; + // `ref_frame_update` is the parser's ref-slot bookkeeping; without it, + // inter frames fail with "Reference is invalid" — the parser validates + // reference integrity rather than trusting the stream. + match parser.parse_obu(obu).expect("parse_obu") { + ParsedObu::FrameHeader(fh) => { + frames += 1; + parser.ref_frame_update(&fh).expect("ref slot update"); + } + ParsedObu::Frame(f) => { + frames += 1; + parser.ref_frame_update(&f.header).expect("ref slot update"); + } + _ => {} + } + } + } + // 525 is upstream's own golden (cross-checked against GStreamer's OBU walk). + assert_eq!(obus, 525); + assert_eq!(frames, 274); + } + + #[test] + fn vp9_splits_superframes_and_parses_headers() { + let mut parser = cros_codecs::codec::vp9::parser::Parser::default(); + let (mut chunks, mut frames) = (0u32, 0u32); + for packet in IvfIterator::new(VP9_25FPS) { + chunks += 1; + frames += parser + .parse_chunk(packet.as_ref()) + .expect("vp9 chunk") + .len() as u32; + } + assert_eq!(chunks, 250); + // > chunks proves superframe splitting engaged. + assert_eq!(frames, 269); + } +} diff --git a/crates/pf-bitstream/src/sei.rs b/crates/pf-bitstream/src/sei.rs new file mode 100644 index 00000000..6ab05efc --- /dev/null +++ b/crates/pf-bitstream/src/sei.rs @@ -0,0 +1,346 @@ +//! SEI payload parsing — the piece the vendored parser layer lacks: upstream classifies +//! SEI NALUs but never reads a payload. punktfunk needs exactly one payload type per +//! codec: the recovery point SEI, which hosts emit on RFI recovery so the client knows +//! where a decode-from-here point lands. Every other payload type is skipped by its +//! declared size. +//! +//! Both codecs put the recovery point at payload type 6 with the same D.1 message +//! framing, but the payload syntax differs: H.264 (D.1.8/D.2.8) counts recovery in +//! `frame_num` increments (`recovery_frame_cnt`, ue(v)) and carries a slice-group bit +//! pair; H.265 (D.2.8/D.3.8) counts in picture order (`recovery_poc_cnt`, se(v) — it +//! can be negative) and has no slice-group field. Hence two parsers over one shared +//! message walk. + +/// Recovery point SEI (D.2.8). +/// +/// `recovery_frame_cnt` counts in `frame_num` increments from the AU carrying the SEI to +/// the picture at which output is exact (`exact_match`) or approximate. `broken_link` set +/// means pictures before the recovery point may be visually broken and must not be shown. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RecoveryPoint { + pub recovery_frame_cnt: u32, + pub exact_match: bool, + pub broken_link: bool, +} + +/// Recovery point SEI, H.265 flavour (D.3.8). +/// +/// `recovery_poc_cnt` is the POC delta from the picture carrying the SEI to the +/// recovery-point picture — se(v)-coded, so unlike H.264's `recovery_frame_cnt` it can +/// be NEGATIVE (a recovery point among leading pictures). `exact_match`/`broken_link` +/// keep their H.264 semantics. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RecoveryPointHevc { + pub recovery_poc_cnt: i32, + pub exact_match: bool, + pub broken_link: bool, +} + +/// Parse the first recovery point SEI message out of an H.264 SEI NALU. +/// +/// `sei_payload` are the bytes of the NALU after its one-byte NAL header, emulation +/// prevention bytes still in place (they are removed here — 7.4.1 RBSP extraction). +/// `Ok(None)` means the NALU parsed cleanly but carries no recovery point. +pub fn parse_recovery_point(sei_payload: &[u8]) -> Result, String> { + let rbsp = strip_emulation_prevention(sei_payload); + let Some(payload) = first_recovery_point_payload(&rbsp)? else { + return Ok(None); + }; + + let mut r = BitCursor::new(payload); + let recovery_frame_cnt = r.read_ue()?; + let exact_match = r.read_bit()? != 0; + let broken_link = r.read_bit()? != 0; + // changing_slice_group_idc u(2): parsed to keep the reader honest, unused — + // slice groups are outside every profile punktfunk hosts emit. + let _changing_slice_group_idc = r.read_bits(2)?; + Ok(Some(RecoveryPoint { + recovery_frame_cnt, + exact_match, + broken_link, + })) +} + +/// Parse the first recovery point SEI message out of an H.265 prefix SEI NALU. +/// +/// `sei_payload` are the bytes of the NALU after its TWO-byte NAL header (H.265 NALU +/// headers are 16 bits), emulation prevention still in place. Only prefix SEI NALUs +/// (type 39) can carry a recovery point — D.2.1 lists it as prefix-only, so suffix SEI +/// NALUs (type 40) need never reach here. +pub fn parse_recovery_point_hevc(sei_payload: &[u8]) -> Result, String> { + let rbsp = strip_emulation_prevention(sei_payload); + let Some(payload) = first_recovery_point_payload(&rbsp)? else { + return Ok(None); + }; + + let mut r = BitCursor::new(payload); + let recovery_poc_cnt = r.read_se()?; + let exact_match = r.read_bit()? != 0; + let broken_link = r.read_bit()? != 0; + Ok(Some(RecoveryPointHevc { + recovery_poc_cnt, + exact_match, + broken_link, + })) +} + +/// Walk the D.1 SEI message framing (shared verbatim between H.264 and H.265) and +/// return the payload bytes of the first recovery point message (payload type 6 in +/// both codecs), if any. `rbsp` is already emulation-prevention-stripped. +fn first_recovery_point_payload(rbsp: &[u8]) -> Result, String> { + let mut i = 0usize; + while i < rbsp.len() && !is_rbsp_trailing(rbsp, i) { + // D.1: payload type and size are ff-coded — 0xFF bytes each add 255 until a + // non-0xFF byte terminates the value. The run length is unbounded, so the type + // accumulates saturating: an adversarial ~16M-byte 0xFF run must not overflow + // (a saturated type simply never matches 6). The size accumulator is a usize + // whose use is bounds-checked below. + let mut payload_type = 0u32; + while i < rbsp.len() && rbsp[i] == 0xFF { + payload_type = payload_type.saturating_add(255); + i += 1; + } + if i >= rbsp.len() { + return Err("truncated SEI payload type".into()); + } + payload_type = payload_type.saturating_add(u32::from(rbsp[i])); + i += 1; + + let mut payload_size = 0usize; + while i < rbsp.len() && rbsp[i] == 0xFF { + payload_size += 255; + i += 1; + } + if i >= rbsp.len() { + return Err("truncated SEI payload size".into()); + } + payload_size += usize::from(rbsp[i]); + i += 1; + + let end = i + .checked_add(payload_size) + .filter(|&end| end <= rbsp.len()) + .ok_or_else(|| "SEI payload overruns the NALU".to_string())?; + + if payload_type == 6 { + return Ok(Some(&rbsp[i..end])); + } + + i = end; + } + + Ok(None) +} + +/// 7.4.1: within the RBSP, `00 00 03` encodes two zero bytes; the `03` is the emulation +/// prevention byte and is dropped. +fn strip_emulation_prevention(data: &[u8]) -> Vec { + let mut out = Vec::with_capacity(data.len()); + let mut zeros = 0usize; + for &byte in data { + if zeros >= 2 && byte == 0x03 { + zeros = 0; + continue; + } + zeros = if byte == 0 { zeros + 1 } else { 0 }; + out.push(byte); + } + out +} + +/// `more_rbsp_data()` at a byte-aligned message boundary: the remainder is trailing bits +/// iff it is the stop bit (0x80) followed by nothing but zero bytes. +fn is_rbsp_trailing(rbsp: &[u8], i: usize) -> bool { + rbsp[i] == 0x80 && rbsp[i + 1..].iter().all(|&b| b == 0) +} + +/// Minimal MSB-first bit reader over an already-unescaped RBSP slice. The vendored +/// `BitReader` is `pub(crate)` to the vendored crate, so this crate carries its own. +struct BitCursor<'a> { + data: &'a [u8], + /// Position in bits from the start of `data`. + pos: usize, +} + +impl<'a> BitCursor<'a> { + fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + fn read_bit(&mut self) -> Result { + let byte = *self + .data + .get(self.pos / 8) + .ok_or("SEI payload out of bits")?; + let bit = (byte >> (7 - self.pos % 8)) & 1; + self.pos += 1; + Ok(u32::from(bit)) + } + + fn read_bits(&mut self, count: usize) -> Result { + debug_assert!(count <= 31); + let mut out = 0u32; + for _ in 0..count { + out = (out << 1) | self.read_bit()?; + } + Ok(out) + } + + /// ue(v), spec 9.1. + fn read_ue(&mut self) -> Result { + let mut leading_zeros = 0usize; + while self.read_bit()? == 0 { + leading_zeros += 1; + if leading_zeros > 31 { + return Err("invalid exp-Golomb code in SEI payload".into()); + } + } + let suffix = self.read_bits(leading_zeros)?; + ((1u32 << leading_zeros) - 1) + .checked_add(suffix) + .ok_or_else(|| "exp-Golomb value overflows u32".to_string()) + } + + /// se(v), spec 9.1.1: the ue(v) code point k maps to (−1)^(k+1) · ⌈k/2⌉. + fn read_se(&mut self) -> Result { + let k = self.read_ue()?; + let magnitude = k.div_ceil(2); + let magnitude = + i32::try_from(magnitude).map_err(|_| "exp-Golomb value overflows i32".to_string())?; + Ok(if k % 2 == 1 { magnitude } else { -magnitude }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_minimal_recovery_point_message_parses_to_its_field_values() { + // Message: type 6, size 1. Payload bits: ue(0)='1', exact=0, broken=0, csg=00, + // then payload alignment '1' + zeros -> 0b1000_0100. NALU trailing 0x80. + let sei = [0x06, 0x01, 0x84, 0x80]; + assert_eq!( + parse_recovery_point(&sei).unwrap(), + Some(RecoveryPoint { + recovery_frame_cnt: 0, + exact_match: false, + broken_link: false + }) + ); + } + + #[test] + fn recovery_frame_cnt_and_both_flags_round_trip_through_the_bit_reader() { + // ue(5)='00110', exact=1, broken=1, csg=00, alignment -> 0b0011_0110 0b0100_0000. + let sei = [0x06, 0x02, 0x36, 0x40, 0x80]; + assert_eq!( + parse_recovery_point(&sei).unwrap(), + Some(RecoveryPoint { + recovery_frame_cnt: 5, + exact_match: true, + broken_link: true + }) + ); + } + + #[test] + fn earlier_messages_and_ff_coded_types_are_skipped_to_reach_the_recovery_point() { + // First message: ff-coded payload type 255 (0xFF 0x00), size 1, payload 0x55. + // Second message: type 5 (user data), size 3. Third: the recovery point. + let sei = [ + 0xFF, 0x00, 0x01, 0x55, // type 255 + 0x05, 0x03, 0xAA, 0xBB, 0xCC, // type 5 + 0x06, 0x01, 0x84, // recovery point + 0x80, + ]; + assert_eq!( + parse_recovery_point(&sei).unwrap(), + Some(RecoveryPoint { + recovery_frame_cnt: 0, + exact_match: false, + broken_link: false + }) + ); + } + + #[test] + fn emulation_prevention_bytes_inside_the_payload_are_removed_before_reading() { + // Unescaped payload (7 bytes): ue with a 22-zero prefix => recovery_frame_cnt + // 2^22-1 = 4194303, exact=1, broken=0, csg=00, alignment. Its first bytes are + // 00 00 02, which the escaper must have written as 00 00 03 02 on the wire. + let sei = [ + 0x06, 0x07, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, 0x04, 0x40, 0x80, + ]; + assert!(sei.windows(3).any(|w| w == [0x00, 0x00, 0x03])); + assert_eq!( + parse_recovery_point(&sei).unwrap(), + Some(RecoveryPoint { + recovery_frame_cnt: 4194303, + exact_match: true, + broken_link: false + }) + ); + } + + #[test] + fn a_sei_nalu_without_a_recovery_point_yields_none_not_an_error() { + let sei = [0x05, 0x01, 0x00, 0x80]; + assert_eq!(parse_recovery_point(&sei).unwrap(), None); + } + + #[test] + fn a_payload_size_overrunning_the_nalu_is_a_parse_error() { + let sei = [0x06, 0x0A, 0x00]; + assert!(parse_recovery_point(&sei).is_err()); + } + + #[test] + fn the_hevc_recovery_point_parses_its_se_coded_poc_count() { + // recovery_poc_cnt se(0) = '1', exact = 0, broken = 0, payload alignment: + // 0b1001_0000. + let sei = [0x06, 0x01, 0x90, 0x80]; + assert_eq!( + parse_recovery_point_hevc(&sei).unwrap(), + Some(RecoveryPointHevc { + recovery_poc_cnt: 0, + exact_match: false, + broken_link: false + }) + ); + + // se(-1) = '011' (ue code point 2), exact = 1, broken = 0, alignment: + // 0b0111_0100 — the negative range H.264's ue(v) syntax cannot express. + let sei = [0x06, 0x01, 0x74, 0x80]; + assert_eq!( + parse_recovery_point_hevc(&sei).unwrap(), + Some(RecoveryPointHevc { + recovery_poc_cnt: -1, + exact_match: true, + broken_link: false + }) + ); + } + + #[test] + fn the_hevc_parser_skips_earlier_messages_and_reports_absence_as_none() { + // User-data message first, then the recovery point (poc_cnt se(3): ue code + // point 5 = '00110', exact = 1, broken = 1, alignment: 0b0011_0111). + let sei = [ + 0x05, 0x02, 0xAA, 0xBB, // type 5 + 0x06, 0x01, 0x37, // recovery point + 0x80, + ]; + assert_eq!( + parse_recovery_point_hevc(&sei).unwrap(), + Some(RecoveryPointHevc { + recovery_poc_cnt: 3, + exact_match: true, + broken_link: true + }) + ); + + let sei = [0x05, 0x01, 0x00, 0x80]; + assert_eq!(parse_recovery_point_hevc(&sei).unwrap(), None); + } +} diff --git a/crates/pf-bitstream/tests/corpus_replay.rs b/crates/pf-bitstream/tests/corpus_replay.rs new file mode 100644 index 00000000..5a759767 --- /dev/null +++ b/crates/pf-bitstream/tests/corpus_replay.rs @@ -0,0 +1,232 @@ +//! Corpus replay: walk a captured real-host stream through the planners. +//! +//! The M0 capture hook (`PUNKTFUNK_DUMP_VIDEO=` on any desktop client) writes +//! the exact decoder input of a live session — `au-.` plus an `.idx` +//! sidecar carrying `offset len flags complete` per AU. This harness feeds those AUs +//! back through [`pf_bitstream::h264::H264Planner`] / [`pf_bitstream::h265::H265Planner`] +//! and asserts the planner survives a REAL host stream: every AU plans (bar the +//! deliberate skips), no panic, and the warnings are only the ones a clean capture may +//! legitimately produce. +//! +//! Why this exists separately from the vendored conformance vectors: those prove we +//! match the spec's own test streams, and the on-glass sessions prove the whole pipe — +//! but between the two sits "does the planner handle what OUR five host encoder +//! families actually emit", which is the question the corpus was captured to answer. +//! For HEVC this is the ONLY pre-wiring validation against real host output (the +//! client's HEVC rung is still being built), so it runs long before M3 finishes. +//! +//! Ignored by default: captures are hundreds of megabytes and live outside the repo. +//! Run one explicitly — +//! +//! ```text +//! PF_CORPUS=/path/to/au-1785970273.h265 \ +//! cargo test -p pf-bitstream --test corpus_replay -- --ignored --nocapture +//! ``` +//! +//! The `.idx` sidecar is found next to the data file (`.idx`); the codec comes +//! from the extension, matching the capture hook's own naming convention. + +use std::path::Path; +use std::path::PathBuf; + +/// One captured access unit: its byte range in the data file, plus the wire bits the +/// byte stream itself cannot carry. +struct CapturedAu { + offset: usize, + len: usize, + /// The wire `flags` byte (`USER_FLAG_*`) — kept for the RFI/intra-refresh legs, + /// which discriminate on it. + _flags: u32, + complete: bool, +} + +/// Parse the `.idx` sidecar: one `offset len flags complete` line per AU, `#` comments +/// and blank lines skipped (the hook writes none today, but a hand-trimmed corpus file +/// is a thing a human will produce). +/// +/// A malformed FINAL line is dropped with a note instead of failing: ending a capture +/// means killing the client, so the last buffered line is routinely half-written (the +/// hook's own docs call a truncated last AU acceptable). Anywhere else a malformed line +/// means the sidecar is corrupt and the run must not quietly replay a subset. +fn read_index(path: &Path) -> Vec { + let text = std::fs::read_to_string(path) + .unwrap_or_else(|e| panic!("cannot read the index sidecar {}: {e}", path.display())); + let lines: Vec<&str> = text + .lines() + .filter(|line| !line.trim().is_empty() && !line.trim_start().starts_with('#')) + .collect(); + let last = lines.len().saturating_sub(1); + let mut out = Vec::with_capacity(lines.len()); + for (n, line) in lines.iter().enumerate() { + match parse_index_line(line) { + Some(au) => out.push(au), + None if n == last => { + println!("note: dropping a truncated final index line ({line:?})"); + } + None => panic!("index line {n} is malformed: {line:?}"), + } + } + out +} + +/// One `offset len flags complete` line, or `None` when it is not four parsable fields. +fn parse_index_line(line: &str) -> Option { + let mut it = line.split_whitespace(); + let num = |raw: &str| -> Option { + match raw.strip_prefix("0x") { + Some(hex) => u64::from_str_radix(hex, 16).ok(), + None => raw.parse().ok(), + } + }; + let offset = num(it.next()?)?; + let len = num(it.next()?)?; + let flags = num(it.next()?)?; + let complete = num(it.next()?)?; + Some(CapturedAu { + offset: offset as usize, + len: len as usize, + _flags: flags as u32, + complete: complete != 0, + }) +} + +/// The capture named by `PF_CORPUS`, or `None` when the variable is unset. +fn corpus_from_env() -> Option<(PathBuf, Vec, Vec)> { + let path = PathBuf::from(std::env::var_os("PF_CORPUS")?); + let data = std::fs::read(&path) + .unwrap_or_else(|e| panic!("cannot read the capture {}: {e}", path.display())); + let mut idx = path.clone().into_os_string(); + idx.push(".idx"); + let mut index = read_index(Path::new(&idx)); + // Same truncation story on the data side: the final AU's bytes may not all have + // reached the file before the client died. Drop AUs the data cannot cover — but + // only from the tail, so a short file can never silently hide a middle gap. + let covered = index + .iter() + .take_while(|au| au.offset.saturating_add(au.len) <= data.len()) + .count(); + if covered < index.len() { + println!( + "note: dropping {} index entr{} past the end of the data file (truncated capture)", + index.len() - covered, + if index.len() - covered == 1 { + "y" + } else { + "ies" + }, + ); + index.truncate(covered); + } + assert!(!index.is_empty(), "the capture's index is empty"); + Some((path, data, index)) +} + +/// Per-AU outcome tally — what the run reports and asserts on. +#[derive(Default)] +struct Tally { + planned: usize, + skipped: usize, + errors: Vec, + warnings: Vec, + partial: usize, +} + +impl Tally { + /// A clean capture of a healthy session must plan every complete AU. Errors are + /// hard failures; warnings are printed and capped — `MissingReference` on a stream + /// that never lost a packet would mean the planner invented a gap. + fn assert_clean(&self, total: usize) { + println!( + "planned {} / skipped {} / partial-AUs-ignored {} / errors {} / warnings {} \ + (of {total} captured AUs)", + self.planned, + self.skipped, + self.partial, + self.errors.len(), + self.warnings.len(), + ); + for w in self.warnings.iter().take(20) { + println!(" warning: {w}"); + } + for e in self.errors.iter().take(20) { + println!(" ERROR: {e}"); + } + assert!( + self.errors.is_empty(), + "{} AUs failed to plan — first: {}", + self.errors.len(), + self.errors[0], + ); + assert!( + self.warnings.is_empty(), + "{} planner warnings on a clean capture — first: {}", + self.warnings.len(), + self.warnings[0], + ); + assert!(self.planned > 0, "no AU planned at all"); + } +} + +#[test] +#[ignore = "needs a capture: PF_CORPUS= (see the module docs)"] +fn a_captured_host_stream_replays_through_the_planner() { + let Some((path, data, index)) = corpus_from_env() else { + panic!("PF_CORPUS is unset — see the module docs for the invocation"); + }; + let ext = path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or_default() + .to_owned(); + println!( + "replaying {} ({} bytes, {} AUs, codec {ext})", + path.display(), + data.len(), + index.len(), + ); + + let mut tally = Tally::default(); + // The planners take one COMPLETE AU. A partial AU (the wire's shard split) is the + // pump's business, not the planner's — count and skip rather than feed a fragment. + let complete: Vec<&CapturedAu> = index.iter().filter(|au| au.complete).collect(); + tally.partial = index.len() - complete.len(); + + match ext.as_str() { + "h265" => { + let mut planner = pf_bitstream::h265::H265Planner::new(); + for (i, au) in complete.iter().enumerate() { + let bytes = &data[au.offset..au.offset + au.len]; + match planner.plan_au(bytes) { + Ok(plan) => { + tally.planned += 1; + for w in &plan.warnings { + tally.warnings.push(format!("AU {i}: {w:?}")); + } + } + // The spec's own skip (8.1.3): decode nothing, show nothing, the + // stream is healthy — never an error (the WP-2 contract note). + Err(pf_bitstream::h265::PlanError::RaslSkipped { .. }) => tally.skipped += 1, + Err(e) => tally.errors.push(format!("AU {i}: {e}")), + } + } + } + "h264" => { + let mut planner = pf_bitstream::h264::H264Planner::new(); + for (i, au) in complete.iter().enumerate() { + let bytes = &data[au.offset..au.offset + au.len]; + match planner.plan_au(bytes) { + Ok(plan) => { + tally.planned += 1; + for w in &plan.warnings { + tally.warnings.push(format!("AU {i}: {w:?}")); + } + } + Err(e) => tally.errors.push(format!("AU {i}: {e}")), + } + } + } + other => panic!("no planner for a .{other} capture (h264/h265 only today)"), + } + + tally.assert_clean(index.len()); +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/Cargo.toml b/crates/pf-bitstream/vendor/cros-codecs/Cargo.toml new file mode 100644 index 00000000..898bfe87 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/Cargo.toml @@ -0,0 +1,17 @@ +# Vendored snapshot — see PROVENANCE.md. Deliberately NOT opted into workspace lints +# or workspace package inheritance: upstream code stays as close to pristine as the +# trim allows, so re-syncing against the AOSP tree stays a diff, not an archaeology dig. +[package] +name = "cros-codecs" +version = "0.0.5" +license = "BSD-3-Clause" +description = "Vendored cros-codecs parser layer (codec module only) for pf-bitstream" +edition = "2021" + +[dependencies] +log = "0.4" + +# Upstream's in-tree unit tests (kept — they are the conformance goldens) want these. +[dev-dependencies] +env_logger = "0.11" +serde_json = "1" diff --git a/crates/pf-bitstream/vendor/cros-codecs/LICENSE b/crates/pf-bitstream/vendor/cros-codecs/LICENSE new file mode 100644 index 00000000..9c1f8394 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/LICENSE @@ -0,0 +1,26 @@ +Copyright 2022 The ChromiumOS Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/crates/pf-bitstream/vendor/cros-codecs/PROVENANCE.md b/crates/pf-bitstream/vendor/cros-codecs/PROVENANCE.md new file mode 100644 index 00000000..d72fb701 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/PROVENANCE.md @@ -0,0 +1,72 @@ +# Vendored: cros-codecs (parser layer only) + +- **Upstream:** (the + authoritative AOSP tree). Snapshot taken from the read-only GitHub mirror + , branch `main`, + commit **`5ff6d693ffae0b36935b8fc13092c733b4c2646f`**, fetched 2026-08-05. +- **License:** BSD-3-Clause (`LICENSE`, copied verbatim). Attribution headers retained + in every source file. +- **Why vendored, not a crates.io dependency:** the GitHub repo is a read-only mirror + and the crates.io release lags it; a pinned, reviewed snapshot is the supply-chain + posture punktfunk already uses elsewhere (`clients/android/native/vendor/ndk`, + `punktfunk-host/vendor/usbip-sim`). Decision of record: + punktfunk-planning `design/client-native-decode.md` §8.1. + +## What was taken + +`src/codec/{h264,h265,av1,vp9}` (parsers, DPBs, picture types, NALU/OBU machinery, +their `test_data` vectors — they double as punktfunk's conformance corpus), +`src/bitstream_utils.rs`, `LICENSE`. Upstream designed the `codec` module for exactly +this extraction — its module doc: "There shall be no dependencies from other modules of +this crate to this module, so that it can be turned into a crate of its own if needed +in the future." + +## What was left behind + +- `decoder/`, `encoder/`, `backend/`, `c2_wrapper/`, `video_frame`, `image_processing`, + `utils` — the Linux-only halves (libva/v4l2/gbm/nix). punktfunk's `pf-bitstream` + + `pf-vkdecode` occupy that layer. +- `codec/vp8` — VP9 has no dependency on it (verified) and no punktfunk host will ever + emit VP8. + +## Deviations from pristine upstream + +1. `src/lib.rs` — rewritten: keeps only the module decls and `Resolution` / + `ResolutionRoundMode` (the sole root items `codec` references), both copied verbatim; + adds crate-level `#![allow(clippy::all, mismatched_lifetime_syntaxes)]` — vendored + code is not held to the workspace lint bar (CI's `-D warnings` legs would fail on + upstream style otherwise). +2. `src/codec.rs` — one line removed (`pub mod vp8;`). +3. `Cargo.toml` — rewritten: `log` is the only dependency the vendored subset needs, + plus `env_logger`/`serde_json` dev-dependencies for upstream's in-tree tests. +4. `cargo fmt` normalization under the workspace's rustfmt config (mechanical only). +5. **Zero-unsafe, enforced**: `#![forbid(unsafe_code)]` added to lib.rs. Upstream's codec + module had exactly one production `unsafe` (h264/dpb.rs `build_ref_pic_lists`: ref→index + via pointer `offset_from`) — replaced with a safe `position(ptr::eq)` over the ≤16-entry + DPB — and three test-only `mem::zeroed()` asserts, replaced with `Default::default()` + (`PredWeightTable` derives `Default`; all-integer struct, identical value). The layer + facing untrusted bytes is now compiler-verified free of unsafe — the property that + motivates replacing libavcodec's C parsers in the first place. +6. `src/codec/h264/picture.rs` — `PictureData::new_from_slice`: `display_resolution` + computed as `visible_rect.max` instead of `max - min`. `Sps::visible_rectangle()` + returns the crop offset in `min` and the visible *size* in `max` (see its + definition: `max.x = width - crop_left - crop_right`); upstream's subtraction + double-counts the left/top crop and, worse, panics on u32 underflow for a + large-but-parser-valid `frame_crop_left_offset` (e.g. 100 crop units on a 320-wide + SPS). Found by pf-bitstream's conformance-window tests; upstream never hits it + because real encoders crop right/bottom only. **Reported upstream 2026-08-06: + .** + +7. `src/codec/h265/parser.rs` — `parse_slice_header`: reject + `num_long_term_sps + num_long_term_pics > 16` before the long-term RPS loop. + Upstream bounds the pair only by `MAX_LONG_TERM_REF_PIC_SETS` (32) combined, while + every long-term array in `SliceHeader` (`poc_lsb_lt`, `used_by_curr_pic_lt`, + `delta_poc_msb_present_flag`, `delta_poc_msb_cycle_lt`, `lt_idx_sps`) is `[_; 16]` + — a hostile slice header with 17+ entries panics the parser with an + index-out-of-bounds (bounds checks stay on in release). Found by pf-bitstream's + H.265 planner review; regression-tested there + (`a_hostile_long_term_count_is_a_parse_error_not_a_panic`). **Reported upstream + 2026-08-06: .** + +Re-sync procedure: fetch the AOSP tree, re-apply this trim, diff `codec/` + +`bitstream_utils.rs` (expect near-zero conflicts), update the commit pin above. diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/bitstream_utils.rs b/crates/pf-bitstream/vendor/cros-codecs/src/bitstream_utils.rs new file mode 100644 index 00000000..07d59dfe --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/bitstream_utils.rs @@ -0,0 +1,788 @@ +// Copyright 2024 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::borrow::Cow; +use std::fmt; +use std::io::Cursor; +use std::io::Read; +use std::io::Seek; +use std::io::SeekFrom; +use std::io::Write; +use std::marker::PhantomData; + +use crate::codec::h264::parser::Nalu as H264Nalu; +use crate::codec::h265::parser::Nalu as H265Nalu; + +/// A bit reader for codec bitstreams. It properly handles emulation-prevention +/// bytes and stop bits for H264. +#[derive(Clone)] +pub(crate) struct BitReader<'a> { + /// A reference into the next unread byte in the stream. + data: Cursor<&'a [u8]>, + /// Contents of the current byte. First unread bit starting at position 8 - + /// num_remaining_bits_in_curr_bytes. + curr_byte: u8, + /// Number of bits remaining in `curr_byte` + num_remaining_bits_in_curr_byte: usize, + /// Used in emulation prevention byte detection. + prev_two_bytes: u16, + /// Number of emulation prevention bytes (i.e. 0x000003) we found. + num_epb: usize, + /// Whether or not we need emulation prevention logic. + needs_epb: bool, + /// How many bits have been read so far. + position: u64, +} + +#[derive(Debug)] +pub(crate) enum GetByteError { + OutOfBits, +} + +impl fmt::Display for GetByteError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "reader ran out of bits") + } +} + +#[derive(Debug)] +pub(crate) enum ReadBitsError { + TooManyBitsRequested(usize), + GetByte(GetByteError), + ConversionFailed, +} + +impl fmt::Display for ReadBitsError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ReadBitsError::TooManyBitsRequested(bits) => { + write!(f, "more than 31 ({}) bits were requested", bits) + } + ReadBitsError::GetByte(_) => write!(f, "failed to advance the current byte"), + ReadBitsError::ConversionFailed => { + write!(f, "failed to convert read input to target type") + } + } + } +} + +impl From for ReadBitsError { + fn from(err: GetByteError) -> Self { + ReadBitsError::GetByte(err) + } +} + +impl<'a> BitReader<'a> { + pub fn new(data: &'a [u8], needs_epb: bool) -> Self { + Self { + data: Cursor::new(data), + curr_byte: Default::default(), + num_remaining_bits_in_curr_byte: Default::default(), + prev_two_bytes: 0xffff, + num_epb: Default::default(), + needs_epb: needs_epb, + position: 0, + } + } + + /// Read a single bit from the stream. + pub fn read_bit(&mut self) -> Result { + let bit = self.read_bits::(1)?; + match bit { + 1 => Ok(true), + 0 => Ok(false), + _ => panic!("Unexpected value {}", bit), + } + } + + /// Read up to 31 bits from the stream. Note that we don't want to read 32 + /// bits even though we're returning a u32 because that would break the + /// read_bits_signed() function. 31 bits should be overkill for compressed + /// header parsing anyway. + pub fn read_bits>(&mut self, num_bits: usize) -> Result { + if num_bits > 31 { + return Err(ReadBitsError::TooManyBitsRequested(num_bits).to_string()); + } + + let mut bits_left = num_bits; + let mut out = 0u32; + + while self.num_remaining_bits_in_curr_byte < bits_left { + out |= (self.curr_byte as u32) << (bits_left - self.num_remaining_bits_in_curr_byte); + bits_left -= self.num_remaining_bits_in_curr_byte; + self.move_to_next_byte().map_err(|err| err.to_string())?; + } + + out |= (self.curr_byte >> (self.num_remaining_bits_in_curr_byte - bits_left)) as u32; + out &= (1 << num_bits) - 1; + self.num_remaining_bits_in_curr_byte -= bits_left; + self.position += num_bits as u64; + + U::try_from(out).map_err(|_| ReadBitsError::ConversionFailed.to_string()) + } + + /// Reads a two's complement signed integer of length |num_bits|. + pub fn read_bits_signed>(&mut self, num_bits: usize) -> Result { + let mut out: i32 = self + .read_bits::(num_bits)? + .try_into() + .map_err(|_| ReadBitsError::ConversionFailed.to_string())?; + if out >> (num_bits - 1) != 0 { + out |= -1i32 ^ ((1 << num_bits) - 1); + } + + U::try_from(out).map_err(|_| ReadBitsError::ConversionFailed.to_string()) + } + + /// Reads an unsigned integer from the stream and checks if the stream is byte aligned. + pub fn read_bits_aligned>(&mut self, num_bits: usize) -> Result { + if self.num_remaining_bits_in_curr_byte % 8 != 0 { + return Err("Attempted unaligned read_le()".into()); + } + + Ok(self.read_bits(num_bits).map_err(|err| err.to_string())?) + } + + /// Skip `num_bits` bits from the stream. + pub fn skip_bits(&mut self, mut num_bits: usize) -> Result<(), String> { + while num_bits > 0 { + let n = std::cmp::min(num_bits, 31); + self.read_bits::(n)?; + num_bits -= n; + } + + Ok(()) + } + + /// Returns the amount of bits left in the stream + pub fn num_bits_left(&mut self) -> usize { + let cur_pos = self.data.position(); + // This should always be safe to unwrap. + let end_pos = self.data.seek(SeekFrom::End(0)).unwrap(); + let _ = self.data.seek(SeekFrom::Start(cur_pos)); + ((end_pos - cur_pos) as usize) * 8 + self.num_remaining_bits_in_curr_byte + } + + /// Returns the number of emulation-prevention bytes read so far. + pub fn num_epb(&self) -> usize { + self.num_epb + } + + /// Whether the stream still has RBSP data. Implements more_rbsp_data(). See + /// the spec for more details. + pub fn has_more_rsbp_data(&mut self) -> bool { + if self.num_remaining_bits_in_curr_byte == 0 && self.move_to_next_byte().is_err() { + // no more data at all in the rbsp + return false; + } + + // If the next bit is the stop bit, then we should only see unset bits + // until the end of the data. + if (self.curr_byte & ((1 << (self.num_remaining_bits_in_curr_byte - 1)) - 1)) != 0 { + return true; + } + + let mut buf = [0u8; 1]; + let orig_pos = self.data.position(); + while let Ok(_) = self.data.read_exact(&mut buf) { + if buf[0] != 0 { + self.data.set_position(orig_pos); + return true; + } + } + false + } + + /// Reads an Unsigned Exponential golomb coding number from the next bytes in the + /// bitstream. This may advance the state of position within the bitstream even if the + /// read operation is unsuccessful. See H264 Annex B specification 9.1 for details. + pub fn read_ue>(&mut self) -> Result { + let mut num_bits = 0; + + while self.read_bits::(1)? == 0 { + num_bits += 1; + if num_bits > 31 { + return Err("invalid stream".into()); + } + } + + let value = ((1u32 << num_bits) - 1) + .checked_add(self.read_bits::(num_bits)?) + .ok_or::("read number cannot fit in 32 bits".into())?; + + U::try_from(value).map_err(|_| "conversion error".into()) + } + + pub fn read_ue_bounded>(&mut self, min: u32, max: u32) -> Result { + let ue = self.read_ue()?; + if ue > max || ue < min { + Err(format!( + "Value out of bounds: expected {} - {}, got {}", + min, max, ue + )) + } else { + Ok(U::try_from(ue).map_err(|_| String::from("Conversion error"))?) + } + } + + pub fn read_ue_max>(&mut self, max: u32) -> Result { + self.read_ue_bounded(0, max) + } + + /// Reads a signed exponential golomb coding number. Instead of using two's + /// complement, this scheme maps even integers to positive numbers and odd + /// integers to negative numbers. The least significant bit indicates the + /// sign. See H264 Annex B specification 9.1.1 for details. + pub fn read_se>(&mut self) -> Result { + let ue = self.read_ue::()? as i32; + + if ue % 2 == 0 { + Ok(U::try_from(-(ue / 2)).map_err(|_| String::from("Conversion error"))?) + } else { + Ok(U::try_from(ue / 2 + 1).map_err(|_| String::from("Conversion error"))?) + } + } + + pub fn read_se_bounded>(&mut self, min: i32, max: i32) -> Result { + let se = self.read_se()?; + if se < min || se > max { + Err(format!( + "Value out of bounds, expected between {}-{}, got {}", + min, max, se + )) + } else { + Ok(U::try_from(se).map_err(|_| String::from("Conversion error"))?) + } + } + + /// Read little endian multi-byte integer. + pub fn read_le>(&mut self, num_bits: u8) -> Result { + let mut t = 0; + + for i in 0..num_bits { + let byte = self.read_bits_aligned::(8)?; + t += byte << (i * 8) + } + + Ok(U::try_from(t).map_err(|_| String::from("Conversion error"))?) + } + + /// Return the position of this bitstream in bits. + pub fn position(&self) -> u64 { + self.position + } + + fn get_byte(&mut self) -> Result { + let mut buf = [0u8; 1]; + self.data + .read_exact(&mut buf) + .map_err(|_| GetByteError::OutOfBits)?; + Ok(buf[0]) + } + + fn move_to_next_byte(&mut self) -> Result<(), GetByteError> { + let mut byte = self.get_byte()?; + + if self.needs_epb { + if self.prev_two_bytes == 0 && byte == 0x03 { + // We found an epb + self.num_epb += 1; + // Read another byte + byte = self.get_byte()?; + // We need another 3 bytes before another epb can happen. + self.prev_two_bytes = 0xffff; + } + self.prev_two_bytes = (self.prev_two_bytes << 8) | u16::from(byte); + } + + self.num_remaining_bits_in_curr_byte = 8; + self.curr_byte = byte; + Ok(()) + } +} + +/// Iterator over IVF packets. +pub struct IvfIterator<'a> { + cursor: Cursor<&'a [u8]>, +} + +impl<'a> IvfIterator<'a> { + pub fn new(data: &'a [u8]) -> Self { + let mut cursor = Cursor::new(data); + + // Skip the IVH header entirely. + cursor.seek(std::io::SeekFrom::Start(32)).unwrap(); + + Self { cursor } + } +} + +impl<'a> Iterator for IvfIterator<'a> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option { + // Make sure we have a header. + let mut len_buf = [0u8; 4]; + self.cursor.read_exact(&mut len_buf).ok()?; + let len = ((len_buf[3] as usize) << 24) + | ((len_buf[2] as usize) << 16) + | ((len_buf[1] as usize) << 8) + | (len_buf[0] as usize); + + // Skip PTS. + self.cursor.seek(std::io::SeekFrom::Current(8)).ok()?; + + let start = self.cursor.position() as usize; + let _ = self + .cursor + .seek(std::io::SeekFrom::Current(len as i64)) + .ok()?; + let end = self.cursor.position() as usize; + + Some(&self.cursor.get_ref()[start..end]) + } +} + +/// Helper struct for synthesizing IVF file header +pub struct IvfFileHeader { + pub magic: [u8; 4], + pub version: u16, + pub header_size: u16, + pub codec: [u8; 4], + pub width: u16, + pub height: u16, + pub framerate: u32, + pub timescale: u32, + pub frame_count: u32, + pub unused: u32, +} + +impl Default for IvfFileHeader { + fn default() -> Self { + Self { + magic: Self::MAGIC, + version: 0, + header_size: 32, + codec: Self::CODEC_VP9, + width: 320, + height: 240, + framerate: 1, + timescale: 1000, + frame_count: 1, + unused: Default::default(), + } + } +} + +impl IvfFileHeader { + pub const MAGIC: [u8; 4] = *b"DKIF"; + pub const CODEC_VP8: [u8; 4] = *b"VP80"; + pub const CODEC_VP9: [u8; 4] = *b"VP90"; + pub const CODEC_AV1: [u8; 4] = *b"AV01"; + + pub fn new(codec: [u8; 4], width: u16, height: u16, framerate: u32, frame_count: u32) -> Self { + let default = Self::default(); + + Self { + codec, + width, + height, + framerate: framerate * default.timescale, + frame_count, + ..default + } + } +} + +impl IvfFileHeader { + /// Writes header into writer + pub fn writo_into(&self, writer: &mut impl std::io::Write) -> std::io::Result<()> { + writer.write_all(&self.magic)?; + writer.write_all(&self.version.to_le_bytes())?; + writer.write_all(&self.header_size.to_le_bytes())?; + writer.write_all(&self.codec)?; + writer.write_all(&self.width.to_le_bytes())?; + writer.write_all(&self.height.to_le_bytes())?; + writer.write_all(&self.framerate.to_le_bytes())?; + writer.write_all(&self.timescale.to_le_bytes())?; + writer.write_all(&self.frame_count.to_le_bytes())?; + writer.write_all(&self.unused.to_le_bytes())?; + + Ok(()) + } +} + +/// Helper struct for synthesizing IVF frame header +pub struct IvfFrameHeader { + pub frame_size: u32, + pub timestamp: u64, +} + +impl IvfFrameHeader { + /// Writes header into writer + pub fn writo_into(&self, writer: &mut impl std::io::Write) -> std::io::Result<()> { + writer.write_all(&self.frame_size.to_le_bytes())?; + writer.write_all(&self.timestamp.to_le_bytes())?; + Ok(()) + } +} + +/// Iterator NALUs in a bitstream. +pub struct NalIterator<'a, Nalu>(Cursor<&'a [u8]>, PhantomData); + +impl<'a, Nalu> NalIterator<'a, Nalu> { + pub fn new(stream: &'a [u8]) -> Self { + Self(Cursor::new(stream), PhantomData) + } +} + +impl<'a> Iterator for NalIterator<'a, H264Nalu<'a>> { + type Item = Cow<'a, [u8]>; + + fn next(&mut self) -> Option { + H264Nalu::next(&mut self.0).map(|n| n.data).ok() + } +} + +impl<'a> Iterator for NalIterator<'a, H265Nalu<'a>> { + type Item = Cow<'a, [u8]>; + + fn next(&mut self) -> Option { + H265Nalu::next(&mut self.0).map(|n| n.data).ok() + } +} + +#[derive(Debug)] +pub enum BitWriterError { + InvalidBitCount, + Io(std::io::Error), +} + +impl fmt::Display for BitWriterError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + BitWriterError::InvalidBitCount => write!(f, "invalid bit count"), + BitWriterError::Io(x) => write!(f, "{}", x.to_string()), + } + } +} + +impl From for BitWriterError { + fn from(err: std::io::Error) -> Self { + BitWriterError::Io(err) + } +} + +pub type BitWriterResult = std::result::Result; + +pub struct BitWriter { + out: W, + nth_bit: u8, + curr_byte: u8, +} + +impl BitWriter { + pub fn new(writer: W) -> Self { + Self { + out: writer, + curr_byte: 0, + nth_bit: 0, + } + } + + /// Writes fixed bit size integer (up to 32 bit) + pub fn write_f>(&mut self, bits: usize, value: T) -> BitWriterResult { + let value = value.into(); + + if bits > 32 { + return Err(BitWriterError::InvalidBitCount); + } + + let mut written = 0; + for bit in (0..bits).rev() { + let bit = (1 << bit) as u32; + + self.write_bit((value & bit) == bit)?; + written += 1; + } + + Ok(written) + } + + /// Takes a single bit that will be outputed to [`std::io::Write`] + pub fn write_bit(&mut self, bit: bool) -> BitWriterResult<()> { + self.curr_byte |= (bit as u8) << (7u8 - self.nth_bit); + self.nth_bit += 1; + + if self.nth_bit == 8 { + self.out.write_all(&[self.curr_byte])?; + self.nth_bit = 0; + self.curr_byte = 0; + } + + Ok(()) + } + + /// Immediately outputs any cached bits to [`std::io::Write`] + pub fn flush(&mut self) -> BitWriterResult<()> { + if self.nth_bit != 0 { + self.out.write_all(&[self.curr_byte])?; + self.nth_bit = 0; + self.curr_byte = 0; + } + + self.out.flush()?; + Ok(()) + } + + /// Returns `true` if ['Self`] hold data that wasn't written to [`std::io::Write`] + pub fn has_data_pending(&self) -> bool { + self.nth_bit != 0 + } + + pub(crate) fn inner(&self) -> &W { + &self.out + } + + pub(crate) fn inner_mut(&mut self) -> &mut W { + &mut self.out + } +} + +impl Drop for BitWriter { + fn drop(&mut self) { + if let Err(e) = self.flush() { + log::error!("Unable to flush bits {e:?}"); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ivf_file_header() { + let mut hdr = IvfFileHeader { + version: 0, + codec: IvfFileHeader::CODEC_VP9, + width: 256, + height: 256, + framerate: 30_000, + timescale: 1_000, + frame_count: 1, + + ..Default::default() + }; + + let mut buf = Vec::new(); + hdr.writo_into(&mut buf).unwrap(); + + const EXPECTED: [u8; 32] = [ + 0x44, 0x4b, 0x49, 0x46, 0x00, 0x00, 0x20, 0x00, 0x56, 0x50, 0x39, 0x30, 0x00, 0x01, + 0x00, 0x01, 0x30, 0x75, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + + assert_eq!(&buf, &EXPECTED); + + hdr.width = 1920; + hdr.height = 800; + hdr.framerate = 24; + hdr.timescale = 1; + hdr.frame_count = 100; + + buf.clear(); + hdr.writo_into(&mut buf).unwrap(); + + const EXPECTED2: [u8; 32] = [ + 0x44, 0x4b, 0x49, 0x46, 0x00, 0x00, 0x20, 0x00, 0x56, 0x50, 0x39, 0x30, 0x80, 0x07, + 0x20, 0x03, 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + + assert_eq!(&buf, &EXPECTED2); + } + + #[test] + fn test_ivf_frame_header() { + let mut hdr = IvfFrameHeader { + frame_size: 199249, + timestamp: 0, + }; + + let mut buf = Vec::new(); + hdr.writo_into(&mut buf).unwrap(); + + const EXPECTED: [u8; 12] = [ + 0x51, 0x0a, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + assert_eq!(&buf, &EXPECTED); + + hdr.timestamp = 1; + hdr.frame_size = 52; + + buf.clear(); + hdr.writo_into(&mut buf).unwrap(); + + const EXPECTED2: [u8; 12] = [ + 0x34, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + assert_eq!(&buf, &EXPECTED2); + } + + #[test] + fn test_bitwriter_f1() { + let mut buf = Vec::::new(); + { + let mut writer = BitWriter::new(&mut buf); + writer.write_f(1, true).unwrap(); + writer.write_f(1, false).unwrap(); + writer.write_f(1, false).unwrap(); + writer.write_f(1, false).unwrap(); + writer.write_f(1, true).unwrap(); + writer.write_f(1, true).unwrap(); + writer.write_f(1, true).unwrap(); + writer.write_f(1, true).unwrap(); + } + assert_eq!(buf, vec![0b10001111u8]); + } + + #[test] + fn test_bitwriter_f3() { + let mut buf = Vec::::new(); + { + let mut writer = BitWriter::new(&mut buf); + writer.write_f(3, 0b100u8).unwrap(); + writer.write_f(3, 0b101u8).unwrap(); + writer.write_f(3, 0b011u8).unwrap(); + } + assert_eq!(buf, vec![0b10010101u8, 0b10000000u8]); + } + + #[test] + fn test_bitwriter_f4() { + let mut buf = Vec::::new(); + { + let mut writer = BitWriter::new(&mut buf); + writer.write_f(4, 0b1000u8).unwrap(); + writer.write_f(4, 0b1011u8).unwrap(); + } + assert_eq!(buf, vec![0b10001011u8]); + } + + // These tests are adapted from the chromium tests at media/video/h264_bit_reader_unitttest.cc + + #[test] + fn read_stream_without_escape_and_trailing_zero_bytes() { + const RBSP: [u8; 6] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xa0]; + + let mut reader = BitReader::new(&RBSP, true); + assert_eq!(reader.read_bits::(1).unwrap(), 0); + assert_eq!(reader.num_bits_left(), 47); + assert!(reader.has_more_rsbp_data()); + + assert_eq!(reader.read_bits::(8).unwrap(), 0x02); + assert_eq!(reader.num_bits_left(), 39); + assert!(reader.has_more_rsbp_data()); + + assert_eq!(reader.read_bits::(31).unwrap(), 0x23456789); + assert_eq!(reader.num_bits_left(), 8); + assert!(reader.has_more_rsbp_data()); + + assert_eq!(reader.read_bits::(1).unwrap(), 1); + assert_eq!(reader.num_bits_left(), 7); + assert!(reader.has_more_rsbp_data()); + + assert_eq!(reader.read_bits::(1).unwrap(), 0); + assert_eq!(reader.num_bits_left(), 6); + assert!(!reader.has_more_rsbp_data()); + } + + #[test] + fn single_byte_stream() { + const RBSP: [u8; 1] = [0x18]; + + let mut reader = BitReader::new(&RBSP, true); + assert_eq!(reader.num_bits_left(), 8); + assert!(reader.has_more_rsbp_data()); + assert_eq!(reader.read_bits::(4).unwrap(), 1); + assert!(!reader.has_more_rsbp_data()); + } + + #[test] + fn stop_bit_occupy_full_byte() { + const RBSP: [u8; 2] = [0xab, 0x80]; + + let mut reader = BitReader::new(&RBSP, true); + assert_eq!(reader.num_bits_left(), 16); + assert!(reader.has_more_rsbp_data()); + + assert_eq!(reader.read_bits::(8).unwrap(), 0xab); + assert_eq!(reader.num_bits_left(), 8); + + assert!(!reader.has_more_rsbp_data()); + } + + // Check that read_ue behaves properly with input at the limits. + #[test] + fn read_ue() { + // Regular value. + let mut reader = BitReader::new(&[0b0001_1010], true); + assert_eq!(reader.read_ue::().unwrap(), 12); + assert_eq!(reader.data.position(), 1); + assert_eq!(reader.num_remaining_bits_in_curr_byte, 1); + + // 0 value. + let mut reader = BitReader::new(&[0b1000_0000], true); + assert_eq!(reader.read_ue::().unwrap(), 0); + assert_eq!(reader.data.position(), 1); + assert_eq!(reader.num_remaining_bits_in_curr_byte, 7); + + // No prefix stop bit. + let mut reader = BitReader::new(&[0b0000_0000], true); + reader.read_ue::().unwrap_err(); + + // u32 max value: 31 0-bits, 1 bit marker, 31 bits 1-bits. + let mut reader = BitReader::new( + &[ + 0b0000_0000, + 0b0000_0000, + 0b0000_0000, + 0b0000_0001, + 0b1111_1111, + 0b1111_1111, + 0b1111_1111, + 0b1111_1110, + ], + true, + ); + assert_eq!(reader.read_ue::().unwrap(), 0xffff_fffe); + assert_eq!(reader.data.position(), 8); + assert_eq!(reader.num_remaining_bits_in_curr_byte, 1); + } + + // Check that emulation prevention is being handled correctly. + #[test] + fn skip_epb_when_enabled() { + let mut reader = BitReader::new(&[0x00, 0x00, 0x03, 0x01], false); + assert_eq!(reader.read_bits::(8).unwrap(), 0x00); + assert_eq!(reader.read_bits::(8).unwrap(), 0x00); + assert_eq!(reader.read_bits::(8).unwrap(), 0x03); + assert_eq!(reader.read_bits::(8).unwrap(), 0x01); + + let mut reader = BitReader::new(&[0x00, 0x00, 0x03, 0x01], true); + assert_eq!(reader.read_bits::(8).unwrap(), 0x00); + assert_eq!(reader.read_bits::(8).unwrap(), 0x00); + assert_eq!(reader.read_bits::(8).unwrap(), 0x01); + } + + #[test] + fn read_signed_bits() { + let mut reader = BitReader::new(&[0b1111_0000], false); + assert_eq!(reader.read_bits_signed::(4).unwrap(), -1); + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec.rs new file mode 100644 index 00000000..23876f40 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec.rs @@ -0,0 +1,17 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +//! Parsers for various kinds of encoded streams. +//! +//! This module does not provide any actual decoding tools - that's the job of the +//! [crate::decoder] module. However the parsers of this module are heavily used in order to +//! implement stateless decoding. +//! +//! There shall be no dependencies from other modules of this crate to this module, so that it +//! can be turned into a crate of its own if needed in the future. + +pub mod av1; +pub mod h264; +pub mod h265; +pub mod vp9; diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1.rs new file mode 100644 index 00000000..477cd906 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1.rs @@ -0,0 +1,9 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +mod helpers; +pub mod parser; +pub mod reader; +pub mod synthesizer; +pub mod writer; diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/helpers.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/helpers.rs new file mode 100644 index 00000000..f8df7cd8 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/helpers.rs @@ -0,0 +1,186 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use crate::codec::av1::parser::NUM_REF_FRAMES; + +const DIV_LUT: [i32; 257] = [ + 16384, 16320, 16257, 16194, 16132, 16070, 16009, 15948, 15888, 15828, 15768, 15709, 15650, + 15592, 15534, 15477, 15420, 15364, 15308, 15252, 15197, 15142, 15087, 15033, 14980, 14926, + 14873, 14821, 14769, 14717, 14665, 14614, 14564, 14513, 14463, 14413, 14364, 14315, 14266, + 14218, 14170, 14122, 14075, 14028, 13981, 13935, 13888, 13843, 13797, 13752, 13707, 13662, + 13618, 13574, 13530, 13487, 13443, 13400, 13358, 13315, 13273, 13231, 13190, 13148, 13107, + 13066, 13026, 12985, 12945, 12906, 12866, 12827, 12788, 12749, 12710, 12672, 12633, 12596, + 12558, 12520, 12483, 12446, 12409, 12373, 12336, 12300, 12264, 12228, 12193, 12157, 12122, + 12087, 12053, 12018, 11984, 11950, 11916, 11882, 11848, 11815, 11782, 11749, 11716, 11683, + 11651, 11619, 11586, 11555, 11523, 11491, 11460, 11429, 11398, 11367, 11336, 11305, 11275, + 11245, 11215, 11185, 11155, 11125, 11096, 11067, 11038, 11009, 10980, 10951, 10923, 10894, + 10866, 10838, 10810, 10782, 10755, 10727, 10700, 10673, 10645, 10618, 10592, 10565, 10538, + 10512, 10486, 10460, 10434, 10408, 10382, 10356, 10331, 10305, 10280, 10255, 10230, 10205, + 10180, 10156, 10131, 10107, 10082, 10058, 10034, 10010, 9986, 9963, 9939, 9916, 9892, 9869, + 9846, 9823, 9800, 9777, 9754, 9732, 9709, 9687, 9664, 9642, 9620, 9598, 9576, 9554, 9533, 9511, + 9489, 9468, 9447, 9425, 9404, 9383, 9362, 9341, 9321, 9300, 9279, 9259, 9239, 9218, 9198, 9178, + 9158, 9138, 9118, 9098, 9079, 9059, 9039, 9020, 9001, 8981, 8962, 8943, 8924, 8905, 8886, 8867, + 8849, 8830, 8812, 8793, 8775, 8756, 8738, 8720, 8702, 8684, 8666, 8648, 8630, 8613, 8595, 8577, + 8560, 8542, 8525, 8508, 8490, 8473, 8456, 8439, 8422, 8405, 8389, 8372, 8355, 8339, 8322, 8306, + 8289, 8273, 8257, 8240, 8224, 8208, 8192, +]; + +const DIV_LUT_BITS: u32 = 8; +const DIV_LUT_PREC_BITS: u32 = 14; + +/// Implements FloorLog2(x), which is defined to be the floor of the base 2 +/// logarithm of the input x. +/// +/// The input x will always be an integer, and will always be greater than or equal to 1. +/// This function extracts the location of the most significant bit in x. +pub fn floor_log2(mut x: u32) -> u32 { + assert!(x > 0); + let mut s = 0; + + while x != 0 { + x >>= 1; + s += 1; + } + + s - 1 +} + +/// Implements 5.9.3. Get relative distance function +pub fn get_relative_dist(enable_order_hint: bool, order_hint_bits: i32, a: i32, b: i32) -> i32 { + if !enable_order_hint { + 0 + } else { + let diff = a - b; + let m = 1 << (order_hint_bits - 1); + (diff & (m - 1)) - (diff & m) + } +} + +/// Implements find_latest_backward from section 7.8. +pub fn find_latest_backward( + shifted_order_hints: &[i32; NUM_REF_FRAMES], + used_frame: &[bool; NUM_REF_FRAMES], + cur_frame_hint: i32, + latest_order_hint: &mut i32, +) -> i32 { + let mut _ref = -1; + + for i in 0..NUM_REF_FRAMES { + let hint = shifted_order_hints[i]; + if !used_frame[i] && hint >= cur_frame_hint && (_ref < 0 || hint >= *latest_order_hint) { + _ref = i as i32; + *latest_order_hint = hint; + } + } + + _ref +} + +/// Implements find_earliest_backward from section 7.8. +pub fn find_earliest_backward( + shifted_order_hints: &[i32; NUM_REF_FRAMES], + used_frame: &[bool; NUM_REF_FRAMES], + cur_frame_hint: i32, + earliest_order_hint: &mut i32, +) -> i32 { + let mut _ref = -1; + + for i in 0..NUM_REF_FRAMES { + let hint = shifted_order_hints[i]; + if !used_frame[i] && hint >= cur_frame_hint && (_ref < 0 || hint < *earliest_order_hint) { + _ref = i as i32; + *earliest_order_hint = hint; + } + } + + _ref +} + +/// Implements find_latest_forward from section 7.8. +pub fn find_latest_forward( + shifted_order_hints: &[i32; NUM_REF_FRAMES], + used_frame: &[bool; NUM_REF_FRAMES], + cur_frame_hint: i32, + latest_order_hint: &mut i32, +) -> i32 { + let mut _ref = -1; + for i in 0..NUM_REF_FRAMES { + let hint = shifted_order_hints[i]; + if !used_frame[i] && hint < cur_frame_hint && (_ref < 0 || hint >= *latest_order_hint) { + _ref = i as i32; + *latest_order_hint = hint; + } + } + + _ref +} + +pub fn tile_log2(blk_size: u32, target: u32) -> u32 { + let mut k = 0; + + while (blk_size << k) < target { + k += 1; + } + + k +} + +pub fn clip3(x: i32, y: i32, z: i32) -> i32 { + if z < x { + x + } else if z > y { + y + } else { + z + } +} + +/// 5.9.29 +pub fn inverse_recenter(r: i32, v: i32) -> i32 { + if v > 2 * r { + v + } else if v & 1 != 0 { + r - ((v + 1) >> 1) + } else { + r + (v >> 1) + } +} + +/// Implements Round2. See 4.7: mathematical functions. +pub fn round2(x: u32, n: u32) -> u32 { + (x + 2u32.pow(n - 1)) / 2u32.pow(n) +} + +/// Implements Round2Signed. See 4.7: mathematical functions. +pub fn round2signed(x: i32, n: u32) -> Result { + if x >= 0 { + i32::try_from(round2(x as u32, n)).map_err(|e| e.to_string()) + } else { + let x = x as i64; + let val = i32::try_from(round2(-x as u32, n)).map_err(|e| e.to_string())?; + Ok(-val) + } +} + +/// Implements 7.11.3.7. Resolve divisor process +pub fn resolve_divisor(d: i32) -> Result<(u32, i32), String> { + let abs_d = u32::try_from(d.abs()).unwrap(); // abs cannot return a negative + let n = floor_log2(abs_d); + let e = abs_d - (1 << n); + + let f = if n > DIV_LUT_BITS { + round2(e, n - DIV_LUT_BITS) + } else { + e << (DIV_LUT_BITS - n) + }; + + let div_shift = n + DIV_LUT_PREC_BITS; + let div_factor = if d < 0 { + -DIV_LUT[f as usize] + } else { + DIV_LUT[f as usize] + }; + + Ok((div_shift, div_factor)) +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/parser.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/parser.rs new file mode 100644 index 00000000..ecb40a9f --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/parser.rs @@ -0,0 +1,4294 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::borrow::Cow; +use std::rc::Rc; + +use crate::codec::av1::helpers; +use crate::codec::av1::reader::Reader; + +pub const TOTAL_REFS_PER_FRAME: usize = 8; +pub const NUM_REF_FRAMES: usize = 8; +pub const REFS_PER_FRAME: usize = 7; +pub const MAX_SEGMENTS: usize = 8; +pub const SEG_LVL_ALT_Q: usize = 0; +pub const SEG_LVL_ALT_LF_Y_V: usize = 1; +pub const SEG_LVL_REF_FRAME: usize = 5; +pub const SEG_LVL_SKIP: usize = 6; +pub const SEG_LVL_GLOBAL_MV: usize = 7; +pub const SEG_LVL_MAX: usize = 8; +pub const MAX_TILE_COLS: usize = 64; +pub const MAX_TILE_ROWS: usize = 64; +pub const CDEF_MAX: usize = 1 << 3; +pub const MAX_NUM_PLANES: usize = 3; +pub const MAX_NUM_Y_POINTS: usize = 16; +pub const MAX_NUM_CB_POINTS: usize = 16; +pub const MAX_NUM_CR_POINTS: usize = 16; +pub const MAX_NUM_POS_LUMA: usize = 25; +pub const MAX_NUM_SPATIAL_LAYERS: usize = 4; +pub const MAX_NUM_TEMPORAL_LAYERS: usize = 8; +pub const MAX_NUM_OPERATING_POINTS: usize = MAX_NUM_SPATIAL_LAYERS * MAX_NUM_TEMPORAL_LAYERS; +pub const SELECT_SCREEN_CONTENT_TOOLS: usize = 2; +pub const SELECT_INTEGER_MV: usize = 2; +pub const PRIMARY_REF_NONE: u32 = 7; +pub const SUPERRES_DENOM_BITS: usize = 3; +pub const SUPERRES_DENOM_MIN: usize = 9; +pub const SUPERRES_NUM: usize = 8; +pub const MAX_TILE_WIDTH: u32 = 4096; +pub const MAX_TILE_HEIGHT: u32 = 2304; +pub const MAX_TILE_AREA: u32 = MAX_TILE_WIDTH * MAX_TILE_HEIGHT; +pub const RESTORATION_TILESIZE_MAX: u16 = 256; +pub const WARPEDMODEL_PREC_BITS: u32 = 16; +pub const WARP_PARAM_REDUCE_BITS: u32 = 6; +pub const GM_ABS_ALPHA_BITS: u32 = 12; +pub const GM_ALPHA_PREC_BITS: u32 = 15; +pub const GM_ABS_TRANS_ONLY_BITS: u32 = 9; +pub const GM_TRANS_ONLY_PREC_BITS: u32 = 3; +pub const GM_ABS_TRANS_BITS: u32 = 12; +pub const GM_TRANS_PREC_BITS: u32 = 6; + +// Same as Segmentation_Feature_Bits in the specification. See 5.9.14 +pub const FEATURE_BITS: [u8; SEG_LVL_MAX] = [8, 6, 6, 6, 6, 3, 0, 0]; +// Same as Segmentation_Feature_Signed in the specification. See 5.9.14 +pub const FEATURE_SIGNED: [bool; SEG_LVL_MAX] = [true, true, true, true, true, false, false, false]; +// Same as Segmentation_Feature_Max in the specification. See 5.9.14 +pub const FEATURE_MAX: [i32; SEG_LVL_MAX] = [255, 63, 63, 63, 63, 7, 0, 0]; + +/// Tells what should be done with the OBU after [`Parser::read_obu`] is called. +pub enum ObuAction<'a> { + /// We should process the OBU normally. + Process(Obu<'a>), + /// We should drop this OBU and advance to the next one. The u32 is how much + /// we should advance. + Drop(u32), +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum ObuType { + #[default] + Reserved = 0, + SequenceHeader = 1, + TemporalDelimiter = 2, + FrameHeader = 3, + TileGroup = 4, + Metadata = 5, + Frame = 6, + RedundantFrameHeader = 7, + TileList = 8, + Reserved2 = 9, + Reserved3 = 10, + Reserved4 = 11, + Reserved5 = 12, + Reserved6 = 13, + Reserved7 = 14, + Padding = 15, +} + +impl TryFrom for ObuType { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(ObuType::Reserved), + 1 => Ok(ObuType::SequenceHeader), + 2 => Ok(ObuType::TemporalDelimiter), + 3 => Ok(ObuType::FrameHeader), + 4 => Ok(ObuType::TileGroup), + 5 => Ok(ObuType::Metadata), + 6 => Ok(ObuType::Frame), + 7 => Ok(ObuType::RedundantFrameHeader), + 8 => Ok(ObuType::TileList), + 9 => Ok(ObuType::Reserved2), + 10 => Ok(ObuType::Reserved3), + 11 => Ok(ObuType::Reserved4), + 12 => Ok(ObuType::Reserved5), + 13 => Ok(ObuType::Reserved6), + 14 => Ok(ObuType::Reserved7), + 15 => Ok(ObuType::Padding), + _ => Err(format!("Invalid ObuType {}", value)), + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum Profile { + #[default] + Profile0 = 0, + Profile1 = 1, + Profile2 = 2, +} + +impl TryFrom for Profile { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Profile::Profile0), + 1 => Ok(Profile::Profile1), + 2 => Ok(Profile::Profile2), + _ => Err(format!("Invalid Profile {}", value)), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ObuHeader { + pub obu_type: ObuType, + pub extension_flag: bool, + pub has_size_field: bool, + pub temporal_id: u32, + pub spatial_id: u32, +} + +impl ObuHeader { + /// Length in bytes + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + if self.extension_flag { + 2 + } else { + 1 + } + } +} + +/// Contains the OBU header and a reference to its data. The OBU itself hasn't been parsed yet. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Obu<'a> { + /// The OBU header. + pub header: ObuHeader, + /// Amount of bytes from the input consumed to parse this OBU. + pub bytes_used: usize, + /// The slice backing the OBU. + data: Cow<'a, [u8]>, +} + +impl<'a> AsRef<[u8]> for Obu<'a> { + fn as_ref(&self) -> &[u8] { + self.data.as_ref() + } +} + +/// A fully parsed OBU, with additional data when relevant. +pub enum ParsedObu<'a> { + Reserved, + SequenceHeader(Rc), + TemporalDelimiter, + FrameHeader(FrameHeaderObu), + TileGroup(TileGroupObu<'a>), + Metadata, + Frame(FrameObu<'a>), + RedundantFrameHeader, + TileList, + Reserved2, + Reserved3, + Reserved4, + Reserved5, + Reserved6, + Reserved7, + Padding, +} + +impl<'a> ParsedObu<'a> { + pub fn obu_type(&self) -> ObuType { + match self { + ParsedObu::Reserved => ObuType::Reserved, + ParsedObu::SequenceHeader(_) => ObuType::SequenceHeader, + ParsedObu::TemporalDelimiter => ObuType::TemporalDelimiter, + ParsedObu::FrameHeader(_) => ObuType::FrameHeader, + ParsedObu::TileGroup(_) => ObuType::TileGroup, + ParsedObu::Metadata => ObuType::Metadata, + ParsedObu::Frame(_) => ObuType::Frame, + ParsedObu::RedundantFrameHeader => ObuType::RedundantFrameHeader, + ParsedObu::TileList => ObuType::TileList, + ParsedObu::Reserved2 => ObuType::Reserved2, + ParsedObu::Reserved3 => ObuType::Reserved3, + ParsedObu::Reserved4 => ObuType::Reserved4, + ParsedObu::Reserved5 => ObuType::Reserved5, + ParsedObu::Reserved6 => ObuType::Reserved6, + ParsedObu::Reserved7 => ObuType::Reserved7, + ParsedObu::Padding => ObuType::Padding, + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Tile { + /// Same as TileOffset in the specification. + pub tile_offset: u32, + /// Same as TileSize in the specification. + pub tile_size: u32, + /// Same as TileRow in the specification. + pub tile_row: u32, + /// Same as TileCol in the specification. + pub tile_col: u32, + // Same as MiRowStart in the specification. + pub mi_row_start: u32, + // Same as MiRowEnd in the specification. + pub mi_row_end: u32, + // Same as MiColStart in the specification. + pub mi_col_start: u32, + // Same as MiColEnd in the specification. + pub mi_col_end: u32, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TileGroupObu<'a> { + /// The OBU backing this tile group. + pub obu: Obu<'a>, + /// Specifies whether tg_start and tg_end are present. If tg_start and + /// tg_end are not present, this tile group covers the entire frame. + pub tile_start_and_end_present_flag: bool, + /// Specifies the zero-based index of the first tile in the current tile + /// group. + pub tg_start: u32, + /// Specifies the zero-based index of the last tile in the current tile + /// group. + pub tg_end: u32, + /// Contains the tiles in this tile group. Use `tile_offset`to index into + /// the OBU data. + /// + /// The tiles in the Vec span from tg_start to tg_end. + pub tiles: Vec, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct OperatingPoint { + /// Specifies the level that the coded video sequence conforms to when + /// operating point i is selected. + pub seq_level_idx: u8, + /// Specifies the tier that the coded video sequence conforms to when + /// operating point i is selected. + pub seq_tier: u8, + /// Specifies the value of operating_point_idc for the selected operating + /// point. + pub idc: u16, + /// If set, indicates that there is a decoder model associated with + /// operating point i. If not set, indicates that there is not a decoder + /// model associated with operating point i. + pub decoder_model_present_for_this_op: bool, + /// Specifies the time interval between the arrival of the first bit in the + /// smoothing buffer and the subsequent removal of the data that belongs to + /// the first coded frame for operating point op, measured in units of + /// 1/90000 seconds. The length of decoder_buffer_delay is specified by + /// buffer_delay_length_minus_1 + 1, in bits. + pub decoder_buffer_delay: u32, + /// Specifies, in combination with decoder_buffer_delay\[ op \] syntax + /// element, the first bit arrival time of frames to be decoded to the + /// smoothing buffer. encoder_buffer_delay is measured in units of 1/90000 + /// seconds. + pub encoder_buffer_delay: u32, + /// If set, indicates that the smoothing buffer operates in low-delay mode + /// for operating point op. In low-delay mode late decode times and buffer + /// underflow are both permitted. If not set, indicates that the smoothing + /// buffer operates in strict mode, where buffer underflow is not allowed. + pub low_delay_mode_flag: bool, + /// If set, indicates that initial_display_delay_minus_1 is specified for + /// operating point i. If not set, indicates that + /// initial_display_delay_minus_1 is not specified for operating point i. + pub initial_display_delay_present_for_this_op: bool, + /// Plus 1 specifies, for operating point i, the number of decoded frames + /// that should be present in the buffer pool before the first presentable + /// frame is displayed. This will ensure that all presentable frames in the + /// sequence can be decoded at or before the time that they are scheduled + /// for display. If not signaled then initial_display_delay_minus_1\[ i \] = + /// BUFFER_POOL_MAX_SIZE - 1. + pub initial_display_delay_minus_1: u32, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TimingInfo { + /// The number of time units of a clock operating at the frequency + /// time_scale Hz that corresponds to one increment of a clock tick counter. + /// A display clock tick, in seconds, is equal to num_units_in_display_tick + /// divided by time_scale: + pub num_units_in_display_tick: u32, + /// The number of time units that pass in one second. + pub time_scale: u32, + /// If set, indicates that pictures should be displayed according to their + /// output order with the number of ticks between two consecutive pictures + /// (without dropping frames) specified by num_ticks_per_picture_minus_1 + + /// 1. If not set, indicates that the interval between two consecutive + /// pictures is not specified. + pub equal_picture_interval: bool, + /// Plus 1 specifies the number of clock ticks corresponding to output time + /// between two consecutive pictures in the output order. + pub num_ticks_per_picture_minus_1: u32, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct DecoderModelInfo { + /// Plus 1 specifies the length of the decoder_buffer_delay and the + /// encoder_buffer_delay syntax elements, in bits. + pub buffer_delay_length_minus_1: u8, + /// The number of time units of a decoding clock operating at the frequency + /// time_scale Hz that corresponds to one increment of a clock tick counter: + pub num_units_in_decoding_tick: u32, + /// Plus 1 specifies the length of the buffer_removal_time syntax element, + /// in bits. + pub buffer_removal_time_length_minus_1: u8, + /// Plus 1 specifies the length of the frame_presentation_time syntax + /// element, in bits. + pub frame_presentation_time_length_minus_1: u32, +} + +/// Defined by the “Color primaries” section of ISO/IEC 23091-4/ITU-T H.273 +/// See 6.4.2 +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum ColorPrimaries { + Bt709 = 1, + #[default] + Unspecified = 2, + Bt470M = 4, + Bt470bg = 5, + Bt601 = 6, + Smpte240 = 7, + GenericFilm = 8, + Bt2020 = 9, + Xyz = 10, + Smpte431 = 11, + Smpte432 = 12, + Ebu3213 = 22, +} + +impl TryFrom for ColorPrimaries { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 1 => Ok(ColorPrimaries::Bt709), + 2 => Ok(ColorPrimaries::Unspecified), + 4 => Ok(ColorPrimaries::Bt470M), + 5 => Ok(ColorPrimaries::Bt470bg), + 6 => Ok(ColorPrimaries::Bt601), + 7 => Ok(ColorPrimaries::Smpte240), + 8 => Ok(ColorPrimaries::GenericFilm), + 9 => Ok(ColorPrimaries::Bt2020), + 10 => Ok(ColorPrimaries::Xyz), + 11 => Ok(ColorPrimaries::Smpte431), + 12 => Ok(ColorPrimaries::Smpte432), + 22 => Ok(ColorPrimaries::Ebu3213), + _ => Err(format!("Invalid ColorPrimaries {}", value)), + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum TransferCharacteristics { + Reserved0 = 0, + Bt709 = 1, + #[default] + Unspecified = 2, + Reserved3 = 3, + Bt470m = 4, + Bt470bg = 5, + Bt601 = 6, + Smpte240 = 7, + Linear = 8, + Log100 = 9, + Log100Sqrt10 = 10, + Iec61966 = 11, + Bt1361 = 12, + Srgb = 13, + Bt202010Bit = 14, + Bt202012Bit = 15, + Smpte2084 = 16, + Smpte428 = 17, + Hlg = 18, +} + +impl TryFrom for TransferCharacteristics { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(TransferCharacteristics::Reserved0), + 1 => Ok(TransferCharacteristics::Bt709), + 2 => Ok(TransferCharacteristics::Unspecified), + 3 => Ok(TransferCharacteristics::Reserved3), + 4 => Ok(TransferCharacteristics::Bt470m), + 5 => Ok(TransferCharacteristics::Bt470bg), + 6 => Ok(TransferCharacteristics::Bt601), + 7 => Ok(TransferCharacteristics::Smpte240), + 8 => Ok(TransferCharacteristics::Linear), + 9 => Ok(TransferCharacteristics::Log100), + 10 => Ok(TransferCharacteristics::Log100Sqrt10), + 11 => Ok(TransferCharacteristics::Iec61966), + 12 => Ok(TransferCharacteristics::Bt1361), + 13 => Ok(TransferCharacteristics::Srgb), + 14 => Ok(TransferCharacteristics::Bt202010Bit), + 15 => Ok(TransferCharacteristics::Bt202012Bit), + 16 => Ok(TransferCharacteristics::Smpte2084), + 17 => Ok(TransferCharacteristics::Smpte428), + 18 => Ok(TransferCharacteristics::Hlg), + _ => Err(format!("Invalid TransferCharacteristics {}", value)), + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum BitDepth { + #[default] + Depth8 = 0, + Depth10 = 1, + Depth12 = 2, +} + +impl TryFrom for BitDepth { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(BitDepth::Depth8), + 1 => Ok(BitDepth::Depth10), + 2 => Ok(BitDepth::Depth12), + _ => Err(format!("Invalid BitDepth {}", value)), + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum MatrixCoefficients { + Identity = 0, + Bt709 = 1, + #[default] + Unspecified = 2, + Reserved3 = 3, + Fcc = 4, + Bt470bg = 5, + Bt601 = 6, + Smpte240 = 7, + Ycgco = 8, + Bt2020Ncl = 9, + Bt2020Cl = 10, + Smpte2085 = 11, + ChromaDerivedNcl = 12, + ChromaDerivedCl = 13, + Ictcp = 14, +} + +impl TryFrom for MatrixCoefficients { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(MatrixCoefficients::Identity), + 1 => Ok(MatrixCoefficients::Bt709), + 2 => Ok(MatrixCoefficients::Unspecified), + 3 => Ok(MatrixCoefficients::Reserved3), + 4 => Ok(MatrixCoefficients::Fcc), + 5 => Ok(MatrixCoefficients::Bt470bg), + 6 => Ok(MatrixCoefficients::Bt601), + 7 => Ok(MatrixCoefficients::Smpte240), + 8 => Ok(MatrixCoefficients::Ycgco), + 9 => Ok(MatrixCoefficients::Bt2020Ncl), + 10 => Ok(MatrixCoefficients::Bt2020Cl), + 11 => Ok(MatrixCoefficients::Smpte2085), + 12 => Ok(MatrixCoefficients::ChromaDerivedNcl), + 13 => Ok(MatrixCoefficients::ChromaDerivedCl), + 14 => Ok(MatrixCoefficients::Ictcp), + _ => Err(format!("Invalid MatrixCoefficients {}", value)), + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum ChromaSamplePosition { + #[default] + Unknown = 0, + Vertical = 1, + Colocated = 2, + Reserved = 3, +} + +impl TryFrom for ChromaSamplePosition { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(ChromaSamplePosition::Unknown), + 1 => Ok(ChromaSamplePosition::Vertical), + 2 => Ok(ChromaSamplePosition::Colocated), + 3 => Ok(ChromaSamplePosition::Reserved), + _ => Err(format!("Invalid ChromaSamplePosition {}", value)), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ColorConfig { + /// Syntax elements which, together with seq_profile, determine the bit + /// depth. + pub high_bitdepth: bool, + /// Syntax elements which, together with seq_profile, determine the bit + /// depth. + pub twelve_bit: bool, + /// If set, indicates that the video does not contain U and V color planes. + /// If not set, indicates that the video contains Y, U, and V color planes. + pub mono_chrome: bool, + /// If set, specifies that color_primaries, transfer_characteristics, and + /// matrix_coefficients are present. If not set, specifies that + /// color_primaries, transfer_characteristics and matrix_coefficients are + /// not present. + pub color_description_present_flag: bool, + /// Defined by the “Color primaries” section of ISO/IEC 23091-4/ITU-T H.273. + pub color_primaries: ColorPrimaries, + /// Defined by the “Transfer characteristics” section of ISO/IEC + /// 23091-4/ITU-T H.273. + pub transfer_characteristics: TransferCharacteristics, + /// Defined by the “Matrix coefficients” section of ISO/IEC 23091-4/ITU-T + /// H.273. + pub matrix_coefficients: MatrixCoefficients, + /// Binary value that is associated with the VideoFullRangeFlag variable + /// specified in ISO/IEC 23091-4/ITU- T H.273. color range equal to 0 shall + /// be referred to as the studio swing representation and color range equal + /// to 1 shall be referred to as the full swing representation for all + /// intents relating to this specification. + pub color_range: bool, + /// Specify the chroma subsampling format + pub subsampling_x: bool, + /// Specify the chroma subsampling format + pub subsampling_y: bool, + /// Specifies the sample position for subsampled streams + pub chroma_sample_position: ChromaSamplePosition, + /// If set, indicates that the U and V planes may have separate delta + /// quantizer values. If not set, indicates that the U and V planes will + /// share the same delta quantizer value. + pub separate_uv_delta_q: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SequenceHeaderObu { + /// The OBU header from the OBU that generated this sequence. + pub obu_header: ObuHeader, + /// Specifies the features that can be used in the coded video sequence. + pub seq_profile: Profile, + /// If set, specifies that the coded video sequence contains only one coded + /// frame. If not set, specifies that the coded video sequence contains one + /// or more coded frames. + pub still_picture: bool, + /// Specifies that the syntax elements not needed by a still picture are + /// omitted. + pub reduced_still_picture_header: bool, + /// Specifies the number of bits minus 1 used for transmitting the frame + /// width syntax elements. + pub frame_width_bits_minus_1: u8, + /// Specifies the number of bits minus 1 used for transmitting the frame + /// height syntax elements. + pub frame_height_bits_minus_1: u8, + /// Specifies the maximum frame width minus 1 for the frames represented by + /// this sequence header. + pub max_frame_width_minus_1: u16, + /// Specifies the maximum frame height minus 1 for the frames represented by + /// this sequence header. + pub max_frame_height_minus_1: u16, + /// Specifies whether frame id numbers are present in the coded video + /// sequence. + pub frame_id_numbers_present_flag: bool, + /// Specifies the number of bits minus 2 used to encode delta_frame_id + /// syntax elements. + pub delta_frame_id_length_minus_2: u32, + /// Used to calculate the number of bits used to encode the frame_id syntax + /// element. + pub additional_frame_id_length_minus_1: u32, + /// When set, indicates that superblocks contain 128x128 luma samples. When + /// not set, it indicates that superblocks contain 64x64 luma samples. (The + /// number of contained chroma samples depends on subsampling_x and + /// subsampling_y.) + pub use_128x128_superblock: bool, + /// When set, specifies that the use_filter_intra syntax element may be + /// present. When not set, specifies that the use_filter_intra syntax + /// element will not be present. + pub enable_filter_intra: bool, + /// Specifies whether the intra edge filtering process should be enabled. + pub enable_intra_edge_filter: bool, + /// When set, specifies that the mode info for inter blocks may contain the + /// syntax element interintra. If not set, specifies that the syntax element + /// interintra will not be present. + pub enable_interintra_compound: bool, + /// When set, specifies that the mode info for inter blocks may contain the + /// syntax element compound_type. When not set, specifies that the syntax + /// element compound_type will not be present. + pub enable_masked_compound: bool, + /// When set, indicates that the allow_warped_motion syntax element may be + /// present. When not set, indicates that the allow_warped_motion syntax + /// element will not be present. + pub enable_warped_motion: bool, + /// When set, indicates that tools based on the values of order hints may be + /// used. When not set, indicates that tools based on order hints are + /// disabled. + pub enable_order_hint: bool, + /// When set, indicates that the inter prediction filter type may be + /// specified independently in the horizontal and vertical directions. If + /// the flag is not set, only one filter type may be specified, which is + /// then used in both directions. + pub enable_dual_filter: bool, + /// If set, indicates that the distance weights process may be used for + /// inter prediction. + pub enable_jnt_comp: bool, + /// If set, indicates that the use_ref_frame_mvs syntax element may be + /// present. If not set, indicates that the use_ref_frame_mvs syntax element + /// will not be present. + pub enable_ref_frame_mvs: bool, + /// If not set, indicates that the seq_force_screen_content_tools syntax + /// element will be present. If set, indicates that + /// seq_force_screen_content_tools should be set equal to + /// SELECT_SCREEN_CONTENT_TOOLS. + pub seq_choose_screen_content_tools: bool, + /// Equal to SELECT_SCREEN_CONTENT_TOOLS indicates that the + /// allow_screen_content_tools syntax element will be present in the frame + /// header. Otherwise, seq_force_screen_content_tools contains the value for + /// allow_screen_content_tools. + pub seq_force_screen_content_tools: u32, + /// If not set, indicates that the seq_force_integer_mv syntax element will + /// be present. If set, indicates that seq_force_integer_mv should be set + /// equal to SELECT_INTEGER_MV. + pub seq_choose_integer_mv: bool, + /// Equal to SELECT_INTEGER_MV indicates that the force_integer_mv syntax + /// element will be present in the frame header (providing + /// allow_screen_content_tools is equal to 1). Otherwise, + /// seq_force_integer_mv contains the value for force_integer_mv. + pub seq_force_integer_mv: u32, + /// Used to compute OrderHintBits. + pub order_hint_bits_minus_1: i32, + /// Specifies the number of bits used for the order_hint syntax element. + pub order_hint_bits: i32, + /// If set, specifies that the use_superres syntax element will be present + /// in the uncompressed header. If not set, specifies that the use_superres + /// syntax element will not be present (instead use_superres will be set to + /// 0 in the uncompressed header without being read). + pub enable_superres: bool, + /// If set, specifies that cdef filtering may be enabled. If not set, + /// specifies that cdef filtering is disabled. + pub enable_cdef: bool, + /// If set, specifies that loop restoration filtering may be enabled. If + /// not set, specifies that loop restoration filtering is disabled. + pub enable_restoration: bool, + /// Specifies whether film grain parameters are present in the coded video + /// sequence. + pub film_grain_params_present: bool, + /// Indicates the number of operating points minus 1 present in the coded + /// video sequence. An operating point specifies which spatial and temporal + /// layers should be decoded. + pub operating_points_cnt_minus_1: u32, + /// The set of operating points. + pub operating_points: [OperatingPoint; MAX_NUM_OPERATING_POINTS], + /// Specifies whether decoder model information is present in the coded + /// video sequence. + pub decoder_model_info_present_flag: bool, + /// The decoder model info. + pub decoder_model_info: DecoderModelInfo, + /// Specifies whether initial display delay information is present in the + /// coded video sequence. + pub initial_display_delay_present_flag: bool, + /// Specifies whether timing info is present in the coded video sequence. + pub timing_info_present_flag: bool, + /// The timing info. + pub timing_info: TimingInfo, + /// The color config. + pub color_config: ColorConfig, + + /* CamelCase variables in the specification */ + pub bit_depth: BitDepth, + pub num_planes: u32, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct StreamInfo { + pub seq_header: Rc, + pub render_width: u32, + pub render_height: u32, +} + +/// A TemporalDelimiterOBU +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TemporalDelimiterObu { + pub obu_header: ObuHeader, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum InterpolationFilter { + #[default] + EightTap = 0, + EightTapSmooth = 1, + EightTapSharp = 2, + Bilinear = 3, + Switchable = 4, +} + +impl TryFrom for InterpolationFilter { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(InterpolationFilter::EightTap), + 1 => Ok(InterpolationFilter::EightTapSmooth), + 2 => Ok(InterpolationFilter::EightTapSharp), + 3 => Ok(InterpolationFilter::Bilinear), + 4 => Ok(InterpolationFilter::Switchable), + _ => Err(format!("Invalid InterpolationFilter {}", value)), + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum TxModes { + #[default] + Only4x4 = 0, + Largest = 1, + Select = 2, +} + +impl TryFrom for TxModes { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(TxModes::Only4x4), + 1 => Ok(TxModes::Largest), + 2 => Ok(TxModes::Select), + _ => Err(format!("Invalid TxModes {}", value)), + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum FrameRestorationType { + #[default] + None = 0, + Wiener = 1, + Sgrproj = 2, + Switchable = 3, +} + +impl TryFrom for FrameRestorationType { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(FrameRestorationType::None), + 1 => Ok(FrameRestorationType::Wiener), + 2 => Ok(FrameRestorationType::Sgrproj), + 3 => Ok(FrameRestorationType::Switchable), + _ => Err(format!("Invalid FrameRestorationType {}", value)), + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum ReferenceFrameType { + #[default] + Intra = 0, + Last = 1, + Last2 = 2, + Last3 = 3, + Golden = 4, + BwdRef = 5, + AltRef2 = 6, + AltRef = 7, +} + +impl TryFrom for ReferenceFrameType { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(ReferenceFrameType::Intra), + 1 => Ok(ReferenceFrameType::Last), + 2 => Ok(ReferenceFrameType::Last2), + 3 => Ok(ReferenceFrameType::Last3), + 4 => Ok(ReferenceFrameType::Golden), + 5 => Ok(ReferenceFrameType::BwdRef), + 6 => Ok(ReferenceFrameType::AltRef2), + 7 => Ok(ReferenceFrameType::AltRef), + _ => Err(format!("Invalid ReferenceFrameType {}", value)), + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum WarpModelType { + #[default] + Identity = 0, + Translation = 1, + RotZoom = 2, + Affine = 3, +} + +impl TryFrom for WarpModelType { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(WarpModelType::Identity), + 1 => Ok(WarpModelType::Translation), + 2 => Ok(WarpModelType::RotZoom), + 3 => Ok(WarpModelType::Affine), + _ => Err(format!("Invalid WarpModelType {}", value)), + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum FrameType { + #[default] + KeyFrame = 0, + InterFrame = 1, + IntraOnlyFrame = 2, + SwitchFrame = 3, +} + +impl TryFrom for FrameType { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(FrameType::KeyFrame), + 1 => Ok(FrameType::InterFrame), + 2 => Ok(FrameType::IntraOnlyFrame), + 3 => Ok(FrameType::SwitchFrame), + _ => Err(format!("Invalid FrameType {}", value)), + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum TxMode { + #[default] + Only4x4 = 0, + Largest = 1, + Select = 2, +} + +impl TryFrom for TxMode { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(TxMode::Only4x4), + 1 => Ok(TxMode::Largest), + 2 => Ok(TxMode::Select), + _ => Err(format!("Invalid TxMode {}", value)), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct FrameObu<'a> { + pub header: FrameHeaderObu, + pub tile_group: TileGroupObu<'a>, +} + +/// A FrameHeaderOBU +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct FrameHeaderObu { + /// The original OBU header. This may be from a FrameOBU or a FrameHeaderOBU + /// directly. + pub obu_header: ObuHeader, + /// If set, indicates the frame indexed by frame_to_show_map_idx is to be + /// output; If not set, indicates that further processing is required. + pub show_existing_frame: bool, + /// Specifies the frame to be output. It is only available if + /// show_existing_frame is set. + pub frame_to_show_map_idx: u8, + /// Specifies the length of the frame_presentation_time syntax element, in + /// bits. + pub frame_presentation_time: u32, + /// Provides the frame id number for the frame to output. + pub display_frame_id: u32, + /// Specifies the type of the frame + pub frame_type: FrameType, + /// If set, specifies that this frame should be immediately output once + /// decoded. If not set specifies that this frame should not be + /// immediately output. (It may be output later if a later uncompressed + /// header uses show_existing_frame is set). + pub show_frame: bool, + /// When set, specifies that the frame may be output using the + /// show_existing_frame mechanism. When not set, specifies that this frame + /// will not be output using the show_existing_frame mechanism. + pub showable_frame: bool, + /// If set, indicates that error resilient mode is enabled; + /// error_resilient_mode equal to 0 indicates that error resilient mode is + /// disabled. + pub error_resilient_mode: bool, + /// Specifies whether the CDF update in the symbol decoding process should + /// be disabled. + pub disable_cdf_update: bool, + /// When set, indicates that intra blocks may use palette encoding; When not + /// set, indicates that palette encoding is never used. + pub allow_screen_content_tools: u32, + /// If set, specifies that motion vectors will always be integers. If not + /// set, specifies that motion vectors can contain fractional bits. + pub force_integer_mv: u32, + /// Specifies the frame id number for the current frame. Frame id numbers + /// are additional information that do not affect the decoding process, but + /// provide decoders with a way of detecting missing reference frames so + /// that appropriate action can be taken. + pub current_frame_id: u32, + /// If not set, specifies that the frame size is equal to the size in the + /// sequence header. If set, specifies that the frame size will either be + /// specified as the size of one of the reference frames, or computed from + /// the frame_width_minus_1 and frame_height_minus_1 syntax elements. + pub frame_size_override_flag: bool, + /// Specifies OrderHintBits least significant bits of the expected output + /// order for this frame. + pub order_hint: u32, + /// Specifies which reference frame contains the CDF values and other state + /// that should be loaded at the start of the frame. + pub primary_ref_frame: u32, + /// If set, specifies that buffer_removal_time is present. If not set, + /// specifies that buffer_removal_time is not present. + pub buffer_removal_time_present_flag: bool, + /// Specifies the frame removal time in units of DecCT clock ticks counted + /// from the removal time of the last random access point for operating + /// point opNum. buffer_removal_time is signaled as a fixed length unsigned + /// integer with a length in bits given by + /// buffer_removal_time_length_minus_1 + 1. + pub buffer_removal_time: Vec, + /// Contains a bitmask that specifies which reference frame slots will be + /// updated with the current frame after it is decoded. + pub refresh_frame_flags: u32, + /// Specifies the expected output order hint for each reference frame. + pub ref_order_hint: [u32; NUM_REF_FRAMES], + /// If set, indicates that intra block copy may be used in this frame. If + /// not set indicates that intra block copy is not allowed in this frame. + pub allow_intrabc: bool, + /// If set, indicates that only two reference frames are explicitly + /// signaled. If not set, indicates that all reference frames are explicitly + /// signaled. + pub frame_refs_short_signaling: bool, + /// Specifies the reference frame to use for LAST_FRAME. + pub last_frame_idx: u8, + /// Specifies the reference frame to use for GOLDEN_FRAME. + pub gold_frame_idx: u8, + /// Specifies which reference frames are used by inter frames + pub ref_frame_idx: [u8; REFS_PER_FRAME], + /// If not set, specifies that motion vectors are specified to quarter pel + /// precision; If set, specifies that motion vectors are specified to eighth + /// pel precision. + pub allow_high_precision_mv: bool, + /// If not set, specifies that only the SIMPLE motion mode will be used. + pub is_motion_mode_switchable: bool, + /// If set, specifies that motion vector information from a previous frame + /// can be used when decoding the current frame. If not set, specifies that + /// this information will not be used. + pub use_ref_frame_mvs: bool, + /// If set, indicates that the end of frame CDF update is disabled; If not + /// set, indicates that the end of frame CDF update is enabled. + pub disable_frame_end_update_cdf: bool, + /// If set, indicates that the syntax element motion_mode may be present. + /// If not set, indicates that the syntax element motion_mode will not be + /// present + pub allow_warped_motion: bool, + /// If set, specifies that the frame is restricted to a reduced subset of + /// the full set of transform types. + pub reduced_tx_set: bool, + /// If not set, means that the render width and height are inferred from the + /// frame width and height. If set, means that the render width and height + /// are explicitly coded. + pub render_and_frame_size_different: bool, + /// If not set, indicates that no upscaling is needed. If set, indicates + /// that upscaling is needed. + pub use_superres: bool, + /// If set indicates that the filter selection is signaled at the block + /// level; If not set, indicates that the filter selection is signaled at + /// the frame level. + pub is_filter_switchable: bool, + /// The interpolation filter parameters. + pub interpolation_filter: InterpolationFilter, + /// The loop filter parameters. + pub loop_filter_params: LoopFilterParams, + /// The quantization parameters. + pub quantization_params: QuantizationParams, + /// The segmentation parameters. + pub segmentation_params: SegmentationParams, + /// The tile info. + pub tile_info: TileInfo, + /// The CDEF parameters. + pub cdef_params: CdefParams, + /// The loop restoration parameters. + pub loop_restoration_params: LoopRestorationParams, + /// Used to compute TxMode. + pub tx_mode_select: u32, + /// If set specifies that the syntax element skip_mode will be present. If + /// not set, specifies that skip_mode will not be used for this frame. + pub skip_mode_present: bool, + /// If set, specifies that the mode info for inter blocks contains the + /// syntax element comp_mode that indicates whether to use single or + /// compound reference prediction. If not set, specifies that all inter + /// blocks will use single prediction. + pub reference_select: bool, + /// The global motion parameters. + pub global_motion_params: GlobalMotionParams, + /// The film grain parameters. + pub film_grain_params: FilmGrainParams, + + /* CamelCase variables */ + pub superres_denom: u32, + pub frame_is_intra: bool, + pub order_hints: [u32; NUM_REF_FRAMES], + pub ref_frame_sign_bias: [bool; NUM_REF_FRAMES], + pub coded_lossless: bool, + pub all_lossless: bool, + pub lossless_array: [bool; MAX_SEGMENTS], + pub seg_qm_level: [[u32; MAX_SEGMENTS]; 3], + pub upscaled_width: u32, + pub frame_width: u32, + pub frame_height: u32, + pub render_width: u32, + pub render_height: u32, + pub tx_mode: TxMode, + pub skip_mode_frame: [u32; 2], + pub mi_cols: u32, + pub mi_rows: u32, + pub header_bytes: usize, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct LoopFilterParams { + /// An array containing loop filter strength values. Different loop filter + /// strength values from the array are used depending on the image plane + /// being filtered, and the edge direction (vertical or horizontal) being + /// filtered. + pub loop_filter_level: [u8; 4], + /// Indicates the sharpness level. The loop_filter_level and + /// loop_filter_sharpness together determine when a block edge is filtered, + /// and by how much the filtering can change the sample values. + pub loop_filter_sharpness: u8, + /// If set, means that the filter level depends on the mode and reference + /// frame used to predict a block. If not set, means that the filter level + /// does not depend on the mode and reference frame. + pub loop_filter_delta_enabled: bool, + /// If set, means that additional syntax elements are present that specify + /// which mode and reference frame deltas are to be updated. + /// loop_filter_delta_update equal to 0 means that these syntax elements are + /// not present. + pub loop_filter_delta_update: bool, + /// Contains the adjustment needed for the filter level based on the chosen + /// reference frame. If this syntax element is not present, it maintains + /// its previous value. + pub loop_filter_ref_deltas: [i8; TOTAL_REFS_PER_FRAME], + /// Contains the adjustment needed for the filter level based on the chosen + /// mode. If this syntax element is not present in the, it maintains its + /// previous value. + pub loop_filter_mode_deltas: [i8; 2], + /// Specifies whether loop filter delta values are present. + pub delta_lf_present: bool, + /// Specifies the left shift which should be applied to decoded loop filter + /// delta values. + pub delta_lf_res: u8, + /// If set, specifies that separate loop filter deltas are sent for + /// horizontal luma edges, vertical luma edges, the U edges, and the V + /// edges. If not set, specifies that the same loop filter delta is used for + /// all edges. + pub delta_lf_multi: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct QuantizationParams { + /// Indicates the base frame qindex. This is used for Y AC coefficients and + /// as the base value for the other quantizers. + pub base_q_idx: u32, + /// Indicates the base frame qindex. This is used for Y AC coefficients and + /// as the base value for the other quantizers. + pub diff_uv_delta: bool, + /// Specifies that the quantizer matrix will be used to compute quantizers. + pub using_qmatrix: bool, + /// Specifies the level in the quantizer matrix that should be used for luma + /// plane decoding. + pub qm_y: u32, + /// Specifies the level in the quantizer matrix that should be used for + /// chroma U plane decoding. + pub qm_u: u32, + /// Specifies the level in the quantizer matrix that should be used for + /// chroma V plane decoding. + pub qm_v: u32, + /// Specifies whether quantizer index delta values are present. + pub delta_q_present: bool, + /// Specifies the left shift which should be applied to decoded quantizer + /// index delta values. + pub delta_q_res: u32, + /// Same as DeltaQYDc + pub delta_q_y_dc: i32, + /// Same as DeltaQUDc + pub delta_q_u_dc: i32, + /// Same as DeltaQUAc + pub delta_q_u_ac: i32, + /// Same as DeltaQVDc + pub delta_q_v_dc: i32, + /// Same as DeltaQVAc + pub delta_q_v_ac: i32, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SegmentationParams { + /// If set, indicates that this frame makes use of the segmentation tool; If + /// not set, indicates that the frame does not use segmentation. + pub segmentation_enabled: bool, + /// If set, indicates that the segmentation map are updated during the + /// decoding of this frame. If not set, means that the segmentation map from + /// the previous frame is used. + pub segmentation_update_map: bool, + /// If set, indicates that the updates to the segmentation map are coded + /// relative to the existing segmentation map. If not set, indicates that + /// the new segmentation map is coded without reference to the existing + /// segmentation map. + pub segmentation_temporal_update: bool, + /// If set, indicates that new parameters are about to be specified for each + /// segment. If not set, indicates that the segmentation parameters should + /// keep their existing values. + pub segmentation_update_data: bool, + /// If not set, indicates that the corresponding feature is unused and has + /// value equal to 0. If set, indicates that the feature value is coded. + pub feature_enabled: [[bool; SEG_LVL_MAX]; MAX_SEGMENTS], + /// Specifies the feature data for a segment feature. + pub feature_data: [[i16; SEG_LVL_MAX]; MAX_SEGMENTS], + /// Same as SegIdPreSkip + pub seg_id_pre_skip: bool, + /// Same as LastActiveSegId + pub last_active_seg_id: u8, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TileInfo { + /// If set, means that the tiles are uniformly spaced across the frame. (In + /// other words, all tiles are the same size except for the ones at the + /// right and bottom edge which can be smaller.) If not set, means that the + /// tile sizes are coded. + pub uniform_tile_spacing_flag: bool, + /// Used to compute TileColsLog2. + pub increment_tile_rows_log2: u32, + /// Specifies the width of a tile minus 1 in units of superblocks. + pub width_in_sbs_minus_1: [u32; MAX_TILE_COLS], + /// Specifies the height of a tile minus 1 in units of superblocks. + pub height_in_sbs_minus_1: [u32; MAX_TILE_ROWS], + /// Specifies which tile to use for the CDF update + pub context_update_tile_id: u32, + /// An array specifying the start column (in units of 4x4 luma samples) for + /// each tile across the image. + pub mi_col_starts: [u32; MAX_TILE_COLS + 1], + /// An array specifying the start row (in units of 4x4 luma samples) for + /// each tile down the image. + pub mi_row_starts: [u32; MAX_TILE_ROWS + 1], + /// Specifies the base 2 logarithm of the desired number of tiles down the + /// frame. + pub tile_cols_log2: u32, + /// Specifies the number of tiles across the frame. + pub tile_cols: u32, + /// Specifies the base 2 logarithm of the desired number of tiles down the + /// frame. + pub tile_rows_log2: u32, + /// Secifies the number of tiles down the frame + pub tile_rows: u32, + /// Specifies the number of bytes needed to code each tile size. + pub tile_size_bytes: u32, +} + +impl Default for TileInfo { + fn default() -> Self { + Self { + uniform_tile_spacing_flag: Default::default(), + increment_tile_rows_log2: Default::default(), + width_in_sbs_minus_1: [0; MAX_TILE_COLS], + height_in_sbs_minus_1: [0; MAX_TILE_ROWS], + context_update_tile_id: Default::default(), + mi_col_starts: [0; MAX_TILE_COLS + 1], + mi_row_starts: [0; MAX_TILE_ROWS + 1], + tile_cols_log2: Default::default(), + tile_cols: Default::default(), + tile_rows_log2: Default::default(), + tile_rows: Default::default(), + tile_size_bytes: Default::default(), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct CdefParams { + /// Controls the amount of damping in the deringing filter. + pub cdef_damping: u32, + /// Specifies the number of bits needed to specify which CDEF filter to + /// apply. + pub cdef_bits: u32, + /// Specify the strength of the primary filter. + pub cdef_y_pri_strength: [u32; CDEF_MAX], + /// Specify the strength of the secondary filter. + pub cdef_y_sec_strength: [u32; CDEF_MAX], + /// Specify the strength of the primary filter. + pub cdef_uv_pri_strength: [u32; CDEF_MAX], + /// Specify the strength of the secondary filter. + pub cdef_uv_sec_strength: [u32; CDEF_MAX], +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct LoopRestorationParams { + /// Specifies if the luma restoration size should be halved. + pub lr_unit_shift: u8, + /// Only present for 4:2:0 formats and specifies if the chroma size should + /// be half the luma size. + pub lr_uv_shift: u8, + /// Same as FrameRestorationType in the specification. + pub frame_restoration_type: [FrameRestorationType; MAX_NUM_PLANES], + /// Same as LoopRestorationSize in the specification. + pub loop_restoration_size: [u16; MAX_NUM_PLANES], + /// Same as UsesLr in the specification. + pub uses_lr: bool, + /// Same as UsesChromaLr in the specification. + pub uses_chroma_lr: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct GlobalMotionParams { + /// Specifies whether global motion parameters are present for a particular + /// reference frame. + pub is_global: [bool; NUM_REF_FRAMES], + /// Specifies whether a particular reference frame uses rotation and zoom + /// global motion. + pub is_rot_zoom: [bool; NUM_REF_FRAMES], + /// Specifies whether a particular reference frame uses translation global + /// motion. + pub is_translation: [bool; NUM_REF_FRAMES], + /// gm_params\[ ref \]\[ j \] is set equal to SavedGmParams\[ + /// frame_to_show_map_idx \]\[ ref \]\[ j \] for ref = LAST_FRAME..ALTREF_FRAME, + /// for j = 0..5. + pub gm_params: [[i32; 6]; NUM_REF_FRAMES], + /// Whether the parameters are valid (see warpValid and section 7.11.3.6) + pub warp_valid: [bool; NUM_REF_FRAMES], + /// Same as GmType. + pub gm_type: [WarpModelType; NUM_REF_FRAMES], +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct FilmGrainParams { + /// If set, specifies that film grain should be added to this frame. If not + /// set, specifies that film grain should not be added. + pub apply_grain: bool, + /// Specifies the starting value for the pseudo-random numbers used during + /// film grain synthesis. + pub grain_seed: u16, + /// If set means that a new set of parameters should be sent. If not set, + /// means that the previous set of parameters should be used. + pub update_grain: bool, + /// Indicates which reference frame contains the film grain parameters to be + /// used for this frame. + pub film_grain_params_ref_idx: u8, + /// Specifies the number of points for the piece-wise linear scaling + /// function of the luma component. + pub num_y_points: u8, + /// Represents the x (luma value) coordinate for the i-th point of the + /// piecewise linear scaling function for luma component. The values are + /// signaled on the scale of 0..255. (In case of 10 bit video, these values + /// correspond to luma values divided by 4. In case of 12 bit video, these + /// values correspond to luma values divided by 16.) + pub point_y_value: [u8; MAX_NUM_Y_POINTS], + /// Pepresents the scaling (output) value for the i-th point of the + /// piecewise linear scaling function for luma component. + pub point_y_scaling: [u8; MAX_NUM_Y_POINTS], + /// Specifies that the chroma scaling is inferred from the luma scaling. + pub chroma_scaling_from_luma: bool, + /// Specifies the number of points for the piece-wise linear scaling + /// function of the cb component. + pub num_cb_points: u8, + /// Represents the x coordinate for the i-th point of the piece-wise linear + /// scaling function for cb component. The values are signaled on the scale + /// of 0..255. + pub point_cb_value: [u8; MAX_NUM_CB_POINTS], + /// Represents the scaling (output) value for the i-th point of the + /// piecewise linear scaling function for cb component. + pub point_cb_scaling: [u8; MAX_NUM_CB_POINTS], + /// Specifies represents the number of points for the piece-wise linear + /// scaling function of the cr component. + pub num_cr_points: u8, + /// Represents the x coordinate for the i-th point of the piece-wise linear + /// scaling function for cr component. The values are signaled on the scale + /// of 0..255. + pub point_cr_value: [u8; MAX_NUM_CR_POINTS], + /// Represents the scaling (output) value for the i-th point of the + /// piecewise linear scaling function for cr component. + pub point_cr_scaling: [u8; MAX_NUM_CR_POINTS], + /// Represents the shift – 8 applied to the values of the chroma component. + /// The grain_scaling_minus_8 can take values of 0..3 and determines the + /// range and quantization step of the standard deviation of film grain. + pub grain_scaling_minus_8: u8, + /// Specifies the number of auto-regressive coefficients for luma and chroma. + pub ar_coeff_lag: u32, + /// Specifies auto-regressive coefficients used for the Y plane. + pub ar_coeffs_y_plus_128: [u8; MAX_NUM_POS_LUMA], + /// Specifies auto-regressive coefficients used for the U plane. + pub ar_coeffs_cb_plus_128: [u8; MAX_NUM_POS_LUMA], + /// Specifies auto-regressive coefficients used for the V plane. + pub ar_coeffs_cr_plus_128: [u8; MAX_NUM_POS_LUMA], + /// Specifies the range of the auto-regressive coefficients. Values of 0, 1, + /// 2, and 3 correspond to the ranges for auto-regressive coefficients of + /// [-2, 2), [-1, 1), [-0.5, 0.5) and [-0.25, 0.25) respectively. + pub ar_coeff_shift_minus_6: u8, + /// Specifies how much the Gaussian random numbers should be scaled down + /// during the grain synthesis process. + pub grain_scale_shift: u8, + /// Represents a multiplier for the cb component used in derivation of the + /// input index to the cb component scaling function. + pub cb_mult: u8, + /// Represents a multiplier for the average luma component used in + /// derivation of the input index to the cb component scaling function. + pub cb_luma_mult: u8, + /// Represents an offset used in derivation of the input index to the cb + /// component scaling function. + pub cb_offset: u16, + /// Represents a multiplier for the cr component used in derivation of the + /// input index to the cr component scaling function. + pub cr_mult: u8, + /// Represents a multiplier for the average luma component used in + /// derivation of the input index to the cr component scaling function. + pub cr_luma_mult: u8, + /// Represents an offset used in derivation of the input index to the cr + /// component scaling function. + pub cr_offset: u16, + /// If set, indicates that the overlap between film grain blocks shall be + /// applied. If not set, indicates that the overlap between film grain + /// blocks shall not be applied. + pub overlap_flag: bool, + /// If set, indicates that clipping to the restricted (studio) range shall + /// be applied to the sample values after adding the film grain (see the + /// semantics for color_range for an explanation of studio swing). If not + /// set, indicates that clipping to the full range shall be applied to the + /// sample values after adding the film grain. + pub clip_to_restricted_range: bool, +} + +/// Keeps track of the state of the reference frames in the parser. All +/// variables are CamelCase. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct ReferenceFrameInfo { + /// An array which is indexed by a reference picture slot number. A value of + /// true in the array signifies that the corresponding reference picture + /// slot is valid for use as a reference picture, while a value of false + /// signifies that the corresponding reference picture slot is not valid for + /// use as a reference picture. + ref_valid: bool, + /// Specifies the frame id for each reference frame. + ref_frame_id: u32, + /// See 7.20 Reference Frame Update Process. + ref_upscaled_width: u32, + /// See 7.20 Reference Frame Update Process. + ref_frame_width: u32, + /// See 7.20 Reference Frame Update Process. + ref_frame_height: u32, + /// See 7.20 Reference Frame Update Process. + ref_render_width: u32, + /// See 7.20 Reference Frame Update Process. + ref_render_height: u32, + /// See 7.20 Reference Frame Update Process. + ref_mi_cols: u32, + /// See 7.20 Reference Frame Update Process. + ref_mi_rows: u32, + /// See 7.20 Reference Frame Update Process. + ref_frame_type: FrameType, + /// See 7.20 Reference Frame Update Process. + ref_subsampling_x: bool, + /// See 7.20 Reference Frame Update Process. + ref_subsampling_y: bool, + /// See 7.20 Reference Frame Update Process. + ref_bit_depth: BitDepth, + /// See 7.20 Reference Frame Update Process. + ref_order_hint: u32, + /// The saved segmentation parameters. + segmentation_params: SegmentationParams, + /// The saved global motion parameters. + global_motion_params: GlobalMotionParams, + /// The saved loop filter parameters. + loop_filter_params: LoopFilterParams, + /// The saved film grain parameters. + film_grain_params: FilmGrainParams, + /// The saved tile info parameters. + tile_info: TileInfo, + display_frame_id: u32, + showable_frame: bool, +} + +#[derive(Clone, Debug, Default)] +pub struct AnnexBState { + pub temporal_unit_size: u32, + pub frame_unit_size: u32, + pub temporal_unit_consumed: u32, + pub frame_unit_consumed: u32, +} + +#[derive(Clone, Debug)] +enum StreamFormat { + LowOverhead, + AnnexB(AnnexBState), +} + +#[derive(Debug)] +pub struct Parser { + stream_format: StreamFormat, + operating_point: u32, + /// Same as SeenFrameHeader in the specification + seen_frame_header: bool, + operating_point_idc: u16, + should_probe_for_annexb: bool, + is_first_frame: bool, + ref_info: [ReferenceFrameInfo; NUM_REF_FRAMES], + + /* CamelCase variables */ + mi_cols: u32, + mi_rows: u32, + prev_frame_id: u32, + current_frame_id: u32, + mi_col_starts: [u32; MAX_TILE_COLS + 1], + mi_row_starts: [u32; MAX_TILE_ROWS + 1], + tile_cols_log2: u32, + tile_cols: u32, + tile_rows_log2: u32, + tile_rows: u32, + tile_size_bytes: u32, + + /// We keep this to implement frame_header_copy() in the specification, and to fill in + /// StreamInfo render_width and render_height. + pub last_frame_header: Option, + /// The last SequenceHeaderObu parsed. + pub sequence_header: Option>, +} + +impl Parser { + /// Probes the input data for the Annex B format. Anything other than + /// Ok(true) refers to data in "low-overhead" format instead, as we are trying to parse + fn annexb_probe(data: &[u8]) -> Result { + let mut r = Reader::new(data); + let mut seen_sequence = false; + let mut seen_frame = false; + + // Try reading the first TU and frame unit size + let temporal_unit_size = r.read_leb128()?; + if temporal_unit_size == 0 { + return Ok(false); + } + + let frame_unit_size = r.read_leb128()?; + if frame_unit_size == 0 || frame_unit_size > temporal_unit_size { + return Ok(false); + } + + let obu_length = r.read_leb128()?; + if obu_length == 0 || obu_length > frame_unit_size { + return Ok(false); + } + + // The first OBU in the first frame_unit of each temporal_unit must + // be a temporal delimiter OBU (and this is the only place temporal + // delimiter OBUs can appear) + let header = Self::parse_obu_header(&mut r.clone())?; + if !matches!(header.obu_type, ObuType::TemporalDelimiter) { + return Ok(false); + } + + // Try identifying a sequence and a frame. + r.0.skip_bits(obu_length as usize * 8)?; + let mut num_bytes_read = 0; + + loop { + let obu_length = r.read_leb128()?; + let mut obu_reader = r.clone(); + + r.0.skip_bits(obu_length as usize * 8)?; + num_bytes_read += obu_length; + + if !seen_sequence { + let header = Self::parse_obu_header(&mut obu_reader)?; + seen_sequence = matches!(header.obu_type, ObuType::SequenceHeader); + } + + if !seen_frame { + let header = Self::parse_obu_header(&mut obu_reader)?; + seen_frame = matches!(header.obu_type, ObuType::Frame | ObuType::FrameHeader); + } + + if seen_sequence && seen_frame { + // OK, enough evidence of Annex B format. + return Ok(true); + } + + if num_bytes_read >= frame_unit_size { + // We read what we've identified as the first frame and yet no + // sequence and no actual frames were found. + return Ok(false); + } + } + } + + fn compute_image_size(&mut self, fh: &mut FrameHeaderObu) { + fh.mi_cols = 2 * ((fh.frame_width + 7) >> 3); + fh.mi_rows = 2 * ((fh.frame_height + 7) >> 3); + self.mi_cols = fh.mi_cols; + self.mi_rows = fh.mi_rows; + } + + // 5.9.8 + fn parse_superres_params( + fh: &mut FrameHeaderObu, + r: &mut Reader, + seq: &SequenceHeaderObu, + ) -> Result<(), String> { + if seq.enable_superres { + fh.use_superres = r.0.read_bit()?; + } else { + fh.use_superres = false; + } + + if fh.use_superres { + fh.superres_denom = + r.0.read_bits::(SUPERRES_DENOM_BITS)? + SUPERRES_DENOM_MIN as u32; + } else { + fh.superres_denom = SUPERRES_NUM as u32; + } + + fh.upscaled_width = fh.frame_width; + fh.frame_width = + (fh.upscaled_width * SUPERRES_NUM as u32 + (fh.superres_denom / 2)) / fh.superres_denom; + + Ok(()) + } + + // 7.8 verbatim. + fn set_frame_refs( + &self, + fh: &mut FrameHeaderObu, + ref_order_hint: &[u32; NUM_REF_FRAMES], + ) -> Result<(), String> { + let seq = self.sequence()?; + let mut ref_frame_idx = [-1i32; REFS_PER_FRAME]; + + ref_frame_idx[0] = fh.last_frame_idx.into(); + ref_frame_idx[ReferenceFrameType::Golden as usize - ReferenceFrameType::Last as usize] = + fh.gold_frame_idx.into(); + + let mut used_frame = [false; NUM_REF_FRAMES]; + used_frame[fh.last_frame_idx as usize] = true; + used_frame[fh.gold_frame_idx as usize] = true; + + let cur_frame_hint = 1 << (seq.order_hint_bits - 1); + let mut shifted_order_hints = [0; NUM_REF_FRAMES]; + for i in 0..NUM_REF_FRAMES { + shifted_order_hints[i] = cur_frame_hint + + helpers::get_relative_dist( + seq.enable_order_hint, + seq.order_hint_bits, + ref_order_hint[i].try_into().unwrap(), + fh.order_hint.try_into().unwrap(), + ); + } + + let mut latest_order_hint = shifted_order_hints[fh.last_frame_idx as usize]; + if latest_order_hint >= cur_frame_hint { + return Err("It is a requirement of bitstream conformance that last_order_hint < cur_frame_hint".into()); + } + + let mut earliest_order_hint = shifted_order_hints[fh.gold_frame_idx as usize]; + if earliest_order_hint >= cur_frame_hint { + return Err("It is a requirement of bitstream conformance that gold_order_hint < cur_frame_hint".into()); + } + + let ref_ = helpers::find_latest_backward( + &shifted_order_hints, + &used_frame, + cur_frame_hint, + &mut latest_order_hint, + ); + + if ref_ >= 0 { + ref_frame_idx + [ReferenceFrameType::AltRef as usize - ReferenceFrameType::Last as usize] = ref_; + used_frame[ref_ as usize] = true; + } + + let ref_ = helpers::find_earliest_backward( + &shifted_order_hints, + &used_frame, + cur_frame_hint, + &mut earliest_order_hint, + ); + + if ref_ >= 0 { + ref_frame_idx + [ReferenceFrameType::BwdRef as usize - ReferenceFrameType::Last as usize] = ref_; + used_frame[ref_ as usize] = true; + } + + let ref_ = helpers::find_earliest_backward( + &shifted_order_hints, + &used_frame, + cur_frame_hint, + &mut earliest_order_hint, + ); + + if ref_ >= 0 { + ref_frame_idx + [ReferenceFrameType::AltRef2 as usize - ReferenceFrameType::Last as usize] = ref_; + used_frame[ref_ as usize] = true; + } + + const REF_FRAME_LIST: [usize; 5] = [ + ReferenceFrameType::Last2 as usize - ReferenceFrameType::Last as usize, + ReferenceFrameType::Last3 as usize - ReferenceFrameType::Last as usize, + ReferenceFrameType::BwdRef as usize - ReferenceFrameType::Last as usize, + ReferenceFrameType::AltRef2 as usize - ReferenceFrameType::Last as usize, + ReferenceFrameType::AltRef as usize - ReferenceFrameType::Last as usize, + ]; + + #[allow(clippy::needless_range_loop)] + for i in 0..REFS_PER_FRAME - 2 { + let ref_frame = REF_FRAME_LIST[i]; + + if ref_frame_idx[ref_frame] < 0 { + let ref_ = helpers::find_latest_forward( + &shifted_order_hints, + &used_frame, + cur_frame_hint, + &mut latest_order_hint, + ); + + if ref_ >= 0 { + ref_frame_idx[ref_frame] = ref_; + used_frame[ref_ as usize] = true; + } + } + } + + let mut ref_ = 0; + earliest_order_hint = shifted_order_hints[0]; + #[allow(clippy::needless_range_loop)] + for i in 1..NUM_REF_FRAMES { + let hint = shifted_order_hints[i]; + if hint < earliest_order_hint { + ref_ = i as u8; + earliest_order_hint = hint; + } + } + + fh.ref_frame_idx + .iter_mut() + .zip(ref_frame_idx.iter().copied()) + .for_each(|(dest, src)| *dest = if src < 0 { ref_ } else { src as u8 }); + + Ok(()) + } + + // 5.9.5. + fn parse_frame_size(&mut self, fh: &mut FrameHeaderObu, r: &mut Reader) -> Result<(), String> { + let seq = self.sequence()?; + if fh.frame_size_override_flag { + let n = seq.frame_width_bits_minus_1 + 1; + fh.frame_width = r.0.read_bits::(n as usize)? + 1; + + let n = seq.frame_height_bits_minus_1 + 1; + fh.frame_height = r.0.read_bits::(n as usize)? + 1; + } else { + fh.frame_width = seq.max_frame_width_minus_1 as u32 + 1; + fh.frame_height = seq.max_frame_height_minus_1 as u32 + 1; + } + + Self::parse_superres_params(fh, r, seq)?; + self.compute_image_size(fh); + + Ok(()) + } + + fn parse_render_size(fh: &mut FrameHeaderObu, r: &mut Reader) -> Result<(), String> { + fh.render_and_frame_size_different = r.0.read_bit()?; + if fh.render_and_frame_size_different { + fh.render_width = r.0.read_bits::(16)? + 1; + fh.render_height = r.0.read_bits::(16)? + 1; + } else { + fh.render_width = fh.upscaled_width; + fh.render_height = fh.frame_height; + } + Ok(()) + } + + fn frame_size_with_refs( + &mut self, + fh: &mut FrameHeaderObu, + r: &mut Reader, + ) -> Result<(), String> { + let mut found_ref = false; + let seq = self.sequence()?; + + for i in 0..REFS_PER_FRAME { + found_ref = r.0.read_bit()?; + + if found_ref { + let rf = &self.ref_info[fh.ref_frame_idx[i] as usize]; + fh.upscaled_width = rf.ref_upscaled_width; + fh.frame_width = fh.upscaled_width; + fh.frame_height = rf.ref_frame_height; + fh.render_width = rf.ref_render_width; + fh.render_height = rf.ref_render_height; + break; + } + } + + if !found_ref { + self.parse_frame_size(fh, r)?; + Self::parse_render_size(fh, r)?; + } else { + Self::parse_superres_params(fh, r, seq)?; + self.compute_image_size(fh); + } + + Ok(()) + } + + /// Skip the padding bits, ensuring that they actually make sense. + fn skip_and_check_trailing_bits(r: &mut Reader, obu: &Obu) -> Result<(), String> { + // We can't have that in parse_obu as per the spec, because the reader + // is not initialized on our design at that point, so move the check to + // inside this function. + if obu.data.len() == 0 + || matches!( + obu.header.obu_type, + ObuType::TileList | ObuType::TileGroup | ObuType::Frame + ) + { + return Ok(()); + } + let num_trailing = obu.as_ref().len() as u64 * 8 - r.0.position(); + r.read_trailing_bits(num_trailing)?; + Ok(()) + } + + fn parse_obu_header(r: &mut Reader) -> Result { + let _obu_forbidden_bit = r.0.read_bit()?; + + let mut header = ObuHeader { + obu_type: ObuType::try_from(r.0.read_bits::(4)?)?, + extension_flag: r.0.read_bit()?, + has_size_field: r.0.read_bit()?, + temporal_id: Default::default(), + spatial_id: Default::default(), + }; + + let obu_reserved_1bit = r.0.read_bit()?; + assert!(!obu_reserved_1bit); // Must be set to zero as per spec. + + if header.extension_flag { + header.temporal_id = r.0.read_bits::(3)?; + header.spatial_id = r.0.read_bits::(2)?; + let _ = r.0.read_bits::(3)?; + } + + Ok(header) + } + + /// Parses one OBU from `data`, which can be in Annex B or low-overhead + /// format. + /// + /// `None` may eventually be returned if the OBU is to be dropped. + pub fn read_obu<'a>(&mut self, data: &'a [u8]) -> Result, String> { + if data.is_empty() { + return Err("Empty data".into()); + } + + let mut reader = Reader::new(data); + + if self.should_probe_for_annexb { + // Try probing for Annex B data. + self.stream_format = if matches!(Self::annexb_probe(data), Ok(true)) { + log::debug!("Parsing an Annex B stream"); + StreamFormat::AnnexB(AnnexBState::default()) + } else { + log::debug!("Parsing a low-overhead stream"); + StreamFormat::LowOverhead + }; + + self.should_probe_for_annexb = false; + } + + let obu_length: usize = if let StreamFormat::AnnexB(annexb_state) = &mut self.stream_format + { + // Read the length to skip to the start of the open_bitstream_unit() + // syntax element. + let obu_length = reader.current_annexb_obu_length(annexb_state)?; + match obu_length { + Some(length) => length, + None => return Ok(ObuAction::Drop(reader.consumed(0))), + } + } else { + 0 + }; + + let start_pos = reader.consumed(0); + + // Both "low-overhead" and Annex B are now at the same point, i.e.: a + // open_bitstream_unit() follows. + let header = Self::parse_obu_header(&mut reader)?; + if matches!(self.stream_format, StreamFormat::LowOverhead) { + assert!(header.has_size_field); + } + + let obu_size: usize = if header.has_size_field { + reader.read_leb128()? as usize + } else { + /* trap any bugs when computing the final length */ + obu_length + .checked_sub(1) + .ok_or::("obu_length must be greater than 0".into())? + .checked_sub(usize::from(header.extension_flag)) + .ok_or::("obu_length too short".into())? + }; + + let consumed = reader.consumed(start_pos); + + if let StreamFormat::AnnexB(annexb_state) = &mut self.stream_format { + annexb_state.temporal_unit_consumed += consumed; + annexb_state.frame_unit_consumed += consumed; + + annexb_state.temporal_unit_consumed += u32::try_from(obu_size).unwrap(); + annexb_state.frame_unit_consumed += u32::try_from(obu_size).unwrap(); + } + + assert!(reader.0.position() % 8 == 0); + let start_offset: usize = (reader.0.position() / 8).try_into().unwrap(); + + log::debug!( + "Identified OBU type {:?}, data size: {}, obu_size: {}", + header.obu_type, + start_offset + obu_size, + obu_size + ); + + if header.obu_type != ObuType::SequenceHeader + && header.obu_type != ObuType::TemporalDelimiter + && self.operating_point_idc != 0 + && header.extension_flag + { + let in_temporal_layer = ((self.operating_point_idc >> header.temporal_id) & 1) != 0; + let in_spatial_layer = ((self.operating_point_idc >> (header.spatial_id + 8)) & 1) != 0; + if !in_temporal_layer || !in_spatial_layer { + log::debug!("Dropping obu as per drop_obu() in the specification",); + return Ok(ObuAction::Drop(reader.consumed(0))); + } + } + + Ok(ObuAction::Process(Obu { + header, + data: Cow::from(&data[start_offset..start_offset + obu_size]), + bytes_used: start_offset + obu_size, + })) + } + + fn parse_color_config(s: &mut SequenceHeaderObu, r: &mut Reader) -> Result<(), String> { + let cc = &mut s.color_config; + + cc.high_bitdepth = r.0.read_bit()?; + if s.seq_profile as u32 == 2 && cc.high_bitdepth { + cc.twelve_bit = r.0.read_bit()?; + if cc.twelve_bit { + s.bit_depth = BitDepth::Depth12; + } else { + s.bit_depth = BitDepth::Depth10; + } + } else if s.seq_profile as u32 <= 2 { + s.bit_depth = if cc.high_bitdepth { + BitDepth::Depth10 + } else { + BitDepth::Depth8 + }; + } + + if s.seq_profile as u32 == 1 { + cc.mono_chrome = false; + } else { + cc.mono_chrome = r.0.read_bit()?; + } + + if cc.mono_chrome { + s.num_planes = 1; + } else { + s.num_planes = 3; + } + + cc.color_description_present_flag = r.0.read_bit()?; + if cc.color_description_present_flag { + cc.color_primaries = ColorPrimaries::try_from(r.0.read_bits::(8)?)?; + cc.transfer_characteristics = + TransferCharacteristics::try_from(r.0.read_bits::(8)?)?; + cc.matrix_coefficients = MatrixCoefficients::try_from(r.0.read_bits::(8)?)?; + } else { + cc.color_primaries = ColorPrimaries::Unspecified; + cc.transfer_characteristics = TransferCharacteristics::Unspecified; + cc.matrix_coefficients = MatrixCoefficients::Unspecified; + } + + if cc.mono_chrome { + cc.color_range = r.0.read_bit()?; + cc.subsampling_x = true; + cc.subsampling_y = true; + cc.chroma_sample_position = ChromaSamplePosition::Unknown; + cc.separate_uv_delta_q = false; + return Ok(()); + } else if matches!(cc.color_primaries, ColorPrimaries::Bt709) + && matches!(cc.transfer_characteristics, TransferCharacteristics::Srgb) + && matches!(cc.matrix_coefficients, MatrixCoefficients::Identity) + { + cc.color_range = true; + cc.subsampling_x = false; + cc.subsampling_y = false; + } else { + cc.color_range = r.0.read_bit()?; + if s.seq_profile as u32 == 0 { + cc.subsampling_x = true; + cc.subsampling_y = true; + } else if s.seq_profile as u32 == 1 { + cc.subsampling_x = false; + cc.subsampling_y = false; + } else if matches!(s.bit_depth, BitDepth::Depth12) { + cc.subsampling_x = r.0.read_bit()?; + if cc.subsampling_x { + cc.subsampling_y = r.0.read_bit()?; + } else { + cc.subsampling_y = false; + } + } else { + cc.subsampling_x = true; + cc.subsampling_y = false; + } + + if cc.subsampling_x && cc.subsampling_y { + cc.chroma_sample_position = + ChromaSamplePosition::try_from(r.0.read_bits::(2)?)?; + } + } + + cc.separate_uv_delta_q = r.0.read_bit()?; + + Ok(()) + } + + fn parse_operating_parameters_info( + opi: &mut OperatingPoint, + r: &mut Reader, + buffer_delay_length_minus_1: u8, + ) -> Result<(), String> { + let n = buffer_delay_length_minus_1 + 1; + opi.decoder_buffer_delay = r.0.read_bits::(n as usize)?; + opi.encoder_buffer_delay = r.0.read_bits::(n as usize)?; + opi.low_delay_mode_flag = r.0.read_bit()?; + Ok(()) + } + + fn parse_decoder_model_info(dmi: &mut DecoderModelInfo, r: &mut Reader) -> Result<(), String> { + dmi.buffer_delay_length_minus_1 = r.0.read_bits::(5)? as u8; + dmi.num_units_in_decoding_tick = r.0.read_bits::(32)?; + dmi.buffer_removal_time_length_minus_1 = r.0.read_bits::(5)? as u8; + dmi.frame_presentation_time_length_minus_1 = r.0.read_bits::(5)?; + Ok(()) + } + + fn parse_timing_info(ti: &mut TimingInfo, r: &mut Reader) -> Result<(), String> { + ti.num_units_in_display_tick = r.0.read_bits::(32)?; + ti.time_scale = r.0.read_bits::(32)?; + ti.equal_picture_interval = r.0.read_bit()?; + if ti.equal_picture_interval { + ti.num_ticks_per_picture_minus_1 = r.read_uvlc()?; + } + Ok(()) + } + + /// Selects an operating point. Only call this after the Sequence OBU for + /// which the operating point should apply has been parsed. + pub fn choose_operating_point(&mut self, operating_point: u32) -> Result<(), String> { + if operating_point > self.sequence()?.operating_points_cnt_minus_1 { + return Err(format!( + "Invalid operating point {} (max {})", + operating_point, + self.sequence()?.operating_points_cnt_minus_1 + )); + } + self.operating_point = operating_point; + self.operating_point_idc = self.sequence()?.operating_points[operating_point as usize].idc; + Ok(()) + } + + fn parse_temporal_delimiter_obu(&mut self) -> Result<(), String> { + self.seen_frame_header = false; + Ok(()) + } + + fn parse_sequence_header_obu(&mut self, obu: &Obu) -> Result, String> { + let mut s = SequenceHeaderObu { + obu_header: obu.header.clone(), + ..Default::default() + }; + + let mut r = Reader::new(obu.as_ref()); + let profile = r.0.read_bits::(3)?; + + s.seq_profile = Profile::try_from(profile)?; + s.still_picture = r.0.read_bit()?; + s.reduced_still_picture_header = r.0.read_bit()?; + + if s.reduced_still_picture_header { + /* Default::default() already ensures a lot of this, but lets go verbatim */ + s.timing_info_present_flag = false; + s.decoder_model_info_present_flag = false; + s.initial_display_delay_present_flag = false; + s.operating_points_cnt_minus_1 = 0; + s.operating_points[0].idc = 0; + s.operating_points[0].seq_level_idx = r.0.read_bits::(5)? as u8; + s.operating_points[0].seq_tier = 0; + s.operating_points[0].decoder_model_present_for_this_op = false; + s.operating_points[0].initial_display_delay_present_for_this_op = false; + } else { + s.timing_info_present_flag = r.0.read_bit()?; + if s.timing_info_present_flag { + Self::parse_timing_info(&mut s.timing_info, &mut r)?; + s.decoder_model_info_present_flag = r.0.read_bit()?; + if s.decoder_model_info_present_flag { + Self::parse_decoder_model_info(&mut s.decoder_model_info, &mut r)?; + } + } else { + s.decoder_model_info_present_flag = false; + } + + s.initial_display_delay_present_flag = r.0.read_bit()?; + s.operating_points_cnt_minus_1 = r.0.read_bits::(5)?; + if s.operating_points_cnt_minus_1 > MAX_NUM_OPERATING_POINTS as u32 { + return Err(format!( + "Invalid operating_points_cnt_minus_1 {}", + s.operating_points_cnt_minus_1 + )); + } + + for i in 0..=s.operating_points_cnt_minus_1 as usize { + s.operating_points[i].idc = r.0.read_bits::(12)? as u16; + s.operating_points[i].seq_level_idx = r.0.read_bits::(5)? as u8; + if s.operating_points[i].seq_level_idx > 7 { + s.operating_points[i].seq_tier = r.0.read_bit()? as u8; + } else { + s.operating_points[i].seq_tier = 0; + } + if s.decoder_model_info_present_flag { + s.operating_points[i].decoder_model_present_for_this_op = r.0.read_bit()?; + if s.operating_points[i].decoder_model_present_for_this_op { + let buffer_delay_length_minus_1 = + s.decoder_model_info.buffer_delay_length_minus_1; + Self::parse_operating_parameters_info( + &mut s.operating_points[i], + &mut r, + buffer_delay_length_minus_1, + )?; + } + } else { + s.operating_points[i].decoder_model_present_for_this_op = false; + } + + if s.initial_display_delay_present_flag { + s.operating_points[i].initial_display_delay_present_for_this_op = + r.0.read_bit()?; + if s.operating_points[i].initial_display_delay_present_for_this_op { + s.operating_points[i].initial_display_delay_minus_1 = + r.0.read_bits::(4)?; + } + } + } + } + + s.frame_width_bits_minus_1 = r.0.read_bits::(4)? as u8; + s.frame_height_bits_minus_1 = r.0.read_bits::(4)? as u8; + // frame_width_bits_minus_1 has been read from 4 bits, meaning we can read 16 bits at most. + s.max_frame_width_minus_1 = + r.0.read_bits::(s.frame_width_bits_minus_1 as usize + 1)? as u16; + // frame_height_bits_minus_1 has been read from 4 bits, meaning we can read 16 bits at most. + s.max_frame_height_minus_1 = + r.0.read_bits::(s.frame_height_bits_minus_1 as usize + 1)? as u16; + if s.reduced_still_picture_header { + s.frame_id_numbers_present_flag = false; + } else { + s.frame_id_numbers_present_flag = r.0.read_bit()?; + } + if s.frame_id_numbers_present_flag { + s.delta_frame_id_length_minus_2 = r.0.read_bits::(4)?; + s.additional_frame_id_length_minus_1 = r.0.read_bits::(3)?; + let frame_id_length = + s.additional_frame_id_length_minus_1 + s.delta_frame_id_length_minus_2 + 3; + if frame_id_length > 16 { + return Err(format!("Invalid frame_id_length {}", frame_id_length)); + } + } + + s.use_128x128_superblock = r.0.read_bit()?; + s.enable_filter_intra = r.0.read_bit()?; + s.enable_intra_edge_filter = r.0.read_bit()?; + if s.reduced_still_picture_header { + s.enable_interintra_compound = false; + s.enable_masked_compound = false; + s.enable_warped_motion = false; + s.enable_dual_filter = false; + s.enable_order_hint = false; + s.enable_jnt_comp = false; + s.enable_ref_frame_mvs = false; + s.seq_force_screen_content_tools = SELECT_SCREEN_CONTENT_TOOLS as _; + s.seq_force_integer_mv = SELECT_INTEGER_MV as _; + s.order_hint_bits = 0; + s.order_hint_bits_minus_1 = -1; + } else { + s.enable_interintra_compound = r.0.read_bit()?; + s.enable_masked_compound = r.0.read_bit()?; + s.enable_warped_motion = r.0.read_bit()?; + s.enable_dual_filter = r.0.read_bit()?; + s.enable_order_hint = r.0.read_bit()?; + if s.enable_order_hint { + s.enable_jnt_comp = r.0.read_bit()?; + s.enable_ref_frame_mvs = r.0.read_bit()?; + } else { + s.enable_jnt_comp = false; + s.enable_ref_frame_mvs = false; + } + s.seq_choose_screen_content_tools = r.0.read_bit()?; + if s.seq_choose_screen_content_tools { + s.seq_force_screen_content_tools = SELECT_SCREEN_CONTENT_TOOLS as _; + } else { + s.seq_force_screen_content_tools = r.0.read_bit()? as _; + } + if s.seq_force_screen_content_tools > 0 { + s.seq_choose_integer_mv = r.0.read_bit()?; + if s.seq_choose_integer_mv { + s.seq_force_integer_mv = SELECT_INTEGER_MV as _; + } else { + s.seq_force_integer_mv = r.0.read_bit()? as _; + } + } else { + s.seq_force_integer_mv = SELECT_INTEGER_MV as _; + } + + if s.enable_order_hint { + s.order_hint_bits_minus_1 = r.0.read_bits::(3)?.try_into().unwrap(); + s.order_hint_bits = s.order_hint_bits_minus_1 + 1; + } else { + s.order_hint_bits_minus_1 = -1; + s.order_hint_bits = 0; + } + } + + s.enable_superres = r.0.read_bit()?; + s.enable_cdef = r.0.read_bit()?; + s.enable_restoration = r.0.read_bit()?; + + Self::parse_color_config(&mut s, &mut r)?; + + s.film_grain_params_present = r.0.read_bit()?; + + Self::skip_and_check_trailing_bits(&mut r, obu)?; + let rc = Rc::new(s); + self.sequence_header = Some(rc.clone()); + + /* Client is supposed to set the operating point through external means, + * here we just set 0 as default. */ + self.choose_operating_point(0)?; + + Ok(rc) + } + + /// Implements 7.21. Note that 7.20 will use the information from the + /// header, so we must save them now, as they will not be parsed from the + /// bitstream. We also save some internal parser state which will be useful + /// later. + fn load_reference_frame(&self, fh: &mut FrameHeaderObu) -> Result<(), String> { + let rf = &self.ref_info[fh.frame_to_show_map_idx as usize]; + + // Section 6.8.1: It is a requirement of bitstream conformance that a + // sequence header OBU has been received before a frame header OBU. + let seq = self.sequence()?; + + /* at least save the sizes and for both kf and non-kf */ + fh.frame_type = rf.ref_frame_type; + fh.upscaled_width = rf.ref_upscaled_width; + fh.frame_width = rf.ref_frame_width; + fh.frame_height = rf.ref_frame_height; + fh.render_width = rf.ref_render_width; + fh.render_height = rf.ref_render_height; + + /* Save into the frame header */ + if fh.frame_type == FrameType::KeyFrame { + fh.current_frame_id = rf.ref_frame_id; + /* We don't keep track of sequence information at the frame level */ + fh.mi_cols = rf.ref_mi_cols; + fh.mi_rows = rf.ref_mi_rows; + /* The accelerator is keeping track of CDF values, so that is skipped too */ + fh.global_motion_params = rf.global_motion_params.clone(); + + if seq.film_grain_params_present { + fh.film_grain_params = rf.film_grain_params.clone(); + } + fh.loop_filter_params = rf.loop_filter_params.clone(); + fh.segmentation_params = rf.segmentation_params.clone(); + } + + Ok(()) + } + + fn setup_past_independence(fh: &mut FrameHeaderObu) { + fh.segmentation_params.feature_enabled = Default::default(); + fh.segmentation_params.feature_data = Default::default(); + + for i in ReferenceFrameType::Last as usize..ReferenceFrameType::AltRef as usize { + fh.global_motion_params.gm_type[i] = WarpModelType::Identity; + } + + fh.loop_filter_params.loop_filter_delta_enabled = true; + fh.loop_filter_params.loop_filter_ref_deltas = [1, 0, 0, 0, -1, 0, -1, -1]; + fh.loop_filter_params.loop_filter_mode_deltas = Default::default(); + } + + fn parse_tile_info(&mut self, r: &mut Reader, ti: &mut TileInfo) -> Result<(), String> { + let seq = self.sequence()?; + + let sb_cols = if seq.use_128x128_superblock { + (self.mi_cols + 31) >> 5 + } else { + (self.mi_cols + 15) >> 4 + }; + + let sb_rows = if seq.use_128x128_superblock { + (self.mi_rows + 31) >> 5 + } else { + (self.mi_rows + 15) >> 4 + }; + + let sb_shift = if seq.use_128x128_superblock { 5 } else { 4 }; + let sb_size = sb_shift + 2; + + let max_tile_width_sb = MAX_TILE_WIDTH >> sb_size; + let mut max_tile_area_sb = MAX_TILE_AREA >> (2 * sb_size); + + let min_log2_tile_cols = helpers::tile_log2(max_tile_width_sb, sb_cols); + + let max_log2_tile_cols = + helpers::tile_log2(1, std::cmp::min(sb_cols, MAX_TILE_COLS as u32)); + + let max_log2_tile_rows = + helpers::tile_log2(1, std::cmp::min(sb_rows, MAX_TILE_ROWS as u32)); + + let min_log2_tiles = std::cmp::max( + min_log2_tile_cols, + helpers::tile_log2(max_tile_area_sb, sb_rows * sb_cols), + ); + + ti.uniform_tile_spacing_flag = r.0.read_bit()?; + + if ti.uniform_tile_spacing_flag { + self.tile_cols_log2 = min_log2_tile_cols; + while self.tile_cols_log2 < max_log2_tile_cols { + let increment_tile_cols_log_2 = r.0.read_bit()?; + if increment_tile_cols_log_2 { + self.tile_cols_log2 += 1; + } else { + break; + } + } + + let tile_width_sb = (sb_cols + (1 << self.tile_cols_log2) - 1) >> self.tile_cols_log2; + + let mut i = 0; + let mut start_sb = 0; + + while start_sb < sb_cols { + self.mi_col_starts[i] = start_sb << sb_shift; + i += 1; + start_sb += tile_width_sb; + } + + self.mi_col_starts[i] = self.mi_cols; + self.tile_cols = i as _; + + if self.tile_cols > MAX_TILE_COLS as u32 { + return Err(format!("Invalid tile_cols {}", self.tile_cols)); + } + + /* compute this anyways */ + while i >= 1 { + ti.width_in_sbs_minus_1[i - 1] = + ((self.mi_col_starts[i] - self.mi_col_starts[i - 1] + ((1 << sb_shift) - 1)) + >> sb_shift) + - 1; + i -= 1; + } + + let min_log2_tile_rows = + std::cmp::max(min_log2_tiles.saturating_sub(self.tile_cols_log2), 0); + self.tile_rows_log2 = min_log2_tile_rows; + + while self.tile_rows_log2 < max_log2_tile_rows { + let increment_tile_rows_log_2 = r.0.read_bit()?; + + if increment_tile_rows_log_2 { + self.tile_rows_log2 += 1; + } else { + break; + } + } + + let tile_height_sb = (sb_rows + (1 << self.tile_rows_log2) - 1) >> self.tile_rows_log2; + + let mut i = 0; + let mut start_sb = 0; + + while start_sb < sb_rows { + self.mi_row_starts[i] = start_sb << sb_shift; + i += 1; + start_sb += tile_height_sb; + } + + self.mi_row_starts[i] = self.mi_rows; + self.tile_rows = i as _; + + if self.tile_rows > MAX_TILE_ROWS as u32 { + return Err(format!("Invalid tile_rows {}", self.tile_cols)); + } + + /* compute this anyways */ + while i >= 1 { + ti.height_in_sbs_minus_1[i - 1] = + ((self.mi_row_starts[i] - self.mi_row_starts[i - 1] + ((1 << sb_shift) - 1)) + >> sb_shift) + - 1; + i -= 1; + } + } else { + let mut widest_tile_sb = 0; + let mut start_sb = 0; + let mut i = 0; + + while start_sb < sb_cols { + self.mi_col_starts[i] = start_sb << sb_shift; + + let max_width = std::cmp::min(sb_cols - start_sb, max_tile_width_sb); + ti.width_in_sbs_minus_1[i] = r.read_ns(max_width.try_into().unwrap())?; + + let size_sb = ti.width_in_sbs_minus_1[i] + 1; + widest_tile_sb = std::cmp::max(size_sb, widest_tile_sb); + + start_sb += size_sb; + i += 1; + } + + self.mi_col_starts[i] = self.mi_cols; + self.tile_cols = i as _; + self.tile_cols_log2 = helpers::tile_log2(1, self.tile_cols); + + if min_log2_tiles > 0 { + max_tile_area_sb = (sb_rows * sb_cols) >> (min_log2_tiles + 1); + } else { + max_tile_area_sb = sb_rows * sb_cols; + } + + let max_tile_height_sb = std::cmp::max(max_tile_area_sb / widest_tile_sb, 1); + let mut start_sb = 0; + let mut i = 0; + while start_sb < sb_rows { + self.mi_row_starts[i] = start_sb << sb_shift; + let max_height = std::cmp::min(sb_rows - start_sb, max_tile_height_sb); + ti.height_in_sbs_minus_1[i] = r.read_ns(max_height.try_into().unwrap())?; + + let size_sb = ti.height_in_sbs_minus_1[i] + 1; + start_sb += size_sb; + i += 1; + } + + self.mi_row_starts[i] = self.mi_rows; + self.tile_rows = i as _; + self.tile_rows_log2 = helpers::tile_log2(1, self.tile_rows); + } + + if self.tile_cols_log2 > 0 || self.tile_rows_log2 > 0 { + let num_bits: usize = (self.tile_rows_log2 + self.tile_cols_log2) + .try_into() + .unwrap(); + ti.context_update_tile_id = r.0.read_bits::(num_bits)?; + + if ti.context_update_tile_id >= self.tile_rows * self.tile_cols { + return Err(format!( + "Invalid context_update_tile_id {}", + ti.context_update_tile_id + )); + } + self.tile_size_bytes = r.0.read_bits::(2)? + 1; + } else { + ti.context_update_tile_id = 0; + } + + ti.mi_col_starts = self.mi_col_starts; + ti.mi_row_starts = self.mi_row_starts; + ti.tile_cols_log2 = self.tile_cols_log2; + ti.tile_cols = self.tile_cols; + ti.tile_rows_log2 = self.tile_rows_log2; + ti.tile_rows = self.tile_rows; + ti.tile_size_bytes = self.tile_size_bytes; + + Ok(()) + } + + fn parse_quantization_params( + r: &mut Reader, + q: &mut QuantizationParams, + num_planes: u32, + separate_uv_delta_q: bool, + ) -> Result<(), String> { + q.base_q_idx = r.0.read_bits::(8)?; + q.delta_q_y_dc = r.read_delta_q()?; + if num_planes > 1 { + if separate_uv_delta_q { + q.diff_uv_delta = r.0.read_bit()?; + } else { + q.diff_uv_delta = false; + } + + q.delta_q_u_dc = r.read_delta_q()?; + q.delta_q_u_ac = r.read_delta_q()?; + if q.diff_uv_delta { + q.delta_q_v_dc = r.read_delta_q()?; + q.delta_q_v_ac = r.read_delta_q()?; + } else { + q.delta_q_v_dc = q.delta_q_u_dc; + q.delta_q_v_ac = q.delta_q_u_ac; + } + } else { + q.delta_q_u_dc = 0; + q.delta_q_u_ac = 0; + q.delta_q_v_dc = 0; + q.delta_q_v_ac = 0; + } + + q.using_qmatrix = r.0.read_bit()?; + if q.using_qmatrix { + q.qm_y = r.0.read_bits::(4)?; + q.qm_u = r.0.read_bits::(4)?; + if !separate_uv_delta_q { + q.qm_v = q.qm_u; + } else { + q.qm_v = r.0.read_bits::(4)?; + } + } + Ok(()) + } + + fn parse_delta_q_params(r: &mut Reader, q: &mut QuantizationParams) -> Result<(), String> { + q.delta_q_res = 0; + q.delta_q_present = false; + if q.base_q_idx > 0 { + q.delta_q_present = r.0.read_bit()?; + } + if q.delta_q_present { + q.delta_q_res = r.0.read_bits::(2)?; + } + + Ok(()) + } + + fn parse_delta_lf_params( + r: &mut Reader, + lf: &mut LoopFilterParams, + delta_q_present: bool, + allow_intrabc: bool, + ) -> Result<(), String> { + lf.delta_lf_present = false; + lf.delta_lf_res = 0; + lf.delta_lf_multi = false; + if delta_q_present { + if !allow_intrabc { + lf.delta_lf_present = r.0.read_bit()?; + } + if lf.delta_lf_present { + lf.delta_lf_res = r.0.read_bits::(2)? as u8; + lf.delta_lf_multi = r.0.read_bit()?; + } + } + Ok(()) + } + + fn parse_segmentation_params( + &self, + r: &mut Reader, + fh: &mut FrameHeaderObu, + ) -> Result<(), String> { + let s = &mut fh.segmentation_params; + s.segmentation_enabled = r.0.read_bit()?; + if s.segmentation_enabled { + if fh.primary_ref_frame == PRIMARY_REF_NONE { + s.segmentation_update_map = true; + s.segmentation_temporal_update = false; + s.segmentation_update_data = true; + } else { + s.segmentation_update_map = r.0.read_bit()?; + if s.segmentation_update_map { + s.segmentation_temporal_update = r.0.read_bit()?; + } + s.segmentation_update_data = r.0.read_bit()?; + } + if s.segmentation_update_data { + for i in 0..MAX_SEGMENTS { + for j in 0..SEG_LVL_MAX { + let feature_enabled = r.0.read_bit()?; + s.feature_enabled[i][j] = feature_enabled; + if feature_enabled { + let bits_to_read = FEATURE_BITS[j]; + let limit = FEATURE_MAX[j]; + let signed = FEATURE_SIGNED[j]; + + if signed { + let feature_value = r.read_su(1 + bits_to_read as usize)?; + let clipped_value = helpers::clip3(-limit, limit, feature_value); + s.feature_data[i][j] = clipped_value as _; + } else { + let feature_value = r.0.read_bits::(bits_to_read as usize)?; + let clipped_value = helpers::clip3( + 0, + limit, + feature_value + .try_into() + .map_err(|_| "Invalid feature_value")?, + ); + s.feature_data[i][j] = clipped_value as _; + } + } + } + } + } else { + /* copy from prev_frame */ + let prev_frame = + &self.ref_info[fh.ref_frame_idx[fh.primary_ref_frame as usize] as usize]; + + if !prev_frame.ref_valid { + return Err("Reference is invalid".into()); + } + + s.feature_enabled = prev_frame.segmentation_params.feature_enabled; + s.feature_data = prev_frame.segmentation_params.feature_data; + } + } else { + for i in 0..MAX_SEGMENTS { + for j in 0..SEG_LVL_MAX { + s.feature_enabled[i][j] = false; + s.feature_data[i][j] = 0; + } + } + } + + s.seg_id_pre_skip = false; + s.last_active_seg_id = 0; + for i in 0..MAX_SEGMENTS { + for j in 0..SEG_LVL_MAX { + if s.feature_enabled[i][j] { + s.last_active_seg_id = i as u8; + if j >= SEG_LVL_REF_FRAME { + s.seg_id_pre_skip = true; + } + } + } + } + + Ok(()) + } + + fn parse_loop_filter_parameters( + r: &mut Reader, + fh: &mut FrameHeaderObu, + num_planes: u32, + ) -> Result<(), String> { + let lf = &mut fh.loop_filter_params; + if fh.coded_lossless || fh.allow_intrabc { + lf.loop_filter_level[0] = 0; + lf.loop_filter_level[1] = 0; + lf.loop_filter_ref_deltas = [1, 0, 0, 0, -1, 0, -1, -1]; + + lf.loop_filter_mode_deltas = Default::default(); + + return Ok(()); + } + + lf.loop_filter_level[0] = r.0.read_bits::(6)? as u8; + lf.loop_filter_level[1] = r.0.read_bits::(6)? as u8; + if num_planes > 1 && (lf.loop_filter_level[0] > 0 || lf.loop_filter_level[1] > 0) { + lf.loop_filter_level[2] = r.0.read_bits::(6)? as u8; + lf.loop_filter_level[3] = r.0.read_bits::(6)? as u8; + } + + lf.loop_filter_sharpness = r.0.read_bits::(3)? as u8; + lf.loop_filter_delta_enabled = r.0.read_bit()?; + if lf.loop_filter_delta_enabled { + lf.loop_filter_delta_update = r.0.read_bit()?; + if lf.loop_filter_delta_update { + for i in 0..TOTAL_REFS_PER_FRAME { + let update_ref_delta = r.0.read_bit()?; + if update_ref_delta { + lf.loop_filter_ref_deltas[i] = r.read_su(7)? as i8; + } + } + + for i in 0..2 { + let update_mode_delta = r.0.read_bit()?; + if update_mode_delta { + lf.loop_filter_mode_deltas[i] = r.read_su(7)? as i8; + } + } + } + } + + Ok(()) + } + + fn parse_cdef_params( + r: &mut Reader, + fh: &mut FrameHeaderObu, + enable_cdef: bool, + num_planes: u32, + ) -> Result<(), String> { + let cdef = &mut fh.cdef_params; + + if fh.coded_lossless || fh.allow_intrabc || !enable_cdef { + cdef.cdef_bits = 0; + cdef.cdef_y_pri_strength[0] = 0; + cdef.cdef_y_sec_strength[0] = 0; + cdef.cdef_uv_pri_strength[0] = 0; + cdef.cdef_uv_sec_strength[0] = 0; + cdef.cdef_damping = 3; + return Ok(()); + } + + cdef.cdef_damping = r.0.read_bits::(2)? + 3; + cdef.cdef_bits = r.0.read_bits::(2)?; + for i in 0..(1 << cdef.cdef_bits) as usize { + cdef.cdef_y_pri_strength[i] = r.0.read_bits::(4)?; + cdef.cdef_y_sec_strength[i] = r.0.read_bits::(2)?; + if cdef.cdef_y_sec_strength[i] == 3 { + cdef.cdef_y_sec_strength[i] += 1; + } + if num_planes > 1 { + cdef.cdef_uv_pri_strength[i] = r.0.read_bits::(4)?; + cdef.cdef_uv_sec_strength[i] = r.0.read_bits::(2)?; + if cdef.cdef_uv_sec_strength[i] == 3 { + cdef.cdef_uv_sec_strength[i] += 1; + } + } + } + + Ok(()) + } + + fn parse_loop_restoration_params( + r: &mut Reader, + fh: &mut FrameHeaderObu, + enable_restoration: bool, + num_planes: u32, + use_128x128_superblock: bool, + subsampling_x: bool, + subsampling_y: bool, + ) -> Result<(), String> { + let lr = &mut fh.loop_restoration_params; + + if fh.all_lossless || fh.allow_intrabc || !enable_restoration { + lr.frame_restoration_type[0] = FrameRestorationType::None; + lr.frame_restoration_type[1] = FrameRestorationType::None; + lr.frame_restoration_type[2] = FrameRestorationType::None; + lr.uses_lr = false; + return Ok(()); + } + + lr.uses_lr = false; + lr.uses_chroma_lr = false; + + const REMAP_LR_TYPE: [FrameRestorationType; 4] = [ + FrameRestorationType::None, + FrameRestorationType::Switchable, + FrameRestorationType::Wiener, + FrameRestorationType::Sgrproj, + ]; + + for i in 0..num_planes as usize { + let lr_type = r.0.read_bits::(2)?; + lr.frame_restoration_type[i] = REMAP_LR_TYPE[lr_type as usize]; + if lr.frame_restoration_type[i] != FrameRestorationType::None { + lr.uses_lr = true; + if i > 0 { + lr.uses_chroma_lr = true; + } + } + } + + if lr.uses_lr { + if use_128x128_superblock { + lr.lr_unit_shift = r.0.read_bits::(1)? as u8 + 1; + } else { + lr.lr_unit_shift = r.0.read_bits::(1)? as u8; + if lr.lr_unit_shift > 0 { + lr.lr_unit_shift += r.0.read_bits::(1)? as u8; + } + } + + lr.loop_restoration_size[0] = RESTORATION_TILESIZE_MAX >> (2 - lr.lr_unit_shift); + if subsampling_x && subsampling_y && lr.uses_chroma_lr { + lr.lr_uv_shift = r.0.read_bits::(1)? as u8; + } else { + lr.lr_uv_shift = 0; + } + + lr.loop_restoration_size[1] = lr.loop_restoration_size[0] >> lr.lr_uv_shift; + lr.loop_restoration_size[2] = lr.loop_restoration_size[0] >> lr.lr_uv_shift; + } + + Ok(()) + } + + fn read_tx_mode(r: &mut Reader, fh: &mut FrameHeaderObu) -> Result<(), String> { + if fh.coded_lossless { + fh.tx_mode = TxMode::Only4x4; + } else { + let tx_mode_select = r.0.read_bit()?; + + if tx_mode_select { + fh.tx_mode = TxMode::Select; + } else { + fh.tx_mode = TxMode::Largest; + } + } + + Ok(()) + } + + fn parse_skip_mode_params( + &self, + r: &mut Reader, + fh: &mut FrameHeaderObu, + enable_order_hint: bool, + order_hint_bits: i32, + ) -> Result<(), String> { + let skip_mode_allowed; + + if fh.frame_is_intra || !fh.reference_select || !enable_order_hint { + skip_mode_allowed = false; + } else { + let mut forward_idx = -1; + let mut backward_idx = -1; + let mut forward_hint = 0; + let mut backward_hint = 0; + for i in 0..REFS_PER_FRAME { + let ref_hint = self.ref_info[fh.ref_frame_idx[i] as usize].ref_order_hint; + if helpers::get_relative_dist( + enable_order_hint, + order_hint_bits, + ref_hint.try_into().unwrap(), + fh.order_hint.try_into().unwrap(), + ) < 0 + && (forward_idx < 0 + || helpers::get_relative_dist( + enable_order_hint, + order_hint_bits, + ref_hint.try_into().unwrap(), + forward_hint, + ) > 0) + { + forward_idx = i32::try_from(i).unwrap(); + forward_hint = ref_hint.try_into().unwrap(); + } else if helpers::get_relative_dist( + enable_order_hint, + order_hint_bits, + ref_hint.try_into().unwrap(), + fh.order_hint.try_into().unwrap(), + ) > 0 + && (backward_idx < 0 || { + helpers::get_relative_dist( + enable_order_hint, + order_hint_bits, + ref_hint.try_into().unwrap(), + backward_hint, + ) < 0 + }) + { + backward_idx = i32::try_from(i).unwrap(); + backward_hint = ref_hint.try_into().unwrap(); + } + } + + if forward_idx < 0 { + skip_mode_allowed = false; + } else if backward_idx >= 0 { + skip_mode_allowed = true; + fh.skip_mode_frame[0] = ReferenceFrameType::Last as u32 + + u32::try_from(std::cmp::min(forward_idx, backward_idx)).unwrap(); + fh.skip_mode_frame[1] = ReferenceFrameType::Last as u32 + + u32::try_from(std::cmp::max(forward_idx, backward_idx)).unwrap(); + } else { + let mut second_forward_idx = -1; + let mut second_forward_hint = 0; + for i in 0..REFS_PER_FRAME { + let ref_hint = self.ref_info[fh.ref_frame_idx[i] as usize].ref_order_hint; + if helpers::get_relative_dist( + enable_order_hint, + order_hint_bits, + ref_hint.try_into().unwrap(), + forward_hint, + ) < 0 + && (second_forward_idx < 0 + || helpers::get_relative_dist( + enable_order_hint, + order_hint_bits, + ref_hint.try_into().unwrap(), + second_forward_hint, + ) > 0) + { + second_forward_idx = i32::try_from(i).unwrap(); + second_forward_hint = ref_hint.try_into().unwrap(); + } + } + + if second_forward_idx < 0 { + skip_mode_allowed = false; + } else { + skip_mode_allowed = true; + fh.skip_mode_frame[0] = ReferenceFrameType::Last as u32 + + u32::try_from(std::cmp::min(forward_idx, second_forward_idx)).unwrap(); + fh.skip_mode_frame[1] = ReferenceFrameType::Last as u32 + + u32::try_from(std::cmp::max(forward_idx, second_forward_idx)).unwrap(); + } + } + } + + if skip_mode_allowed { + fh.skip_mode_present = r.0.read_bit()?; + } else { + fh.skip_mode_present = false; + } + + Ok(()) + } + + fn parse_frame_reference_mode(r: &mut Reader, fh: &mut FrameHeaderObu) -> Result<(), String> { + if fh.frame_is_intra { + fh.reference_select = false; + } else { + fh.reference_select = r.0.read_bit()?; + } + Ok(()) + } + + fn seg_feature_active_idx(seg: &SegmentationParams, idx: u32, feature: u32) -> bool { + seg.segmentation_enabled && seg.feature_enabled[idx as usize][feature as usize] + } + + fn get_qindex(fh: &FrameHeaderObu, ignore_deltaq: bool, segment_id: u32) -> i32 { + let base_q_idx = i32::try_from(fh.quantization_params.base_q_idx).unwrap(); + if Self::seg_feature_active_idx(&fh.segmentation_params, segment_id, SEG_LVL_ALT_Q as u32) { + let data = fh.segmentation_params.feature_data[segment_id as usize][SEG_LVL_ALT_Q]; + let mut qindex = base_q_idx + i32::from(data); + if !ignore_deltaq && fh.quantization_params.delta_q_present { + qindex += i32::try_from(fh.quantization_params.delta_q_res).unwrap(); + } + helpers::clip3(0, 255, qindex) + } else { + base_q_idx + } + } + + fn setup_shear(warp_params: &[i32; 6]) -> Result { + let mut default = true; + for (i, param) in warp_params.iter().enumerate() { + let default_value = if i % 3 == 2 { + 1 << WARPEDMODEL_PREC_BITS + } else { + 0 + }; + if *param != default_value { + default = false; + break; + } + } + + /* assume the default params to be valid */ + if default { + return Ok(true); + } + + let alpha0 = helpers::clip3(-32768, 32767, warp_params[2] - (1 << WARPEDMODEL_PREC_BITS)); + let beta0 = helpers::clip3(-32768, 32767, warp_params[3]); + + let (div_shift, div_factor) = helpers::resolve_divisor(warp_params[2])?; + + let v = i64::from(warp_params[4] << WARPEDMODEL_PREC_BITS); + let v = (v * i64::from(div_factor)) as i32; + let gamma0 = helpers::clip3(-32678, 32767, helpers::round2signed(v, div_shift)?); + + let w = warp_params[3] * warp_params[4]; + + let delta0 = helpers::clip3( + -32768, + 32767, + warp_params[5] + - helpers::round2signed(w * div_factor, div_shift)? + - (1 << WARPEDMODEL_PREC_BITS), + ); + + let alpha = + helpers::round2signed(alpha0, WARP_PARAM_REDUCE_BITS)? << WARP_PARAM_REDUCE_BITS; + let beta = helpers::round2signed(beta0, WARP_PARAM_REDUCE_BITS)? << WARP_PARAM_REDUCE_BITS; + let gamma = + helpers::round2signed(gamma0, WARP_PARAM_REDUCE_BITS)? << WARP_PARAM_REDUCE_BITS; + let delta = + helpers::round2signed(delta0, WARP_PARAM_REDUCE_BITS)? << WARP_PARAM_REDUCE_BITS; + + #[allow(clippy::needless_bool)] + let warp_valid = if 4 * alpha.abs() + 7 * beta.abs() >= (1 << WARPEDMODEL_PREC_BITS) + || 4 * gamma.abs() + 4 * delta.abs() >= (1 << WARPEDMODEL_PREC_BITS) + { + false + } else { + true + }; + + Ok(warp_valid) + } + + fn read_global_param( + reader: &mut Reader, + type_: WarpModelType, + ref_frame: usize, + idx: usize, + allow_high_precision_mv: bool, + prev_gm_params: &[[i32; 6]; NUM_REF_FRAMES], + gm_params: &mut [[i32; 6]; NUM_REF_FRAMES], + ) -> Result<(), String> { + let mut abs_bits = GM_ABS_ALPHA_BITS; + let mut prec_bits = GM_ALPHA_PREC_BITS; + if idx < 2 { + if type_ == WarpModelType::Translation { + abs_bits = GM_ABS_TRANS_ONLY_BITS - !allow_high_precision_mv as u32; + prec_bits = GM_TRANS_ONLY_PREC_BITS - !allow_high_precision_mv as u32; + } else { + abs_bits = GM_ABS_TRANS_BITS; + prec_bits = GM_TRANS_PREC_BITS; + } + } + + let prec_diff = WARPEDMODEL_PREC_BITS - prec_bits; + + let (round, sub) = if (idx % 3) == 2 { + (1 << WARPEDMODEL_PREC_BITS, 1 << prec_bits) + } else { + (0, 0) + }; + + let mx = 1 << abs_bits; + let r = (prev_gm_params[ref_frame][idx] >> prec_diff) - sub; + gm_params[ref_frame][idx] = + (reader.decode_signed_subexp_with_ref(-mx, mx + 1, r)? << prec_diff) + round; + + Ok(()) + } + + fn parse_global_motion_params( + &mut self, + r: &mut Reader, + fh: &mut FrameHeaderObu, + ) -> Result<(), String> { + let gm = &mut fh.global_motion_params; + let mut type_; + let mut prev_gm_params: [[i32; 6]; NUM_REF_FRAMES] = Default::default(); + + for ref_frame in ReferenceFrameType::Last as usize..=ReferenceFrameType::AltRef as usize { + gm.gm_type[ref_frame] = WarpModelType::Identity; + for i in 0..6 { + gm.gm_params[ref_frame][i] = if i % 3 == 2 { + 1 << WARPEDMODEL_PREC_BITS + } else { + 0 + } + } + gm.warp_valid[ref_frame] = true; + } + + if fh.frame_is_intra { + return Ok(()); + } + + // Following libgav1: implement part of setup_past_independence() and + // load_previous(), i.e.: the parts that refer to the global motion + // parameters. + if fh.primary_ref_frame == PRIMARY_REF_NONE { + // setup_past_independence() + #[allow(clippy::needless_range_loop)] + for ref_frame in ReferenceFrameType::Last as usize..ReferenceFrameType::AltRef as usize + { + for i in 0..5 { + prev_gm_params[ref_frame][i] = if i % 3 == 2 { + 1 << WARPEDMODEL_PREC_BITS + } else { + 0 + } + } + } + } else { + // load_previous(): + // 1. The variable prevFrame is set equal to ref_frame_idx[ primary_ref_frame ]. + // 2. PrevGmParams is set equal to SavedGmParams[ prevFrame ]. + let prev_frame = fh.ref_frame_idx[fh.primary_ref_frame as usize]; + prev_gm_params = self.ref_info[prev_frame as usize] + .global_motion_params + .gm_params; + } + + for ref_frame in ReferenceFrameType::Last as usize..=ReferenceFrameType::AltRef as usize { + gm.is_global[ref_frame] = r.0.read_bit()?; + if gm.is_global[ref_frame] { + gm.is_rot_zoom[ref_frame] = r.0.read_bit()?; + if gm.is_rot_zoom[ref_frame] { + type_ = WarpModelType::RotZoom; + } else { + gm.is_translation[ref_frame] = r.0.read_bit()?; + if gm.is_translation[ref_frame] { + type_ = WarpModelType::Translation; + } else { + type_ = WarpModelType::Affine; + } + } + } else { + type_ = WarpModelType::Identity; + } + + gm.gm_type[ref_frame] = type_; + if gm.gm_type[ref_frame] as u32 >= WarpModelType::RotZoom as u32 { + Self::read_global_param( + r, + type_, + ref_frame, + 2, + fh.allow_high_precision_mv, + &prev_gm_params, + &mut gm.gm_params, + )?; + + Self::read_global_param( + r, + type_, + ref_frame, + 3, + fh.allow_high_precision_mv, + &prev_gm_params, + &mut gm.gm_params, + )?; + + if type_ == WarpModelType::Affine { + Self::read_global_param( + r, + type_, + ref_frame, + 4, + fh.allow_high_precision_mv, + &prev_gm_params, + &mut gm.gm_params, + )?; + + Self::read_global_param( + r, + type_, + ref_frame, + 5, + fh.allow_high_precision_mv, + &prev_gm_params, + &mut gm.gm_params, + )?; + } else { + gm.gm_params[ref_frame][4] = -gm.gm_params[ref_frame][3]; + gm.gm_params[ref_frame][5] = gm.gm_params[ref_frame][2]; + } + } + + if gm.gm_type[ref_frame] as u32 >= WarpModelType::Translation as u32 { + Self::read_global_param( + r, + type_, + ref_frame, + 0, + fh.allow_high_precision_mv, + &prev_gm_params, + &mut gm.gm_params, + )?; + + Self::read_global_param( + r, + type_, + ref_frame, + 1, + fh.allow_high_precision_mv, + &prev_gm_params, + &mut gm.gm_params, + )?; + } + + gm.warp_valid[ref_frame] = Self::setup_shear(&gm.gm_params[ref_frame])?; + } + + Ok(()) + } + + fn parse_film_grain_parameters( + &self, + r: &mut Reader, + fh: &mut FrameHeaderObu, + film_grain_params_present: bool, + mono_chrome: bool, + subsampling_x: bool, + subsampling_y: bool, + ) -> Result<(), String> { + let fg = &mut fh.film_grain_params; + + if !film_grain_params_present || (!fh.show_frame && !fh.showable_frame) { + *fg = Default::default(); + return Ok(()); + } + + fg.apply_grain = r.0.read_bit()?; + if !fg.apply_grain { + *fg = Default::default(); + return Ok(()); + } + + fg.grain_seed = r.0.read_bits::(16)? as u16; + if fh.frame_type == FrameType::InterFrame { + fg.update_grain = r.0.read_bit()?; + } else { + fg.update_grain = true; + } + + if !fg.update_grain { + fg.film_grain_params_ref_idx = r.0.read_bits::(3)? as u8; + let temp_grain_seed = fg.grain_seed; + + if !fh + .ref_frame_idx + .iter() + .any(|&ref_frame_idx| ref_frame_idx == fg.film_grain_params_ref_idx) + { + return Err("Invalid film_grain_params_ref_idx".into()); + } + + // load_grain_params() + *fg = self.ref_info[fg.film_grain_params_ref_idx as usize] + .film_grain_params + .clone(); + + fg.grain_seed = temp_grain_seed; + + return Ok(()); + } + + fg.num_y_points = r.0.read_bits::(4)? as u8; + fg.point_y_value + .iter_mut() + .zip(fg.point_y_scaling.iter_mut()) + .take(fg.num_y_points as usize) + .try_for_each(|(point_y_value, point_y_scaling)| { + *point_y_value = r.0.read_bits::(8)? as u8; + *point_y_scaling = r.0.read_bits::(8)? as u8; + Ok::<_, String>(()) + })?; + + if mono_chrome { + fg.chroma_scaling_from_luma = false; + } else { + fg.chroma_scaling_from_luma = r.0.read_bit()?; + } + + if mono_chrome + || fg.chroma_scaling_from_luma + || (subsampling_x && subsampling_y && fg.num_y_points == 0) + { + fg.num_cb_points = 0; + fg.num_cr_points = 0; + } else { + fg.num_cb_points = r.0.read_bits::(4)? as u8; + if fg.num_cb_points > 10 { + return Err(format!("Invalid num_cb_points {}", fg.num_cb_points)); + } + + for i in 0..fg.num_cb_points as usize { + fg.point_cb_value[i] = r.0.read_bits::(8)? as u8; + if i > 0 && fg.point_cb_value[i - 1] >= fg.point_cb_value[i] { + return Err(format!( + "Invalid point_cb_value[{}] {}", + i, fg.point_cb_value[i] + )); + } + fg.point_cb_scaling[i] = r.0.read_bits::(8)? as u8; + } + + fg.num_cr_points = r.0.read_bits::(4)? as u8; + for i in 0..fg.num_cr_points as usize { + fg.point_cr_value[i] = r.0.read_bits::(8)? as u8; + if i > 0 && fg.point_cr_value[i - 1] >= fg.point_cr_value[i] { + return Err(format!( + "Invalid point_cr_value[{}] {}", + i, fg.point_cr_value[i] + )); + } + fg.point_cr_scaling[i] = r.0.read_bits::(8)? as u8; + } + } + + fg.grain_scaling_minus_8 = r.0.read_bits::(2)? as u8; + fg.ar_coeff_lag = r.0.read_bits::(2)?; + + let num_pos_luma = 2 * fg.ar_coeff_lag * (fg.ar_coeff_lag + 1); + let num_pos_chroma = if fg.num_y_points > 0 { + for i in 0..num_pos_luma as usize { + fg.ar_coeffs_y_plus_128[i] = r.0.read_bits::(8)? as u8; + } + num_pos_luma + 1 + } else { + num_pos_luma + }; + + if fg.chroma_scaling_from_luma || fg.num_cb_points > 0 { + for i in 0..num_pos_chroma as usize { + fg.ar_coeffs_cb_plus_128[i] = r.0.read_bits::(8)? as u8; + } + } + + if fg.chroma_scaling_from_luma || fg.num_cr_points > 0 { + for i in 0..num_pos_chroma as usize { + fg.ar_coeffs_cr_plus_128[i] = r.0.read_bits::(8)? as u8; + } + } + + fg.ar_coeff_shift_minus_6 = r.0.read_bits::(2)? as u8; + fg.grain_scale_shift = r.0.read_bits::(2)? as u8; + + if fg.num_cb_points > 0 { + fg.cb_mult = r.0.read_bits::(8)? as u8; + fg.cb_luma_mult = r.0.read_bits::(8)? as u8; + fg.cb_offset = r.0.read_bits::(9)? as u16; + } + + if fg.num_cr_points > 0 { + fg.cr_mult = r.0.read_bits::(8)? as u8; + fg.cr_luma_mult = r.0.read_bits::(8)? as u8; + fg.cr_offset = r.0.read_bits::(9)? as u16; + } + + fg.overlap_flag = r.0.read_bit()?; + fg.clip_to_restricted_range = r.0.read_bit()?; + + Ok(()) + } + + fn sequence(&self) -> Result<&SequenceHeaderObu, String> { + let Some(seq) = self.sequence_header.as_ref() else { + return Err("No sequence header parsed yet".into()); + }; + + Ok(seq) + } + + fn parse_uncompressed_frame_header(&mut self, obu: &Obu) -> Result { + let mut r = Reader::new(obu.as_ref()); + + let mut fh = FrameHeaderObu { + obu_header: obu.header.clone(), + ..Default::default() + }; + + // Section 6.8.1: It is a requirement of bitstream conformance that a + // sequence header OBU has been received before a frame header OBU. + let &SequenceHeaderObu { + operating_points_cnt_minus_1, + seq_force_integer_mv, + additional_frame_id_length_minus_1, + delta_frame_id_length_minus_2, + decoder_model_info_present_flag, + reduced_still_picture_header, + frame_id_numbers_present_flag, + use_128x128_superblock, + enable_order_hint, + seq_force_screen_content_tools, + order_hint_bits, + enable_cdef, + enable_restoration, + enable_warped_motion, + color_config: + ColorConfig { + subsampling_x, + subsampling_y, + separate_uv_delta_q, + mono_chrome, + .. + }, + timing_info: + TimingInfo { + equal_picture_interval, + .. + }, + decoder_model_info: + DecoderModelInfo { + frame_presentation_time_length_minus_1, + buffer_removal_time_length_minus_1, + .. + }, + num_planes, + film_grain_params_present, + .. + } = self.sequence()?; + + let mut id_len = 0; + + if frame_id_numbers_present_flag { + id_len = additional_frame_id_length_minus_1 + delta_frame_id_length_minus_2 + 3; + } + + const ALL_FRAMES: u32 = (1 << NUM_REF_FRAMES) - 1; + + if reduced_still_picture_header { + fh.show_existing_frame = false; + fh.frame_type = FrameType::KeyFrame; + fh.frame_is_intra = true; + fh.show_frame = true; + fh.showable_frame = false; + } else { + fh.show_existing_frame = r.0.read_bit()?; + if matches!(obu.header.obu_type, ObuType::Frame) && fh.show_existing_frame { + return Err("If obu_type is equal to OBU_FRAME, it is a requirement of bitstream conformance that show_existing_frame is equal to 0.".into()); + } + if fh.show_existing_frame { + fh.frame_to_show_map_idx = r.0.read_bits::(3)? as u8; + + if decoder_model_info_present_flag && !equal_picture_interval { + fh.frame_presentation_time = + r.0.read_bits::(frame_presentation_time_length_minus_1 as usize + 1)?; + } + + let ref_frame = &self.ref_info[fh.frame_to_show_map_idx as usize]; + + fh.refresh_frame_flags = 0; + if frame_id_numbers_present_flag { + if id_len == 0 { + return Err(format!("Invalid id_len {}", id_len)); + } + fh.display_frame_id = r.0.read_bits::(id_len.try_into().unwrap())?; + if ref_frame.display_frame_id != fh.display_frame_id || !ref_frame.ref_valid { + return Err("Invalid display_frame_id".into()); + } + } + + if !ref_frame.showable_frame { + return Err("Invalid bitstream: can't show this past frame".into()); + } + + // In decode_frame_wrapup(): + // + // Otherwise (show_existing_frame is equal to 1), if frame_type + // is equal to KEY_FRAME, the reference frame loading process as + // specified in section 7.21 is invoked (this process loads + // frame state from the reference frames into the current frame + // state variables) + // + // The following ordered steps now apply: + // + // 1. The reference frame update process as specified in section + // 7.20 is invoked (this process saves the current frame state + // into the reference frames). + // + // 2. If show_frame is equal to 1 or show_existing_frame is + // equal to 1, the output process as specified in section 7.18 + // is invoked (this will output the current frame or a saved + // frame). + // + // We implement 1. here while 2. is left to the actual decoder + self.load_reference_frame(&mut fh)?; + if fh.frame_type == FrameType::KeyFrame { + fh.refresh_frame_flags = ALL_FRAMES; + } + + if film_grain_params_present { + // load_grain_params() + fh.film_grain_params = self.ref_info[fh.frame_to_show_map_idx as usize] + .film_grain_params + .clone(); + } + + // See 5.10. + if matches!(obu.header.obu_type, ObuType::Frame) { + r.byte_alignment()?; + } + + fh.header_bytes = usize::try_from(r.0.position() / 8).unwrap(); + return Ok(fh); + } + + fh.frame_type = FrameType::try_from(r.0.read_bits::(2)?)?; + fh.frame_is_intra = matches!( + fh.frame_type, + FrameType::IntraOnlyFrame | FrameType::KeyFrame + ); + + fh.show_frame = r.0.read_bit()?; + + if fh.show_frame && decoder_model_info_present_flag && equal_picture_interval { + fh.frame_presentation_time = + r.0.read_bits::(frame_presentation_time_length_minus_1 as usize + 1)?; + } + + if fh.show_frame { + fh.showable_frame = !matches!(fh.frame_type, FrameType::KeyFrame); + } else { + fh.showable_frame = r.0.read_bit()?; + } + + if fh.frame_type == FrameType::SwitchFrame + || (fh.frame_type == FrameType::KeyFrame && fh.show_frame) + { + fh.error_resilient_mode = true; + } else { + fh.error_resilient_mode = r.0.read_bit()?; + } + } + + if fh.frame_type == FrameType::KeyFrame && fh.show_frame { + for i in 0..NUM_REF_FRAMES { + self.ref_info[i].ref_valid = false; + self.ref_info[i].ref_order_hint = 0; + } + for i in 0..REFS_PER_FRAME { + fh.order_hints[ReferenceFrameType::Last as usize + i] = 0; + } + } + + fh.disable_cdf_update = r.0.read_bit()?; + if seq_force_screen_content_tools == SELECT_SCREEN_CONTENT_TOOLS as u32 { + fh.allow_screen_content_tools = r.0.read_bit()? as u32; + } else { + fh.allow_screen_content_tools = seq_force_screen_content_tools; + } + + if fh.allow_screen_content_tools > 0 { + if seq_force_integer_mv == SELECT_INTEGER_MV as u32 { + fh.force_integer_mv = r.0.read_bit()? as u32; + } else { + fh.force_integer_mv = seq_force_integer_mv; + } + } else { + fh.force_integer_mv = 0; + } + + if fh.frame_is_intra { + fh.force_integer_mv = 1; + } + + if frame_id_numbers_present_flag { + self.prev_frame_id = self.current_frame_id; + self.current_frame_id = r.0.read_bits::(id_len.try_into().unwrap())?; + fh.current_frame_id = self.current_frame_id; + + /* conformance checking, as per aom */ + let have_prev_frame_id = + !(self.is_first_frame || fh.frame_type == FrameType::KeyFrame && fh.show_frame); + + if have_prev_frame_id { + let frame_id_length = + additional_frame_id_length_minus_1 + delta_frame_id_length_minus_2 + 3; + + let diff_frame_id = if self.current_frame_id > self.prev_frame_id { + self.current_frame_id - self.prev_frame_id + } else { + if frame_id_length > 16 { + return Err(format!("Invalid frame_id_length {}", frame_id_length)); + } + (1 << frame_id_length) + self.current_frame_id - self.prev_frame_id + }; + + if self.prev_frame_id == self.current_frame_id + || diff_frame_id >= (1 << (frame_id_length - 1)) + { + return Err(format!( + "Invalid frame_id: prev_frame_id = {}, current_frame_id = {}", + self.prev_frame_id, self.current_frame_id + )); + } + } + + /* mark_ref_frames (idLen) */ + let diff_len = delta_frame_id_length_minus_2 + 2; + let shifted_diff_len = 1 << diff_len; + let shifted_id_len = 1 << id_len; + + for i in 0..NUM_REF_FRAMES { + if self.current_frame_id > shifted_diff_len { + if self.ref_info[i].ref_frame_id > self.current_frame_id + || self.ref_info[i].ref_frame_id + < (self.current_frame_id - shifted_diff_len) + { + self.ref_info[i].ref_valid = false; + } + } else if self.ref_info[i].ref_frame_id > self.current_frame_id + && self.ref_info[i].ref_frame_id + < shifted_id_len + self.current_frame_id - shifted_diff_len + { + self.ref_info[i].ref_valid = false; + } + } + } else { + self.current_frame_id = 0; + self.prev_frame_id = self.current_frame_id; + fh.current_frame_id = self.current_frame_id; + } + + if fh.frame_type == FrameType::SwitchFrame { + fh.frame_size_override_flag = true; + } else if reduced_still_picture_header { + fh.frame_size_override_flag = false; + } else { + fh.frame_size_override_flag = r.0.read_bit()?; + } + + fh.order_hint = r.0.read_bits::(order_hint_bits.try_into().unwrap())?; + + if fh.frame_is_intra || fh.error_resilient_mode { + fh.primary_ref_frame = PRIMARY_REF_NONE; + } else { + fh.primary_ref_frame = r.0.read_bits::(3)?; + } + + let operating_points = &self.sequence()?.operating_points; + if decoder_model_info_present_flag { + fh.buffer_removal_time_present_flag = r.0.read_bit()?; + if fh.buffer_removal_time_present_flag { + #[allow(clippy::needless_range_loop)] + for op_num in 0..=operating_points_cnt_minus_1 as usize { + if operating_points[op_num].decoder_model_present_for_this_op { + let op_pt_idc = operating_points[op_num].idc; + let in_temporal_layer = (op_pt_idc >> fh.obu_header.temporal_id) & 1 != 0; + let in_spatial_layer = + (op_pt_idc >> (fh.obu_header.spatial_id + 8)) & 1 != 0; + + if op_pt_idc == 0 || (in_temporal_layer && in_spatial_layer) { + let n = buffer_removal_time_length_minus_1 + 1; + fh.buffer_removal_time[op_num] = r.0.read_bits::(n as usize)?; + } + } + } + } + } + + fh.allow_high_precision_mv = false; + fh.use_ref_frame_mvs = false; + fh.allow_intrabc = false; + if fh.frame_type == FrameType::SwitchFrame + || (fh.frame_type == FrameType::KeyFrame && fh.show_frame) + { + fh.refresh_frame_flags = ALL_FRAMES; + } else { + fh.refresh_frame_flags = r.0.read_bits::(8)?; + } + + /* equivalent boolean expression */ + if (!fh.frame_is_intra || fh.refresh_frame_flags != ALL_FRAMES) + && fh.error_resilient_mode + && enable_order_hint + { + for i in 0..NUM_REF_FRAMES { + fh.ref_order_hint[i] = r.0.read_bits::(order_hint_bits.try_into().unwrap())?; + if fh.ref_order_hint[i] != self.ref_info[i].ref_order_hint { + self.ref_info[i].ref_valid = false; + } + } + } + + if fh.frame_is_intra { + self.parse_frame_size(&mut fh, &mut r)?; + Self::parse_render_size(&mut fh, &mut r)?; + if fh.allow_screen_content_tools > 0 && fh.upscaled_width == fh.frame_width { + fh.allow_intrabc = r.0.read_bit()?; + } + } else { + if !enable_order_hint { + fh.frame_refs_short_signaling = false; + } else { + fh.frame_refs_short_signaling = r.0.read_bit()?; + if fh.frame_refs_short_signaling { + fh.last_frame_idx = r.0.read_bits::(3)? as u8; + fh.gold_frame_idx = r.0.read_bits::(3)? as u8; + let ref_order_hints = self + .ref_info + .iter() + .map(|i| i.ref_order_hint) + .collect::>() + .try_into() + .unwrap(); + self.set_frame_refs(&mut fh, &ref_order_hints)?; + } + } + + let mut expected_frame_id = [0; REFS_PER_FRAME]; + #[allow(clippy::needless_range_loop)] + for i in 0..REFS_PER_FRAME { + if !fh.frame_refs_short_signaling { + fh.ref_frame_idx[i] = r.0.read_bits::(3)?.try_into().unwrap(); + } + + if frame_id_numbers_present_flag { + /* DeltaFrameId */ + let delta_frame_id = + r.0.read_bits::(delta_frame_id_length_minus_2 as usize + 2)? + 1; + + if id_len == 0 { + return Err(format!("Invalid id_len {}", id_len)); + } + + let shifted_id_len = 1 << id_len; + + expected_frame_id[i] = + (self.current_frame_id + shifted_id_len - delta_frame_id) % shifted_id_len; + + let actual_frame_id = self.ref_info[fh.ref_frame_idx[i] as usize].ref_frame_id; + + if expected_frame_id[i] != actual_frame_id { + return Err(format!( + "Invalid frame id, expected {} got {}", + expected_frame_id[i], actual_frame_id + )); + } + } + } + + if fh.frame_size_override_flag && !fh.error_resilient_mode { + self.frame_size_with_refs(&mut fh, &mut r)?; + } else { + self.parse_frame_size(&mut fh, &mut r)?; + Self::parse_render_size(&mut fh, &mut r)?; + } + + if fh.force_integer_mv > 0 { + fh.allow_high_precision_mv = false; + } else { + fh.allow_high_precision_mv = r.0.read_bit()?; + } + + /* read_interpolation_filter */ + fh.is_filter_switchable = r.0.read_bit()?; + if fh.is_filter_switchable { + fh.interpolation_filter = InterpolationFilter::Switchable; + } else { + fh.interpolation_filter = InterpolationFilter::try_from(r.0.read_bits::(2)?)?; + } + + fh.is_motion_mode_switchable = r.0.read_bit()?; + + if fh.error_resilient_mode || !self.sequence()?.enable_ref_frame_mvs { + fh.use_ref_frame_mvs = false; + } else { + fh.use_ref_frame_mvs = r.0.read_bit()?; + } + + for i in 0..REFS_PER_FRAME { + let ref_frame = ReferenceFrameType::Last as usize + i; + let hint = self.ref_info[fh.ref_frame_idx[i] as usize].ref_order_hint; + fh.order_hints[ref_frame] = hint; + + if !enable_order_hint { + fh.ref_frame_sign_bias[i] = false; + } else { + fh.ref_frame_sign_bias[i] = helpers::get_relative_dist( + enable_order_hint, + order_hint_bits, + hint.try_into().unwrap(), + fh.order_hint.try_into().unwrap(), + ) > 0; + } + } + } + + if reduced_still_picture_header || fh.disable_cdf_update { + fh.disable_frame_end_update_cdf = true; + } else { + fh.disable_frame_end_update_cdf = r.0.read_bit()?; + } + + if fh.primary_ref_frame == PRIMARY_REF_NONE { + Self::setup_past_independence(&mut fh); + } else { + /* load from the past reference */ + let prev_frame = + &self.ref_info[fh.ref_frame_idx[fh.primary_ref_frame as usize] as usize]; + + if !prev_frame.ref_valid { + return Err("Reference is invalid".into()); + } + + /* load_loop_filter_params: load ref_deltas and mode_deltas */ + fh.loop_filter_params.loop_filter_ref_deltas = + prev_frame.loop_filter_params.loop_filter_ref_deltas; + fh.loop_filter_params.loop_filter_mode_deltas = + prev_frame.loop_filter_params.loop_filter_mode_deltas; + + /* load_segmentation_params: load feature_enabled and feature_data */ + fh.segmentation_params.feature_enabled = prev_frame.segmentation_params.feature_enabled; + fh.segmentation_params.feature_data = prev_frame.segmentation_params.feature_data; + } + + // TODO: we can live without this for now. + // if fh.use_ref_frame_mvs { + // // motion_field_estimators() + // } + + self.parse_tile_info(&mut r, &mut fh.tile_info)?; + Self::parse_quantization_params( + &mut r, + &mut fh.quantization_params, + num_planes, + separate_uv_delta_q, + )?; + self.parse_segmentation_params(&mut r, &mut fh)?; + Self::parse_delta_q_params(&mut r, &mut fh.quantization_params)?; + Self::parse_delta_lf_params( + &mut r, + &mut fh.loop_filter_params, + fh.quantization_params.delta_q_present, + fh.allow_intrabc, + )?; + + fh.coded_lossless = true; + for segment_id in 0..MAX_SEGMENTS { + let q_index = Self::get_qindex(&fh, true, segment_id as _); + let q = &fh.quantization_params; + fh.lossless_array[segment_id] = q_index == 0 + && q.delta_q_y_dc == 0 + && q.delta_q_u_ac == 0 + && q.delta_q_u_dc == 0 + && q.delta_q_v_ac == 0 + && q.delta_q_v_dc == 0; + if !fh.lossless_array[segment_id] { + fh.coded_lossless = false; + } + if q.using_qmatrix { + if fh.lossless_array[segment_id] { + fh.seg_qm_level[0][segment_id] = 15; + fh.seg_qm_level[1][segment_id] = 15; + fh.seg_qm_level[2][segment_id] = 15; + } else { + fh.seg_qm_level[0][segment_id] = q.qm_y; + fh.seg_qm_level[1][segment_id] = q.qm_u; + fh.seg_qm_level[2][segment_id] = q.qm_v; + } + } + } + + fh.all_lossless = fh.coded_lossless && fh.frame_width == fh.upscaled_width; + Self::parse_loop_filter_parameters(&mut r, &mut fh, num_planes)?; + Self::parse_cdef_params(&mut r, &mut fh, enable_cdef, num_planes)?; + Self::parse_loop_restoration_params( + &mut r, + &mut fh, + enable_restoration, + num_planes, + use_128x128_superblock, + subsampling_x, + subsampling_y, + )?; + Self::read_tx_mode(&mut r, &mut fh)?; + Self::parse_frame_reference_mode(&mut r, &mut fh)?; + self.parse_skip_mode_params(&mut r, &mut fh, enable_order_hint, order_hint_bits)?; + + if fh.frame_is_intra || fh.error_resilient_mode || !enable_warped_motion { + fh.allow_warped_motion = false; + } else { + fh.allow_warped_motion = r.0.read_bit()?; + } + + fh.reduced_tx_set = r.0.read_bit()?; + self.parse_global_motion_params(&mut r, &mut fh)?; + self.parse_film_grain_parameters( + &mut r, + &mut fh, + film_grain_params_present, + mono_chrome, + subsampling_x, + subsampling_y, + )?; + + Self::skip_and_check_trailing_bits(&mut r, obu)?; + + // See 5.10 + if matches!(obu.header.obu_type, ObuType::Frame) { + r.byte_alignment()?; + } + + fh.header_bytes = usize::try_from(r.0.position() / 8).unwrap(); + Ok(fh) + } + + fn parse_tile_group_obu<'a>(&mut self, obu: Obu<'a>) -> Result, String> { + let mut tg = TileGroupObu { + obu, + ..Default::default() + }; + + let mut r = Reader::new(tg.obu.as_ref()); + + if r.0.num_bits_left() % 8 != 0 { + return Err("Bitstream is not byte aligned".into()); + } + + let mut sz: u64 = r.0.num_bits_left() as u64 / 8; + + let num_tiles = self.tile_rows * self.tile_cols; + let start_bit_pos = r.0.position(); + + if num_tiles > 1 { + tg.tile_start_and_end_present_flag = r.0.read_bit()?; + } + + if num_tiles == 1 || !tg.tile_start_and_end_present_flag { + tg.tg_start = 0; + tg.tg_end = num_tiles - 1; + } else { + let tile_bits = (self.tile_cols_log2 + self.tile_rows_log2) as usize; + tg.tg_start = r.0.read_bits::(tile_bits)?; + tg.tg_end = r.0.read_bits::(tile_bits)?; + } + + r.byte_alignment()?; + + let end_bit_pos = r.0.position(); + let header_bytes = (end_bit_pos - start_bit_pos) / 8; + sz -= header_bytes; + + let mut tile_num = tg.tg_start; + while tile_num <= tg.tg_end { + let tile_row = tile_num / self.tile_cols; + let tile_col = tile_num % self.tile_cols; + let last_tile = tile_num == tg.tg_end; + let tile_size; + + if last_tile { + tile_size = u32::try_from(sz).unwrap(); + } else { + tile_size = + r.0.read_le::(self.tile_size_bytes.try_into().unwrap())? + 1; + sz -= u64::from(tile_size + self.tile_size_bytes); + } + + let tile = Tile { + tile_offset: u32::try_from(r.0.position()).unwrap() / 8, + tile_size, + tile_row, + tile_col, + mi_row_start: self.mi_row_starts[tile_row as usize], + mi_row_end: self.mi_row_starts[tile_row as usize + 1], + mi_col_start: self.mi_row_starts[tile_col as usize], + mi_col_end: self.mi_row_starts[tile_col as usize + 1], + }; + + tg.tiles.push(tile); + + // init_symbol, decode_tile() and exit_symbol() left to the accelerator. + + // Skip the actual tile data + if tile_num < tg.tg_end { + r.0.skip_bits(tile_size as usize * 8)?; + } + + tile_num += 1; + } + + if tg.tg_end == num_tiles - 1 { + // left to the accelerator: + // if ( !disable_frame_end_update_cdf ) { + // frame_end_update_cdf( ) + // } + // decode_frame_wrapup( ) + self.seen_frame_header = false; + } + + Ok(tg) + } + + fn parse_frame_obu<'a>(&mut self, obu: Obu<'a>) -> Result, String> { + if !matches!(obu.header.obu_type, ObuType::Frame) { + return Err(format!( + "Expected a FrameOBU, got {:?}", + obu.header.obu_type + )); + } + + let frame_header_obu = self.parse_frame_header_obu(&obu)?; + let obu = Obu { + header: obu.header, + data: match obu.data { + Cow::Borrowed(d) => Cow::Borrowed(&d[frame_header_obu.header_bytes..]), + Cow::Owned(d) => Cow::Owned(d[frame_header_obu.header_bytes..].to_owned()), + }, + bytes_used: obu.bytes_used, + }; + let tile_group_obu = self.parse_tile_group_obu(obu)?; + + Ok(FrameObu { + header: frame_header_obu, + tile_group: tile_group_obu, + }) + } + + pub fn parse_frame_header_obu(&mut self, obu: &Obu) -> Result { + if !matches!(obu.header.obu_type, ObuType::FrameHeader | ObuType::Frame) { + return Err(format!( + "Expected a FrameHeaderOBU, got {:?}", + obu.header.obu_type + )); + } + + if self.seen_frame_header { + Ok(self + .last_frame_header + .clone() + .take() + .ok_or::("Broken stream: no previous frame header to copy".into())?) + } else { + self.seen_frame_header = true; + let header = self.parse_uncompressed_frame_header(obu)?; + if header.show_existing_frame { + self.last_frame_header = None; + self.seen_frame_header = false; + } else { + /* TileNum = 0 */ + self.seen_frame_header = true; + self.last_frame_header = Some(header.clone()); + } + + Ok(header) + } + } + + /// Implements 7.20. This function should be called right after decoding a + /// frame. + pub fn ref_frame_update(&mut self, fh: &FrameHeaderObu) -> Result<(), String> { + // This was found as a bug otherwise by Nicolas Dufresne in GStreamer's + // av1parse. + if fh.show_existing_frame && !matches!(fh.frame_type, FrameType::KeyFrame) { + return Ok(()); + } + + if matches!(fh.frame_type, FrameType::IntraOnlyFrame) && fh.refresh_frame_flags == 0xff { + return Err("Intra-only frames cannot refresh all of the DPB as per the spec.".into()); + } + + let &SequenceHeaderObu { + color_config: + ColorConfig { + subsampling_x, + subsampling_y, + .. + }, + film_grain_params_present, + bit_depth, + .. + } = self.sequence()?; + + for (i, ref_info) in self.ref_info.iter_mut().enumerate() { + if ((fh.refresh_frame_flags >> i) & 1) != 0 { + ref_info.ref_valid = true; + + ref_info.ref_frame_id = fh.current_frame_id; + ref_info.ref_frame_type = fh.frame_type; + ref_info.ref_upscaled_width = fh.upscaled_width; + ref_info.ref_frame_width = fh.frame_width; + ref_info.ref_frame_height = fh.frame_height; + ref_info.ref_render_width = fh.render_width; + ref_info.ref_render_height = fh.render_height; + ref_info.ref_order_hint = fh.order_hint; + ref_info.ref_mi_cols = self.mi_cols; + ref_info.ref_mi_rows = self.mi_rows; + ref_info.ref_subsampling_x = subsampling_x; + ref_info.ref_subsampling_y = subsampling_y; + ref_info.ref_bit_depth = bit_depth; + ref_info.segmentation_params = fh.segmentation_params.clone(); + ref_info.global_motion_params = fh.global_motion_params.clone(); + ref_info.loop_filter_params = fh.loop_filter_params.clone(); + ref_info.tile_info = fh.tile_info.clone(); + ref_info.display_frame_id = fh.display_frame_id; + ref_info.showable_frame = fh.showable_frame; + + if film_grain_params_present { + ref_info.film_grain_params = fh.film_grain_params.clone(); + } + } + } + + Ok(()) + } + + pub fn highest_operating_point(&self) -> Option { + if self.operating_point_idc == 0 { + /* No scalability information, all OBUs must be decoded */ + None + } else { + Some(helpers::floor_log2((self.operating_point_idc >> 8) as u32)) + } + } + + /// Fully parse an OBU. + pub fn parse_obu<'a>(&mut self, obu: Obu<'a>) -> Result, String> { + match obu.header.obu_type { + ObuType::Reserved => Ok(ParsedObu::Reserved), + ObuType::SequenceHeader => self + .parse_sequence_header_obu(&obu) + .map(ParsedObu::SequenceHeader), + ObuType::TemporalDelimiter => self + .parse_temporal_delimiter_obu() + .map(|_| ParsedObu::TemporalDelimiter), + ObuType::FrameHeader => self + .parse_frame_header_obu(&obu) + .map(ParsedObu::FrameHeader), + ObuType::TileGroup => self.parse_tile_group_obu(obu).map(ParsedObu::TileGroup), + ObuType::Metadata => Ok(ParsedObu::Metadata), + ObuType::Frame => self.parse_frame_obu(obu).map(ParsedObu::Frame), + ObuType::RedundantFrameHeader => Ok(ParsedObu::RedundantFrameHeader), + ObuType::TileList => Ok(ParsedObu::TileList), + ObuType::Reserved2 => Ok(ParsedObu::Reserved2), + ObuType::Reserved3 => Ok(ParsedObu::Reserved3), + ObuType::Reserved4 => Ok(ParsedObu::Reserved4), + ObuType::Reserved5 => Ok(ParsedObu::Reserved5), + ObuType::Reserved6 => Ok(ParsedObu::Reserved6), + ObuType::Reserved7 => Ok(ParsedObu::Reserved7), + ObuType::Padding => Ok(ParsedObu::Padding), + } + } +} + +impl Default for Parser { + fn default() -> Self { + Self { + stream_format: StreamFormat::LowOverhead, + operating_point: Default::default(), + seen_frame_header: Default::default(), + last_frame_header: Default::default(), + operating_point_idc: Default::default(), + should_probe_for_annexb: true, + is_first_frame: Default::default(), + mi_cols: Default::default(), + mi_rows: Default::default(), + prev_frame_id: Default::default(), + current_frame_id: Default::default(), + ref_info: Default::default(), + mi_col_starts: [0; MAX_TILE_COLS + 1], + mi_row_starts: [0; MAX_TILE_ROWS + 1], + tile_cols_log2: Default::default(), + tile_cols: Default::default(), + tile_rows_log2: Default::default(), + tile_rows: Default::default(), + tile_size_bytes: Default::default(), + sequence_header: Default::default(), + } + } +} + +impl Clone for Parser { + fn clone(&self) -> Self { + let sequence_header = self + .sequence_header + .as_ref() + .map(|s| Rc::new((**s).clone())); + + Self { + stream_format: self.stream_format.clone(), + operating_point: self.operating_point, + seen_frame_header: self.seen_frame_header, + last_frame_header: self.last_frame_header.clone(), + operating_point_idc: self.operating_point_idc, + should_probe_for_annexb: self.should_probe_for_annexb, + is_first_frame: self.is_first_frame, + ref_info: self.ref_info.clone(), + mi_cols: self.mi_cols, + mi_rows: self.mi_rows, + prev_frame_id: self.prev_frame_id, + current_frame_id: self.current_frame_id, + mi_col_starts: self.mi_col_starts, + mi_row_starts: self.mi_row_starts, + tile_cols_log2: self.tile_cols_log2, + tile_cols: self.tile_cols, + tile_rows_log2: self.tile_rows_log2, + tile_rows: self.tile_rows, + tile_size_bytes: self.tile_size_bytes, + sequence_header, + } + } +} + +#[cfg(test)] +mod tests { + use crate::bitstream_utils::IvfIterator; + use crate::codec::av1::parser::{ObuAction, Parser, StreamFormat}; + + use super::ObuType; + + /// Same as test-25fps.av1.ivf from Chromium + const STREAM_TEST_25_FPS: &[u8] = include_bytes!("test_data/test-25fps.ivf.av1"); + + /// Encoded with + /// + /// gst-launch-1.0 videotestsrc num-buffers=1 ! + /// video/x-raw,format=I420,width=64,height=64 ! filesink + /// location=aom_input.yuv + /// + /// And: + /// + /// aomenc -p 1 --ivf -w 64 -h 64 -o av1-annexb.ivf.av1 aom_input.yuv --annexb=1 + const STREAM_ANNEXB: &[u8] = include_bytes!("test_data/av1-annexb.ivf.av1"); + + #[test] + fn parse_test25fps() { + let mut parser = Parser::default(); + let ivf_iter = IvfIterator::new(STREAM_TEST_25_FPS); + let mut num_obus = 0; + + for packet in ivf_iter { + let mut consumed = 0; + + while let Ok(obu) = parser.read_obu(&packet[consumed..]) { + let obu = match obu { + ObuAction::Process(obu) => obu, + // This OBU should be dropped. + ObuAction::Drop(length) => { + consumed += usize::try_from(length).unwrap(); + continue; + } + }; + consumed += obu.bytes_used; + num_obus += 1; + } + } + + // Manually checked with GStreamer under GDB by using a hitcount on + // "gst_av1_parse_identify_one_obu" *after* the stream format has been + // detected. + assert_eq!(num_obus, 525); + } + + #[test] + /// Test that we can correctly identify streams in both "low-overhead" and + /// Annex B formats. + fn parse_annexb() { + let mut parser = Parser::default(); + let mut ivf_iter = IvfIterator::new(STREAM_TEST_25_FPS); + let packet = ivf_iter.next().unwrap(); + + parser.read_obu(packet).unwrap(); + assert!(matches!(parser.stream_format, StreamFormat::LowOverhead)); + + let mut parser = Parser::default(); + let mut ivf_iter = IvfIterator::new(STREAM_ANNEXB); + let packet = ivf_iter.next().unwrap(); + + parser.read_obu(packet).unwrap(); + assert!(matches!(parser.stream_format, StreamFormat::AnnexB { .. })); + } + + #[test] + /// Test that we can correctly identify streams in both "low-overhead" and + /// Annex B formats and identify all the OBUs in the stream until the end. + fn parse_annexb_full() { + let mut parser = Parser::default(); + let ivf_iter = IvfIterator::new(STREAM_TEST_25_FPS); + + for packet in ivf_iter { + let mut consumed = 0; + + while let Ok(obu) = parser.read_obu(&packet[consumed..]) { + let obu = match obu { + ObuAction::Process(obu) => obu, + // This OBU should be dropped. + ObuAction::Drop(length) => { + consumed += usize::try_from(length).unwrap(); + continue; + } + }; + assert!(matches!(parser.stream_format, StreamFormat::LowOverhead)); + consumed += obu.bytes_used; + } + } + + let mut parser = Parser::default(); + let ivf_iter = IvfIterator::new(STREAM_ANNEXB); + let mut num_obus = 0; + + for packet in ivf_iter { + let mut consumed = 0; + + while let Ok(obu) = parser.read_obu(&packet[consumed..]) { + let obu = match obu { + ObuAction::Process(obu) => obu, + // This OBU should be dropped. + ObuAction::Drop(length) => { + consumed += usize::try_from(length).unwrap(); + continue; + } + }; + assert!(matches!(parser.stream_format, StreamFormat::AnnexB { .. })); + consumed += obu.bytes_used; + num_obus += 1; + } + } + + assert_eq!(num_obus, 3); + let annexb_state = match parser.stream_format { + StreamFormat::AnnexB(annexb_state) => annexb_state, + _ => panic!("Wrong StreamFormat, expected AnnexB"), + }; + assert_eq!( + annexb_state.temporal_unit_consumed, + annexb_state.temporal_unit_size + ); + assert_eq!( + annexb_state.frame_unit_consumed, + annexb_state.frame_unit_size + ); + } + + #[test] + fn parse_test25fps_obus() { + let mut parser = Parser::default(); + let ivf_iter = IvfIterator::new(STREAM_TEST_25_FPS); + + for packet in ivf_iter { + let mut consumed = 0; + + while let Ok(obu) = parser.read_obu(&packet[consumed..]) { + let obu = match obu { + ObuAction::Process(obu) => obu, + // This OBU should be dropped. + ObuAction::Drop(length) => { + consumed += usize::try_from(length).unwrap(); + continue; + } + }; + + let data_len = obu.bytes_used; + + match obu.header.obu_type { + ObuType::SequenceHeader => { + parser.parse_sequence_header_obu(&obu).unwrap(); + } + ObuType::FrameHeader | ObuType::RedundantFrameHeader => { + let fh = parser.parse_frame_header_obu(&obu).unwrap(); + parser.ref_frame_update(&fh).unwrap(); + } + ObuType::TileGroup => { + parser.parse_tile_group_obu(obu).unwrap(); + } + ObuType::Frame => { + let frame = parser.parse_frame_obu(obu).unwrap(); + parser.ref_frame_update(&frame.header).unwrap(); + } + _ => {} + }; + + consumed += data_len; + } + } + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/reader.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/reader.rs new file mode 100644 index 00000000..7daccfab --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/reader.rs @@ -0,0 +1,251 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use crate::bitstream_utils::BitReader; + +use crate::codec::av1::helpers; + +use super::parser::AnnexBState; + +pub(crate) struct Reader<'a>(pub BitReader<'a>); + +impl<'a> Reader<'a> { + pub fn new(data: &'a [u8]) -> Self { + Self(BitReader::new(data, false)) + } + + /// Implements uvlc(): Variable length unsigned n-bit number appearing + /// directly in the bitstream. See 4.10.3 + pub fn read_uvlc(&mut self) -> Result { + let mut leading_zeroes = 0; + loop { + let done = self.0.read_bit()?; + + if done { + break; + } + + leading_zeroes += 1; + } + + if leading_zeroes >= 32 { + return Ok(u32::MAX); + } + + let value = self.0.read_bits::(leading_zeroes)?; + Ok(value + (1 << leading_zeroes) - 1) + } + + /// Implements leb128(): Unsigned integer represented by a variable number + /// of little-endian bytes. See 4.10.5 + pub fn read_leb128(&mut self) -> Result { + let mut value = 0u64; + + for i in 0..8 { + let byte = u64::from(self.0.read_bits_aligned::(8)?); + value |= (byte & 0x7f) << (i * 7); + + if byte & 0x80 == 0 { + break; + } + } + + Ok(value as u32) + } + + /// Implements su(n): Signed integer converted from an n bits unsigned + /// integer in the bitstream. (The unsigned integer corresponds to the + /// bottom n bits of the signed integer.). See 4.10.6 + pub fn read_su(&mut self, num_bits: usize) -> Result { + let mut value: i32 = self + .0 + .read_bits::(num_bits)? + .try_into() + .map_err(|_| String::from("Read more than 31 signed bits!"))?; + let sign_mask = 1 << (num_bits - 1); + + if (value & sign_mask) != 0 { + value -= 2 * sign_mask; + } + + Ok(value) + } + + /// Implements ns(n): Unsigned encoded integer with maximum number of values + /// n (i.e. output in range 0..n-1). See 4.10.7 + pub fn read_ns(&mut self, num_bits: usize) -> Result { + let w = helpers::floor_log2(num_bits as u32) + 1; + let m = (1 << w) - num_bits as u32; + let v = self.0.read_bits::( + usize::try_from(w).map_err(|_| String::from("Invalid num_bits"))? - 1, + )?; + + if v < m.into() { + return Ok(v); + } + + let extra_bit = self.0.read_bit()?; + Ok((v << 1) - u32::from(m) + u32::from(extra_bit)) + } + + /// Implements 5.9.13: Delta quantizer syntax. + pub fn read_delta_q(&mut self) -> Result { + let delta_coded = self.0.read_bit()?; + + if delta_coded { + self.read_su(7) + } else { + Ok(0) + } + } + + pub fn more_data_in_bitstream(&mut self) -> bool { + self.0.num_bits_left() > 0 + } + + pub(crate) fn consumed(&self, start_pos: u32) -> u32 { + (self.0.position() / 8) as u32 - start_pos + } + + /// Get the length of the current OBU in AnnexB format. + pub fn current_annexb_obu_length( + &mut self, + annexb_state: &mut AnnexBState, + ) -> Result, String> { + if !self.more_data_in_bitstream() { + return Ok(None); + } + + #[allow(clippy::comparison_chain)] + if annexb_state.temporal_unit_consumed == annexb_state.temporal_unit_size { + annexb_state.temporal_unit_size = 0; + } else if annexb_state.temporal_unit_consumed > annexb_state.temporal_unit_size { + return Err(format!( + "temporal_unit_size is {} but we consumed {} bytes", + annexb_state.temporal_unit_size, annexb_state.temporal_unit_consumed, + )); + } + + if annexb_state.temporal_unit_size == 0 { + annexb_state.temporal_unit_size = self.read_leb128()?; + if annexb_state.temporal_unit_size == 0 { + return Ok(None); + } + } + + let start_pos = self.consumed(0); + + #[allow(clippy::comparison_chain)] + if annexb_state.frame_unit_consumed == annexb_state.frame_unit_size { + annexb_state.frame_unit_size = 0; + } else if annexb_state.frame_unit_consumed > annexb_state.frame_unit_size { + return Err(format!( + "frame_unit_size is {} but we consumed {} bytes", + annexb_state.frame_unit_size, annexb_state.frame_unit_consumed, + )); + } + + if annexb_state.frame_unit_size == 0 { + annexb_state.frame_unit_size = self.read_leb128()?; + if annexb_state.frame_unit_size == 0 { + return Ok(None); + } + annexb_state.temporal_unit_consumed += self.consumed(start_pos); + } + + let start_pos = self.consumed(0); + let obu_length = self.read_leb128()?; + let consumed = self.consumed(start_pos); + + annexb_state.temporal_unit_consumed += consumed; + annexb_state.frame_unit_consumed += consumed; + + Ok(Some(obu_length.try_into().unwrap())) + } + + /// Implements 5.3.4. + pub fn read_trailing_bits(&mut self, mut num_bits: u64) -> Result<(), String> { + let trailing_one_bit = self.0.read_bit()?; + num_bits -= 1; + + if !trailing_one_bit { + return Err("bad padding: trailing_one_bit is not set".into()); + } + + while num_bits > 0 { + let trailing_zero_bit = self.0.read_bit()?; + if trailing_zero_bit { + return Err("bad padding: trailing_zero_bit is set".into()); + } + num_bits -= 1; + } + + Ok(()) + } + + fn decode_subexp(&mut self, num_syms: i32) -> Result { + let mut i = 0; + let mut mk = 0; + let k = 3; + + loop { + let b2 = if i != 0 { k + i - 1 } else { k }; + let a = 1 << b2; + if num_syms <= mk + 3 * a { + let num_bits = num_syms - mk; + let subexp_final_bits = self.read_ns(num_bits as usize)?; + return Ok(subexp_final_bits); + } else { + let subexp_more_bits = self.0.read_bit()?; + if subexp_more_bits { + i += 1; + mk += a; + } else { + let num_bits = b2 as usize; + let subexp_bits = self.0.read_bits::(num_bits)?; + return Ok(subexp_bits + mk as u32); + } + } + } + } + + /// Implements 5.9.27. + pub fn decode_unsigned_subexp_with_ref(&mut self, mx: i32, r: i32) -> Result { + let v = self.decode_subexp(mx)?; + if (r << 1) <= mx { + Ok(helpers::inverse_recenter(r, v.try_into().unwrap()) + .try_into() + .unwrap()) + } else { + let res = mx - 1 - helpers::inverse_recenter(mx - 1 - r, v.try_into().unwrap()); + Ok(res.try_into().unwrap()) + } + } + + /// Implements 5.9.26. + pub fn decode_signed_subexp_with_ref( + &mut self, + low: i32, + high: i32, + r: i32, + ) -> Result { + let x = self.decode_unsigned_subexp_with_ref(high - low, r - low)?; + Ok(i32::try_from(x).unwrap() + low) + } + + /// Implements 5.3.5 Byte alignment syntax + pub fn byte_alignment(&mut self) -> Result<(), String> { + while (self.0.position() & 7) != 0 { + self.0.read_bit()?; + } + + Ok(()) + } +} + +impl<'a> Clone for Reader<'a> { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/synthesizer.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/synthesizer.rs new file mode 100644 index 00000000..4faf7346 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/synthesizer.rs @@ -0,0 +1,1825 @@ +// Copyright 2024 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::fmt; +use std::io::Write; +use std::num::TryFromIntError; + +use crate::codec::av1::helpers::clip3; +use crate::codec::av1::parser::BitDepth; +use crate::codec::av1::parser::ChromaSamplePosition; +use crate::codec::av1::parser::ColorPrimaries; +use crate::codec::av1::parser::FrameHeaderObu; +use crate::codec::av1::parser::FrameRestorationType; +use crate::codec::av1::parser::FrameType; +use crate::codec::av1::parser::InterpolationFilter; +use crate::codec::av1::parser::MatrixCoefficients; +use crate::codec::av1::parser::ObuHeader; +use crate::codec::av1::parser::ObuType; +use crate::codec::av1::parser::Profile; +use crate::codec::av1::parser::ReferenceFrameType; +use crate::codec::av1::parser::SequenceHeaderObu; +use crate::codec::av1::parser::TemporalDelimiterObu; +use crate::codec::av1::parser::TransferCharacteristics; +use crate::codec::av1::parser::TxMode; +use crate::codec::av1::parser::WarpModelType; +use crate::codec::av1::parser::FEATURE_BITS; +use crate::codec::av1::parser::FEATURE_MAX; +use crate::codec::av1::parser::FEATURE_SIGNED; +use crate::codec::av1::parser::MAX_NUM_OPERATING_POINTS; +use crate::codec::av1::parser::MAX_NUM_PLANES; +use crate::codec::av1::parser::MAX_SEGMENTS; +use crate::codec::av1::parser::NUM_REF_FRAMES; +use crate::codec::av1::parser::PRIMARY_REF_NONE; +use crate::codec::av1::parser::REFS_PER_FRAME; +use crate::codec::av1::parser::SEG_LVL_MAX; +use crate::codec::av1::parser::SELECT_INTEGER_MV; +use crate::codec::av1::parser::SELECT_SCREEN_CONTENT_TOOLS; +use crate::codec::av1::parser::SUPERRES_DENOM_BITS; +use crate::codec::av1::parser::SUPERRES_DENOM_MIN; +use crate::codec::av1::parser::SUPERRES_NUM; +use crate::codec::av1::parser::TOTAL_REFS_PER_FRAME; +use crate::codec::av1::writer::ObuWriter; +use crate::codec::av1::writer::ObuWriterError; + +mod private { + pub trait ObuStruct {} +} + +impl private::ObuStruct for SequenceHeaderObu {} + +impl private::ObuStruct for TemporalDelimiterObu {} + +impl private::ObuStruct for FrameHeaderObu {} + +#[derive(Debug)] +pub enum SynthesizerError { + Unsupported, + InvalidSyntaxElementValue(&'static str), + ConversionError(TryFromIntError), + ObuWriter(ObuWriterError), + Io(std::io::Error), +} + +impl fmt::Display for SynthesizerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SynthesizerError::Unsupported => write!(f, "tried to synthesize unsupported settings"), + SynthesizerError::InvalidSyntaxElementValue(x) => { + write!(f, "invalid syntax element for value {}", x) + } + SynthesizerError::ConversionError(x) => write!(f, "{}", x.to_string()), + SynthesizerError::ObuWriter(x) => write!(f, "{}", x.to_string()), + SynthesizerError::Io(x) => write!(f, "{}", x.to_string()), + } + } +} + +impl From for SynthesizerError { + fn from(err: TryFromIntError) -> Self { + SynthesizerError::ConversionError(err) + } +} + +impl From for SynthesizerError { + fn from(err: ObuWriterError) -> Self { + SynthesizerError::ObuWriter(err) + } +} + +impl From for SynthesizerError { + fn from(err: std::io::Error) -> Self { + SynthesizerError::Io(err) + } +} + +pub type SynthesizerResult = Result; + +pub struct Synthesizer<'o, O: private::ObuStruct, W: Write> { + writer: ObuWriter, + obu: &'o O, +} + +impl<'o, O, W> Synthesizer<'o, O, W> +where + O: private::ObuStruct, + W: Write, +{ + fn new(writer: W, obu: &'o O) -> Self { + Self { + writer: ObuWriter::new(writer), + obu, + } + } + + fn f>(&mut self, bits: usize, value: T) -> SynthesizerResult<()> { + let value: u32 = value.into(); + self.writer.write_f(bits, value)?; + Ok(()) + } + + fn leb128>(&mut self, value: T) -> SynthesizerResult<()> { + let value: u32 = value.into(); + self.writer.write_leb128(value, 0)?; + Ok(()) + } + + fn uvlc>(&mut self, value: T) -> SynthesizerResult<()> { + self.writer.write_uvlc(value)?; + Ok(()) + } + + fn su>(&mut self, bits: usize, value: T) -> SynthesizerResult<()> { + self.writer.write_su(bits, value)?; + Ok(()) + } + + #[cfg(any(test, debug_assertions))] + fn invalid_element_value(&mut self, element: &'static str) -> SynthesizerResult<()> { + Err(SynthesizerError::InvalidSyntaxElementValue(element)) + } + + #[cfg(not(any(test, debug_assertions)))] + fn invalid_element_value(&mut self, element: &'static str) -> SynthesizerResult<()> { + log::error!("Invalid syntax element value: '{element}', expect corrupted bitstream"); + Ok(()) + } + + /// Writes 5.3.2. OBU header syntax + fn obu_header(&mut self, obu: &ObuHeader) -> SynthesizerResult<()> { + self.f(1, /* obu_forbidden_bit */ 0u32)?; + self.f(4, obu.obu_type as u32)?; + self.f(1, obu.extension_flag)?; + self.f(1, obu.has_size_field)?; + self.f(1, /* obu_reserved_1bit */ 0u32)?; + + if obu.extension_flag { + self.obu_extension_header(obu)?; + } + + Ok(()) + } + + /// Writes AV1 5.3.3. OBU extension header syntax + fn obu_extension_header(&mut self, obu: &ObuHeader) -> SynthesizerResult<()> { + // AV1 5.3.3 + self.f(3, obu.temporal_id)?; + self.f(2, obu.spatial_id)?; + self.f(3, /* extension_header_reserved_3bits */ 0u32)?; + + Ok(()) + } + + fn obu_size(&mut self, size: u32) -> SynthesizerResult<()> { + self.leb128(size) + } + + /// Writes AV1 5.3.4. Trailing bits syntax + fn trailing_bits(&mut self) -> SynthesizerResult<()> { + self.f(1, /* trailing_one_bit */ 1u32)?; + while !self.writer.aligned() { + self.f(1, /* trailing_zero_bit */ 0u32)?; + } + + Ok(()) + } +} + +impl<'o, W> Synthesizer<'o, TemporalDelimiterObu, W> +where + W: Write, +{ + pub fn synthesize(obu: &'o TemporalDelimiterObu, writer: W) -> SynthesizerResult<()> { + let mut s = Self::new(writer, obu); + + if obu.obu_header.obu_type != ObuType::TemporalDelimiter { + s.invalid_element_value("obu_type")?; + } + + s.obu_header(&obu.obu_header)?; + + if obu.obu_header.has_size_field { + s.obu_size(0u32)?; + } + + Ok(()) + } +} + +impl<'o, W> Synthesizer<'o, SequenceHeaderObu, W> +where + W: Write, +{ + pub fn synthesize(obu: &'o SequenceHeaderObu, mut writer: W) -> SynthesizerResult<()> { + let mut s = Synthesizer::new(&mut writer, obu); + + if obu.obu_header.obu_type != ObuType::SequenceHeader { + s.invalid_element_value("obu_type")?; + } + + s.obu_header(&obu.obu_header)?; + + if !obu.obu_header.has_size_field { + s.sequence_header_obu()?; + s.trailing_bits()?; + return Ok(()); + } + + let mut buf = Vec::::new(); + let mut buffered = Synthesizer::new(&mut buf, obu); + buffered.sequence_header_obu()?; + buffered.trailing_bits()?; + drop(buffered); + + s.obu_size(buf.len() as u32)?; + drop(s); + + writer.write_all(&buf)?; + + Ok(()) + } + + /// Writes AV1 5.5.1. General sequence header OBU syntax + fn sequence_header_obu(&mut self) -> SynthesizerResult<()> { + self.f(3, self.obu.seq_profile as u32)?; + self.f(1, self.obu.still_picture)?; + self.f(1, self.obu.reduced_still_picture_header)?; + + if self.obu.reduced_still_picture_header { + if self.obu.timing_info_present_flag { + self.invalid_element_value("reduced_still_picture_header")? + } + if self.obu.timing_info_present_flag { + self.invalid_element_value("timing_info_present_flag")? + } + if self.obu.initial_display_delay_present_flag { + self.invalid_element_value("initial_display_delay_present_flag")? + } + if self.obu.operating_points_cnt_minus_1 != 0 { + self.invalid_element_value("operating_points_cnt_minus_1")? + } + if self.obu.operating_points[0].idc != 0 { + self.invalid_element_value("operating_point_idc")? + } + + self.f(5, self.obu.operating_points[0].seq_level_idx)?; + + if self.obu.operating_points[0].decoder_model_present_for_this_op { + self.invalid_element_value("decoder_model_present_for_this_op")? + } + if self.obu.operating_points[0].initial_display_delay_present_for_this_op { + self.invalid_element_value("initial_display_delay_present_for_this_op")? + } + } else { + self.f(1, self.obu.timing_info_present_flag)?; + if self.obu.timing_info_present_flag { + self.timing_info()?; + self.f(1, self.obu.decoder_model_info_present_flag)?; + if self.obu.decoder_model_info_present_flag { + self.decoder_model_info()?; + } + } else if self.obu.decoder_model_info_present_flag { + self.invalid_element_value("decoder_model_info_present_flag")?; + } + + self.f(1, self.obu.initial_display_delay_present_flag)?; + + if self.obu.operating_points_cnt_minus_1 > MAX_NUM_OPERATING_POINTS as u32 { + self.invalid_element_value("operating_points_cnt_minus_1")?; + } + self.f(5, self.obu.operating_points_cnt_minus_1)?; + for i in 0..=self.obu.operating_points_cnt_minus_1 { + let op = &self.obu.operating_points[i as usize]; + + self.f(12, op.idc)?; + self.f(5, op.seq_level_idx)?; + if op.seq_level_idx > 7 { + self.f(1, op.seq_tier)?; + } else if op.seq_tier != 0 { + self.invalid_element_value("seq_tier")?; + } + + if self.obu.decoder_model_info_present_flag { + self.f(1, op.decoder_model_present_for_this_op)?; + if op.decoder_model_present_for_this_op { + self.operating_parameters_info(i as usize)?; + } + } else if op.decoder_model_present_for_this_op { + self.invalid_element_value("decoder_model_present_for_this_op")?; + } + + if self.obu.initial_display_delay_present_flag { + self.f(1, op.initial_display_delay_present_for_this_op)?; + if op.initial_display_delay_present_for_this_op { + self.f(4, op.initial_display_delay_minus_1)?; + } + } + } + } + + let bits = u16::BITS - self.obu.max_frame_width_minus_1.leading_zeros(); + if self.obu.frame_width_bits_minus_1 as u32 + 1 < bits { + self.invalid_element_value("frame_width_bits_minus_1")?; + } + + let bits = u16::BITS - self.obu.max_frame_height_minus_1.leading_zeros(); + if self.obu.frame_height_bits_minus_1 as u32 + 1 < bits { + self.invalid_element_value("frame_height_bits_minus_1")?; + } + + self.f(4, self.obu.frame_width_bits_minus_1)?; + self.f(4, self.obu.frame_height_bits_minus_1)?; + + let n = self.obu.frame_width_bits_minus_1 as usize + 1; + if (n as u32) < u16::BITS - self.obu.max_frame_width_minus_1.leading_zeros() { + self.invalid_element_value("max_frame_width_minus_1")?; + } + self.f(n, self.obu.max_frame_width_minus_1)?; + + let n = self.obu.frame_height_bits_minus_1 as usize + 1; + if (n as u32) < u16::BITS - self.obu.max_frame_height_minus_1.leading_zeros() { + self.invalid_element_value("max_frame_height_minus_1")?; + } + self.f(n, self.obu.max_frame_height_minus_1)?; + + if self.obu.reduced_still_picture_header { + if self.obu.frame_id_numbers_present_flag { + self.invalid_element_value("frame_id_numbers_present_flag")?; + } + } else { + self.f(1, self.obu.frame_id_numbers_present_flag)?; + } + + if self.obu.frame_id_numbers_present_flag { + self.f(4, self.obu.delta_frame_id_length_minus_2)?; + self.f(3, self.obu.additional_frame_id_length_minus_1)?; + } + + self.f(1, self.obu.use_128x128_superblock)?; + self.f(1, self.obu.enable_filter_intra)?; + self.f(1, self.obu.enable_intra_edge_filter)?; + + if self.obu.reduced_still_picture_header { + if self.obu.enable_interintra_compound { + self.invalid_element_value("enable_interintra_compound")?; + } + if self.obu.enable_masked_compound { + self.invalid_element_value("enable_masked_compound")?; + } + if self.obu.enable_warped_motion { + self.invalid_element_value("enable_warped_motion")?; + } + if self.obu.enable_dual_filter { + self.invalid_element_value("enable_dual_filter")?; + } + if self.obu.enable_order_hint { + self.invalid_element_value("enable_order_hint")?; + } + if self.obu.enable_jnt_comp { + self.invalid_element_value("enable_jnt_comp")?; + } + if self.obu.enable_ref_frame_mvs { + self.invalid_element_value("enable_ref_frame_mvs")?; + } + if self.obu.seq_force_screen_content_tools != SELECT_SCREEN_CONTENT_TOOLS as u32 { + self.invalid_element_value("seq_force_screen_content_tools")?; + } + if self.obu.seq_force_integer_mv != SELECT_INTEGER_MV as u32 { + self.invalid_element_value("seq_force_integer_mv")?; + } + if self.obu.order_hint_bits != 0 { + self.invalid_element_value("OrderHintBits")?; + } + } else { + self.f(1, self.obu.enable_interintra_compound)?; + self.f(1, self.obu.enable_masked_compound)?; + self.f(1, self.obu.enable_warped_motion)?; + self.f(1, self.obu.enable_dual_filter)?; + self.f(1, self.obu.enable_order_hint)?; + + if self.obu.enable_order_hint { + self.f(1, self.obu.enable_jnt_comp)?; + self.f(1, self.obu.enable_ref_frame_mvs)?; + } else { + if self.obu.enable_jnt_comp { + self.invalid_element_value("enable_jnt_comp")?; + } + if self.obu.enable_ref_frame_mvs { + self.invalid_element_value("enable_ref_frame_mvs")?; + } + } + + self.f(1, self.obu.seq_choose_screen_content_tools)?; + if self.obu.seq_choose_screen_content_tools { + if self.obu.seq_force_screen_content_tools != SELECT_SCREEN_CONTENT_TOOLS as u32 { + self.invalid_element_value("seq_force_screen_content_tools")?; + } + } else { + self.f(1, self.obu.seq_force_screen_content_tools)?; + } + + if self.obu.seq_force_screen_content_tools > 0 { + self.f(1, self.obu.seq_choose_integer_mv)?; + if self.obu.seq_choose_integer_mv { + if self.obu.seq_force_integer_mv != SELECT_INTEGER_MV as u32 { + self.invalid_element_value("seq_force_integer_mv")?; + } + } else { + self.f(1, self.obu.seq_force_integer_mv)?; + } + } else if self.obu.seq_force_integer_mv != SELECT_INTEGER_MV as u32 { + self.invalid_element_value("seq_force_integer_mv")?; + } + + if self.obu.enable_order_hint { + self.f(3, self.obu.order_hint_bits_minus_1 as u32)?; + if self.obu.order_hint_bits != self.obu.order_hint_bits_minus_1 + 1 { + self.invalid_element_value("OrderHintBits")?; + } + } else if self.obu.order_hint_bits != 0 { + self.invalid_element_value("OrderHintBits")?; + } + } + + self.f(1, self.obu.enable_superres)?; + self.f(1, self.obu.enable_cdef)?; + self.f(1, self.obu.enable_restoration)?; + self.color_config()?; + self.f(1, self.obu.film_grain_params_present)?; + + Ok(()) + } + + /// Writes AV1 5.5.2. Color config syntax + fn color_config(&mut self) -> SynthesizerResult<()> { + let cc = &self.obu.color_config; + + self.f(1, cc.high_bitdepth)?; + if matches!(self.obu.seq_profile, Profile::Profile2) && cc.high_bitdepth { + self.f(1, cc.twelve_bit)?; + + if (cc.twelve_bit && self.obu.bit_depth != BitDepth::Depth12) + || (!cc.twelve_bit && self.obu.bit_depth != BitDepth::Depth10) + { + self.invalid_element_value("BitDepth")?; + } + } else if self.obu.seq_profile <= Profile::Profile2 + && ((cc.high_bitdepth && self.obu.bit_depth != BitDepth::Depth10) + || (!cc.high_bitdepth && self.obu.bit_depth != BitDepth::Depth8)) + { + self.invalid_element_value("BitDepth")?; + } + + if matches!(self.obu.seq_profile, Profile::Profile1) { + if cc.mono_chrome { + self.invalid_element_value("mono_chrome")?; + } + } else { + self.f(1, cc.mono_chrome)?; + } + + if (cc.mono_chrome && self.obu.num_planes != 1) + || (!cc.mono_chrome && self.obu.num_planes != 3) + { + self.invalid_element_value("NumPlanes")?; + } + + self.f(1, cc.color_description_present_flag)?; + if cc.color_description_present_flag { + self.f(8, cc.color_primaries as u32)?; + self.f(8, cc.transfer_characteristics as u32)?; + self.f(8, cc.matrix_coefficients as u32)?; + } else { + if !matches!(cc.color_primaries, ColorPrimaries::Unspecified) { + self.invalid_element_value("color_primaries")?; + } + if !matches!( + cc.transfer_characteristics, + TransferCharacteristics::Unspecified + ) { + self.invalid_element_value("transfer_characteristics")?; + } + if !matches!(cc.matrix_coefficients, MatrixCoefficients::Unspecified) { + self.invalid_element_value("matrix_coefficients")?; + } + } + + if cc.mono_chrome { + self.f(1, cc.color_range)?; + + if !cc.subsampling_x { + self.invalid_element_value("subsampling_x")?; + } + if !cc.subsampling_y { + self.invalid_element_value("subsampling_y")?; + } + if !matches!(cc.chroma_sample_position, ChromaSamplePosition::Unknown) { + self.invalid_element_value("chroma_sample_position")?; + } + if !matches!(cc.chroma_sample_position, ChromaSamplePosition::Unknown) { + self.invalid_element_value("chroma_sample_position")?; + } + if cc.separate_uv_delta_q { + self.invalid_element_value("separate_uv_delta_q")?; + } + + return Ok(()); + } else if matches!(cc.color_primaries, ColorPrimaries::Bt709) + && matches!(cc.transfer_characteristics, TransferCharacteristics::Srgb) + && matches!(cc.matrix_coefficients, MatrixCoefficients::Identity) + { + if !cc.color_range { + self.invalid_element_value("color_range")?; + } + if cc.subsampling_x { + self.invalid_element_value("subsampling_x")?; + } + if cc.subsampling_y { + self.invalid_element_value("subsampling_y")?; + } + } else { + self.f(1, cc.color_range)?; + + match self.obu.seq_profile { + Profile::Profile0 => { + if !cc.subsampling_x { + self.invalid_element_value("subsampling_x")?; + } + if !cc.subsampling_y { + self.invalid_element_value("subsampling_y")?; + } + } + Profile::Profile1 => { + if cc.subsampling_x { + self.invalid_element_value("subsampling_x")?; + } + if cc.subsampling_y { + self.invalid_element_value("subsampling_y")?; + } + } + _ => { + if matches!(self.obu.bit_depth, BitDepth::Depth12) { + self.f(1, cc.subsampling_x)?; + if cc.subsampling_x { + self.f(1, cc.subsampling_y)?; + } else if cc.subsampling_y { + self.invalid_element_value("subsampling_y")?; + } + } else { + if !cc.subsampling_x { + self.invalid_element_value("subsampling_x")?; + } + if cc.subsampling_y { + self.invalid_element_value("subsampling_y")?; + } + } + } + } + + if cc.subsampling_x && cc.subsampling_y { + self.f(2, cc.chroma_sample_position as u32)?; + } + } + + self.f(1, cc.separate_uv_delta_q)?; + + Ok(()) + } + + /// Writes AV1 5.5.3. Timing info syntax + fn timing_info(&mut self) -> SynthesizerResult<()> { + // AV1 5.5.3 + let ti = &self.obu.timing_info; + + self.f(32, ti.num_units_in_display_tick)?; + self.f(32, ti.time_scale)?; + self.f(1, ti.equal_picture_interval)?; + if ti.equal_picture_interval { + self.uvlc(ti.num_ticks_per_picture_minus_1)?; + } + + Ok(()) + } + + /// Writes AV1 5.5.4. Decoder model info syntax + fn decoder_model_info(&mut self) -> SynthesizerResult<()> { + let dm = &self.obu.decoder_model_info; + + self.f(5, dm.buffer_delay_length_minus_1)?; + self.f(32, dm.num_units_in_decoding_tick)?; + self.f(5, dm.buffer_removal_time_length_minus_1)?; + self.f(5, dm.frame_presentation_time_length_minus_1)?; + + Ok(()) + } + + /// Writes AV1 5.5.5. Operating parameters info syntax + fn operating_parameters_info(&mut self, i: usize) -> SynthesizerResult<()> { + let op = &self.obu.operating_points[i]; + + let n = usize::from(self.obu.decoder_model_info.buffer_delay_length_minus_1) + 1; + self.f(n, op.decoder_buffer_delay)?; + self.f(n, op.encoder_buffer_delay)?; + self.f(1, op.low_delay_mode_flag)?; + + Ok(()) + } +} + +impl<'o, W> Synthesizer<'o, FrameHeaderObu, W> +where + W: Write, +{ + pub fn synthesize( + obu: &'o FrameHeaderObu, + sequence: &'o SequenceHeaderObu, + mut writer: W, + ) -> SynthesizerResult<()> { + let mut s = Synthesizer::new(&mut writer, obu); + + if obu.obu_header.obu_type != ObuType::FrameHeader { + s.invalid_element_value("obu_type")?; + } + + s.obu_header(&obu.obu_header)?; + + if !obu.obu_header.has_size_field { + s.frame_header_obu(sequence)?; + s.trailing_bits()?; + return Ok(()); + } + + let mut buf = Vec::::new(); + let mut buffered = Synthesizer::new(&mut buf, obu); + buffered.frame_header_obu(sequence)?; + buffered.trailing_bits()?; + drop(buffered); + + s.obu_size(buf.len() as u32)?; + drop(s); + + writer.write_all(&buf)?; + + Ok(()) + } + + /// Writes AV1 5.9.1. General frame header OBU syntax + fn frame_header_obu(&mut self, sequence: &'o SequenceHeaderObu) -> SynthesizerResult<()> { + self.uncompressed_header(sequence) + } + + /// Writes AV1 5.9.2. Uncompressed header syntax + fn uncompressed_header(&mut self, sequence: &'o SequenceHeaderObu) -> SynthesizerResult<()> { + const ALL_FRAMES: u32 = (1 << NUM_REF_FRAMES) - 1; + + // idLen + let id_len = usize::try_from( + sequence.additional_frame_id_length_minus_1 + + sequence.delta_frame_id_length_minus_2 + + 3, + )?; + + if sequence.reduced_still_picture_header { + if !self.obu.show_existing_frame { + self.invalid_element_value("show_existing_frame")?; + } + if !matches!(self.obu.frame_type, FrameType::KeyFrame) { + self.invalid_element_value("frame_type")?; + } + if !self.obu.show_frame { + self.invalid_element_value("show_frame")?; + } + if !self.obu.showable_frame { + self.invalid_element_value("showable_frame")?; + } + if !self.obu.frame_is_intra { + self.invalid_element_value("FrameIsIntra")?; + } + } else { + self.f(1, self.obu.show_existing_frame)?; + if self.obu.show_existing_frame { + self.f(3, self.obu.frame_to_show_map_idx)?; + + if sequence.decoder_model_info_present_flag + && !sequence.timing_info.equal_picture_interval + { + self.temporal_point_info(sequence)?; + } + + if sequence.frame_id_numbers_present_flag { + self.f(id_len, self.obu.display_frame_id)?; + } + return Ok(()); + } + + self.f(2, self.obu.frame_type as u32)?; + if self.obu.frame_is_intra + ^ matches!( + self.obu.frame_type, + FrameType::IntraOnlyFrame | FrameType::KeyFrame + ) + { + self.invalid_element_value("FrameIsIntra")?; + } + + self.f(1, self.obu.show_frame)?; + if self.obu.show_frame + && sequence.decoder_model_info_present_flag + && !sequence.timing_info.equal_picture_interval + { + self.temporal_point_info(sequence)?; + } + + if self.obu.show_frame { + if self.obu.showable_frame ^ !matches!(self.obu.frame_type, FrameType::KeyFrame) { + self.invalid_element_value("showable_frame")?; + } + } else { + self.f(1, self.obu.showable_frame)?; + } + + if matches!(self.obu.frame_type, FrameType::SwitchFrame) + || (matches!(self.obu.frame_type, FrameType::KeyFrame) && self.obu.show_frame) + { + if !self.obu.error_resilient_mode { + self.invalid_element_value("error_resilient_mode")?; + } + } else { + self.f(1, self.obu.error_resilient_mode)?; + } + } + + self.f(1, self.obu.disable_cdf_update)?; + if sequence.seq_force_screen_content_tools == SELECT_SCREEN_CONTENT_TOOLS as u32 { + self.f(1, self.obu.allow_screen_content_tools)?; + } else if self.obu.allow_screen_content_tools != sequence.seq_force_screen_content_tools { + self.invalid_element_value("allow_screen_content_tools")?; + } + + if self.obu.allow_screen_content_tools != 0 { + if sequence.seq_force_integer_mv == SELECT_INTEGER_MV as u32 { + self.f(1, self.obu.force_integer_mv)?; + } else if self.obu.force_integer_mv != sequence.seq_force_integer_mv { + self.invalid_element_value("force_integer_mv")?; + } + } else if self.obu.force_integer_mv != 0 + && !(self.obu.frame_is_intra && self.obu.force_integer_mv != 1) + { + self.invalid_element_value("force_integer_mv")?; + } + + if sequence.frame_id_numbers_present_flag { + self.f(id_len, self.obu.current_frame_id)?; + } else if self.obu.current_frame_id != 0 { + self.invalid_element_value("current_frame_id")?; + } + + if matches!(self.obu.frame_type, FrameType::SwitchFrame) { + if !self.obu.frame_size_override_flag { + self.invalid_element_value("frame_size_override_flag")?; + } + } else if sequence.reduced_still_picture_header { + if self.obu.frame_size_override_flag { + self.invalid_element_value("frame_size_override_flag")?; + } + } else { + self.f(1, self.obu.frame_size_override_flag)?; + } + + if sequence.order_hint_bits != 0 { + if sequence.order_hint_bits != sequence.order_hint_bits_minus_1 + 1 { + self.invalid_element_value("order_hint_bits_minus_1")?; + } + + self.f(sequence.order_hint_bits as usize, self.obu.order_hint)?; + } + + if self.obu.frame_is_intra || self.obu.error_resilient_mode { + if self.obu.primary_ref_frame != PRIMARY_REF_NONE { + self.invalid_element_value("primary_ref_frame")?; + } + } else { + self.f(3, self.obu.primary_ref_frame)?; + } + + if sequence.decoder_model_info_present_flag { + self.f(1, self.obu.buffer_removal_time_present_flag)?; + + for op_num in 0..=sequence.operating_points_cnt_minus_1 { + let op = &sequence.operating_points[op_num as usize]; + if op.decoder_model_present_for_this_op { + let in_temporal_layer = (op.idc >> self.obu.obu_header.temporal_id) & 1 != 0; + let in_spatial_layer = + (op.idc >> (self.obu.obu_header.spatial_id + 8)) & 1 != 0; + + if op.idc == 0 || (in_temporal_layer && in_spatial_layer) { + let n = usize::from( + sequence + .decoder_model_info + .buffer_removal_time_length_minus_1 + + 1, + ); + + self.f(n, op.decoder_buffer_delay)?; + } + } + } + } + + if matches!(self.obu.frame_type, FrameType::SwitchFrame) + || (matches!(self.obu.frame_type, FrameType::KeyFrame) && self.obu.show_frame) + { + if self.obu.refresh_frame_flags != ALL_FRAMES { + self.invalid_element_value("refresh_frame_flags")?; + } + } else { + self.f(8, self.obu.refresh_frame_flags)?; + } + + if (!self.obu.frame_is_intra || self.obu.refresh_frame_flags != ALL_FRAMES) + && self.obu.error_resilient_mode + && sequence.enable_order_hint + { + for i in 0..NUM_REF_FRAMES { + self.f( + sequence.order_hint_bits as usize, + self.obu.ref_order_hint[i], + )?; + } + } + + if self.obu.frame_is_intra { + self.frame_size(sequence)?; + self.render_size()?; + + if self.obu.allow_screen_content_tools != 0 + && self.obu.upscaled_width == self.obu.frame_width + { + self.f(1, self.obu.allow_intrabc)?; + } + } else { + if !sequence.enable_order_hint { + if self.obu.frame_refs_short_signaling { + self.invalid_element_value("frame_refs_short_signaling")?; + } + } else { + self.f(1, self.obu.frame_refs_short_signaling)?; + if self.obu.frame_refs_short_signaling { + self.f(3, self.obu.last_frame_idx)?; + self.f(3, self.obu.gold_frame_idx)?; + } + } + + for i in 0..REFS_PER_FRAME { + let ref_frame_idx = self.obu.ref_frame_idx[i] as u32; + if !self.obu.frame_refs_short_signaling { + self.f(3, ref_frame_idx)?; + } + + if sequence.frame_id_numbers_present_flag { + let n = usize::try_from(sequence.delta_frame_id_length_minus_2 + 2)?; + + let delta_frame_id_minus_1 = ((self.obu.current_frame_id - ref_frame_idx + + (1 << id_len)) + % (1 << id_len)) + - 1; + + self.f(n, delta_frame_id_minus_1)?; + } + } + + if self.obu.frame_size_override_flag && !self.obu.error_resilient_mode { + self.frame_size_with_refs()?; + } else { + self.frame_size(sequence)?; + self.render_size()?; + } + + if self.obu.force_integer_mv != 0 { + if !self.obu.allow_high_precision_mv { + self.invalid_element_value("allow_high_precision_mv")?; + } + } else { + self.f(1, self.obu.allow_high_precision_mv)?; + } + + self.read_interpolation_filter()?; + self.f(1, self.obu.is_motion_mode_switchable)?; + + if self.obu.error_resilient_mode || !sequence.enable_ref_frame_mvs { + if self.obu.use_ref_frame_mvs { + self.invalid_element_value("use_ref_frame_mvs")?; + } + } else { + self.f(1, self.obu.use_ref_frame_mvs)?; + } + } + + if sequence.reduced_still_picture_header || self.obu.disable_cdf_update { + if !self.obu.disable_frame_end_update_cdf { + self.invalid_element_value("disable_frame_end_update_cdf")? + } + } else { + self.f(1, self.obu.disable_frame_end_update_cdf)?; + } + + self.tile_info()?; + self.quantization_params(sequence)?; + self.segmentation_params()?; + + self.delta_q_params()?; + self.delta_lf_params()?; + + if self.obu.coded_lossless && self.obu.lossless_array != [true; MAX_SEGMENTS] { + self.invalid_element_value("CodedLossless")?; + } + + if self.obu.all_lossless + ^ (self.obu.coded_lossless && self.obu.frame_width == self.obu.upscaled_width) + { + self.invalid_element_value("AllLossless")?; + } + + self.loop_filter_params(sequence)?; + self.cdef_params(sequence)?; + self.lr_params(sequence)?; + self.read_tx_mode()?; + self.frame_reference_mode()?; + self.skip_mode_params()?; + + if self.obu.frame_is_intra + || self.obu.error_resilient_mode + || !sequence.enable_warped_motion + { + if self.obu.allow_warped_motion { + self.invalid_element_value("allow_warped_motion")?; + } + } else { + self.f(1, self.obu.allow_warped_motion)?; + } + + self.f(1, self.obu.reduced_tx_set)?; + + self.global_motion_params()?; + self.film_grain_params(sequence)?; + + Ok(()) + } + + /// Writes AV1 5.9.31. Temporal point info syntax + fn temporal_point_info(&mut self, sequence: &'o SequenceHeaderObu) -> SynthesizerResult<()> { + let n = usize::try_from( + sequence + .decoder_model_info + .frame_presentation_time_length_minus_1 + + 1, + )?; + + self.f(n, self.obu.frame_presentation_time)?; + + Ok(()) + } + + /// Writes AV1 5.9.5. Frame size syntax + fn frame_size(&mut self, sequence: &'o SequenceHeaderObu) -> SynthesizerResult<()> { + if self.obu.frame_size_override_flag { + let n = sequence.frame_width_bits_minus_1 as usize + 1; + self.f(n, self.obu.frame_width - 1)?; + let n = sequence.frame_height_bits_minus_1 as usize + 1; + self.f(n, self.obu.frame_height - 1)?; + } else { + if self.obu.frame_width != sequence.max_frame_width_minus_1 as u32 + 1 { + self.invalid_element_value("FrameWidth")?; + } + if self.obu.frame_height != sequence.max_frame_height_minus_1 as u32 + 1 { + self.invalid_element_value("FrameHeight")?; + } + + self.superres_params(sequence)?; + } + + Ok(()) + } + + /// Writes AV1 5.9.6. Render size syntax + fn render_size(&mut self) -> SynthesizerResult<()> { + self.f(1, self.obu.render_and_frame_size_different)?; + + if self.obu.render_and_frame_size_different { + self.f(16, self.obu.render_width - 1)?; + self.f(16, self.obu.render_height - 1)?; + } else { + if self.obu.render_width != self.obu.upscaled_width { + self.invalid_element_value("RenderWidth")?; + } + if self.obu.render_height != self.obu.frame_height { + self.invalid_element_value("RenderHeight")?; + } + } + + Ok(()) + } + + /// Writes AV1 5.9.7. Frame size with refs syntax + fn frame_size_with_refs(&mut self) -> SynthesizerResult<()> { + log::error!("Syntax element frame_size_with_refs is unsupported"); + Err(SynthesizerError::Unsupported) + } + + /// Writes AV1 5.9.8. Superres params syntax + fn superres_params(&mut self, sequence: &'o SequenceHeaderObu) -> SynthesizerResult<()> { + if sequence.enable_superres { + self.f(1, self.obu.use_superres)?; + } else if self.obu.use_superres { + self.invalid_element_value("use_superres")?; + } + + if self.obu.use_superres { + let coded_denom = self.obu.superres_denom - SUPERRES_DENOM_MIN as u32; + + self.f(SUPERRES_DENOM_BITS, coded_denom)?; + } else if self.obu.superres_denom != SUPERRES_NUM as u32 { + self.invalid_element_value("superres_denom")?; + } + + Ok(()) + } + + /// Writes AV1 5.9.10. Interpolation filter syntax + fn read_interpolation_filter(&mut self) -> SynthesizerResult<()> { + self.f(1, self.obu.is_filter_switchable)?; + if self.obu.is_filter_switchable { + if !matches!( + self.obu.interpolation_filter, + InterpolationFilter::Switchable + ) { + self.invalid_element_value("interpolation_filter")?; + } + } else { + self.f(2, self.obu.interpolation_filter as u32)?; + } + + Ok(()) + } + + /// Writes AV1 5.9.11. Loop filter params syntax + fn loop_filter_params(&mut self, sequence: &'o SequenceHeaderObu) -> SynthesizerResult<()> { + if self.obu.coded_lossless || self.obu.allow_intrabc { + if !matches!(self.obu.loop_filter_params.loop_filter_level, [0, 0, _, _]) { + self.invalid_element_value("loop_filter_level")?; + } + if self.obu.loop_filter_params.loop_filter_ref_deltas != [1, 0, 0, 0, 0, -1, -1, -1] { + self.invalid_element_value("loop_filter_ref_deltas")?; + } + if self.obu.loop_filter_params.loop_filter_mode_deltas != [0, 0] { + self.invalid_element_value("loop_filter_mode_deltas")?; + } + return Ok(()); + } + + self.f(6, self.obu.loop_filter_params.loop_filter_level[0])?; + self.f(6, self.obu.loop_filter_params.loop_filter_level[1])?; + if sequence.num_planes > 1 + && self.obu.loop_filter_params.loop_filter_level[0] != 0 + && self.obu.loop_filter_params.loop_filter_level[1] != 0 + { + self.f(6, self.obu.loop_filter_params.loop_filter_level[2])?; + self.f(6, self.obu.loop_filter_params.loop_filter_level[3])?; + } + + self.f(3, self.obu.loop_filter_params.loop_filter_sharpness)?; + self.f(1, self.obu.loop_filter_params.loop_filter_delta_enabled)?; + if self.obu.loop_filter_params.loop_filter_delta_enabled { + self.f(1, self.obu.loop_filter_params.loop_filter_delta_update)?; + if self.obu.loop_filter_params.loop_filter_delta_update { + for i in 0..TOTAL_REFS_PER_FRAME { + // NOTE: Currently we have no way of checking if the value changed between + // frames, always update the value to make sure the decoder will recreate + // the same state. + const UPDATE_REF_DELTA: bool = true; + if UPDATE_REF_DELTA { + self.su(1 + 6, self.obu.loop_filter_params.loop_filter_ref_deltas[i])?; + } + } + + for i in 0..2 { + // NOTE: Same as above + const UPDATE_MODE_DELTA: bool = true; + if UPDATE_MODE_DELTA { + self.su( + 1 + 6, + self.obu.loop_filter_params.loop_filter_mode_deltas[i], + )?; + } + } + } + } + + Ok(()) + } + + /// Writes AV1 5.9.12. Quantization params syntax + fn quantization_params(&mut self, sequence: &'o SequenceHeaderObu) -> SynthesizerResult<()> { + self.f(8, self.obu.quantization_params.base_q_idx)?; + self.read_delta_q(self.obu.quantization_params.delta_q_y_dc)?; + if sequence.num_planes > 1 { + if sequence.color_config.separate_uv_delta_q { + self.f(1, self.obu.quantization_params.diff_uv_delta)?; + } else if self.obu.quantization_params.diff_uv_delta { + self.invalid_element_value("diff_uv_delta")?; + }; + + self.read_delta_q(self.obu.quantization_params.delta_q_u_dc)?; + self.read_delta_q(self.obu.quantization_params.delta_q_u_ac)?; + + if self.obu.quantization_params.diff_uv_delta { + self.read_delta_q(self.obu.quantization_params.delta_q_v_dc)?; + self.read_delta_q(self.obu.quantization_params.delta_q_v_ac)?; + } else { + if self.obu.quantization_params.delta_q_v_dc != 0 { + self.invalid_element_value("delta_q_v_dc")?; + } + if self.obu.quantization_params.delta_q_v_ac != 0 { + self.invalid_element_value("delta_q_v_ac")?; + } + } + } else { + if self.obu.quantization_params.delta_q_u_dc != 0 { + self.invalid_element_value("delta_q_u_dc")?; + } + if self.obu.quantization_params.delta_q_u_ac != 0 { + self.invalid_element_value("delta_q_u_ac")?; + } + if self.obu.quantization_params.delta_q_v_dc != 0 { + self.invalid_element_value("delta_q_v_dc")?; + } + if self.obu.quantization_params.delta_q_v_ac != 0 { + self.invalid_element_value("delta_q_v_ac")?; + } + } + + self.f(1, self.obu.quantization_params.using_qmatrix)?; + if self.obu.quantization_params.using_qmatrix { + self.f(4, self.obu.quantization_params.qm_y)?; + self.f(4, self.obu.quantization_params.qm_u)?; + + if !sequence.color_config.separate_uv_delta_q { + if self.obu.quantization_params.qm_v != self.obu.quantization_params.qm_u { + self.invalid_element_value("qm_v")?; + } + } else { + self.f(4, self.obu.quantization_params.qm_v)?; + } + } + + Ok(()) + } + + /// Writes AV1 5.9.13. Delta quantizer syntax + fn read_delta_q(&mut self, delta_q: i32) -> SynthesizerResult<()> { + self.f(1, delta_q != 0)?; + if delta_q != 0 { + self.su(1 + 6, delta_q)?; + } + Ok(()) + } + + /// Writes AV1 5.9.14. Segmentation params syntax + fn segmentation_params(&mut self) -> SynthesizerResult<()> { + self.f(1, self.obu.segmentation_params.segmentation_enabled)?; + if self.obu.segmentation_params.segmentation_enabled { + if self.obu.primary_ref_frame == PRIMARY_REF_NONE { + if !self.obu.segmentation_params.segmentation_update_map { + self.invalid_element_value("segmentation_update_map")?; + } + + if self.obu.segmentation_params.segmentation_temporal_update { + self.invalid_element_value("segmentation_temporal_update")?; + } + + if !self.obu.segmentation_params.segmentation_update_data { + self.invalid_element_value("segmentation_update_data")?; + } + } else { + self.f(1, self.obu.segmentation_params.segmentation_update_map)?; + if self.obu.segmentation_params.segmentation_update_map { + self.f(1, self.obu.segmentation_params.segmentation_temporal_update)?; + } + self.f(1, self.obu.segmentation_params.segmentation_temporal_update)?; + } + + if self.obu.segmentation_params.segmentation_update_data { + for i in 0..MAX_SEGMENTS { + for j in 0..SEG_LVL_MAX { + let feature_enabled = self.obu.segmentation_params.feature_enabled[i][j]; + self.f(1, feature_enabled)?; + + if feature_enabled { + let bits_to_read = FEATURE_BITS[j] as usize; + let limit = FEATURE_MAX[j]; + let signed = FEATURE_SIGNED[j]; + + let value = i32::from(self.obu.segmentation_params.feature_data[i][j]); + if signed { + let clipped_value = clip3(-limit, limit, value); + self.su(bits_to_read + 1, clipped_value)?; + } else { + let clipped_value = clip3(0, limit, value); + self.f(bits_to_read, u32::try_from(clipped_value)?)?; + } + } + } + } + } else { + if self.obu.segmentation_params.feature_enabled + != [[false; SEG_LVL_MAX]; MAX_SEGMENTS] + { + self.invalid_element_value("feature_enabled")?; + } + if self.obu.segmentation_params.feature_data != [[0; SEG_LVL_MAX]; MAX_SEGMENTS] { + self.invalid_element_value("feature_data")?; + } + } + } + + Ok(()) + } + + /// Writes AV1 5.9.15. Tile info syntax + fn tile_info(&mut self) -> SynthesizerResult<()> { + // From AV1 5.9.9. Compute image size function + self.f(1, self.obu.tile_info.uniform_tile_spacing_flag)?; + if self.obu.tile_info.uniform_tile_spacing_flag { + if self.obu.tile_info.tile_cols != 1 + && self.obu.tile_info.tile_cols_log2 != 0 + && self.obu.tile_info.tile_rows != 1 + && self.obu.tile_info.tile_rows_log2 != 0 + { + // TODO: Allow more then single tile + log::error!("Only 1x1 tiles frame is currently supported"); + return Err(SynthesizerError::Unsupported); + } + + const INCREMENT_TILE_COLS_LOG2: u32 = 0; + const INCREMENT_TILE_ROWS_LOG2: u32 = 0; + + self.f(1, INCREMENT_TILE_COLS_LOG2)?; + self.f(1, INCREMENT_TILE_ROWS_LOG2)?; + } else { + // TODO + log::error!("Only uniformly sized tiles are currently supported"); + return Err(SynthesizerError::Unsupported); + } + + Ok(()) + } + + /// Writes AV1 5.9.17. Quantizer index delta parameters syntax + fn delta_q_params(&mut self) -> SynthesizerResult<()> { + if self.obu.quantization_params.base_q_idx > 0 { + self.f(1, self.obu.quantization_params.delta_q_present)?; + + if self.obu.quantization_params.delta_q_present { + self.f(2, self.obu.quantization_params.delta_q_res)?; + } + } else if self.obu.quantization_params.delta_q_present { + self.invalid_element_value("delta_q_present")?; + } + + Ok(()) + } + + /// Writes AV1 5.9.18. Loop filter delta parameters syntax + fn delta_lf_params(&mut self) -> SynthesizerResult<()> { + if self.obu.quantization_params.delta_q_present { + if self.obu.allow_intrabc { + self.f(1, self.obu.loop_filter_params.delta_lf_present)?; + + if self.obu.loop_filter_params.delta_lf_present { + self.f(2, self.obu.loop_filter_params.delta_lf_res)?; + self.f(1, self.obu.loop_filter_params.delta_lf_multi)?; + } + } else if self.obu.loop_filter_params.delta_lf_present { + self.invalid_element_value("delta_lf_present")?; + } + } + + Ok(()) + } + + /// Writes AV1 5.9.19. CDEF params syntax + fn cdef_params(&mut self, sequence: &'o SequenceHeaderObu) -> SynthesizerResult<()> { + if self.obu.coded_lossless || self.obu.allow_intrabc || !sequence.enable_cdef { + if self.obu.cdef_params.cdef_bits != 0 { + self.invalid_element_value("cdef_bits")?; + } + + if self.obu.cdef_params.cdef_y_pri_strength[0] != 0 { + self.invalid_element_value("cdef_y_pri_strength")?; + } + + if self.obu.cdef_params.cdef_y_sec_strength[0] != 0 { + self.invalid_element_value("cdef_y_sec_strength")?; + } + + if self.obu.cdef_params.cdef_uv_pri_strength[0] != 0 { + self.invalid_element_value("cdef_uv_pri_strength")?; + } + + if self.obu.cdef_params.cdef_uv_sec_strength[0] != 0 { + self.invalid_element_value("cdef_uv_sec_strength")?; + } + + if self.obu.cdef_params.cdef_damping != 3 { + self.invalid_element_value("cdef_damping")?; + } + + return Ok(()); + } + + self.f(2, self.obu.cdef_params.cdef_damping - 3)?; + self.f(2, self.obu.cdef_params.cdef_bits)?; + + for i in 0..(1 << self.obu.cdef_params.cdef_bits) { + self.f(4, self.obu.cdef_params.cdef_y_pri_strength[i])?; + + let mut cdef_y_sec_strength = self.obu.cdef_params.cdef_y_sec_strength[i]; + if cdef_y_sec_strength == 4 { + cdef_y_sec_strength -= 1; + } + + self.f(2, cdef_y_sec_strength)?; + + if sequence.num_planes > 1 { + self.f(4, self.obu.cdef_params.cdef_uv_pri_strength[i])?; + + let mut cdef_uv_sec_strength = self.obu.cdef_params.cdef_uv_sec_strength[i]; + if cdef_uv_sec_strength == 4 { + cdef_uv_sec_strength -= 1; + } + + self.f(2, cdef_uv_sec_strength)?; + } + } + + Ok(()) + } + + /// Writes AV1 5.9.20. Loop restoration params syntax + fn lr_params(&mut self, sequence: &'o SequenceHeaderObu) -> SynthesizerResult<()> { + if self.obu.all_lossless || self.obu.allow_intrabc || !sequence.enable_restoration { + if self.obu.loop_restoration_params.frame_restoration_type + != [FrameRestorationType::None; MAX_NUM_PLANES] + { + self.invalid_element_value("frame_restoration_type")?; + } + + if self.obu.loop_restoration_params.uses_lr { + self.invalid_element_value("uses_lr")?; + } + + return Ok(()); + } + + let mut uses_lr = false; + let mut uses_chroma_lr = false; + for i in 0..MAX_NUM_PLANES { + let lr_type = self.obu.loop_restoration_params.frame_restoration_type[i]; + self.f(2, lr_type as u32)?; + + if lr_type != FrameRestorationType::None { + uses_lr = true; + + if i > 0 { + uses_chroma_lr = true; + } + } + } + + if uses_lr ^ self.obu.loop_restoration_params.uses_lr { + self.invalid_element_value("uses_lr")?; + } + if uses_chroma_lr ^ self.obu.loop_restoration_params.uses_chroma_lr { + self.invalid_element_value("uses_chroma_lr")?; + } + + if uses_lr { + if sequence.use_128x128_superblock { + if self.obu.loop_restoration_params.lr_unit_shift == 0 { + self.invalid_element_value("lr_unit_shift")?; + } + + self.f(1, self.obu.loop_restoration_params.lr_unit_shift - 1)?; + } else { + self.f(1, self.obu.loop_restoration_params.lr_unit_shift)?; + if self.obu.loop_restoration_params.lr_unit_shift != 0 { + self.f(1, self.obu.loop_restoration_params.lr_unit_shift > 1)?; + } + } + + if sequence.color_config.subsampling_x + && sequence.color_config.subsampling_y + && uses_chroma_lr + { + self.f(1, self.obu.loop_restoration_params.lr_uv_shift)?; + } else if self.obu.loop_restoration_params.lr_uv_shift != 0 { + self.invalid_element_value("lr_uv_shift")?; + } + } + + Ok(()) + } + + /// Writes AV1 5.9.21. TX mode syntax + fn read_tx_mode(&mut self) -> SynthesizerResult<()> { + if self.obu.coded_lossless { + if self.obu.tx_mode != TxMode::Only4x4 { + self.invalid_element_value("TxMode")?; + } + } else { + self.f(1, self.obu.tx_mode_select)?; + + if (self.obu.tx_mode_select != 0 && self.obu.tx_mode != TxMode::Select) + || (self.obu.tx_mode_select == 0 && self.obu.tx_mode != TxMode::Largest) + { + self.invalid_element_value("TxMode")?; + } + } + + Ok(()) + } + + /// Writes AV1 5.9.22. Skip mode params syntax + fn skip_mode_params(&mut self) -> SynthesizerResult<()> { + // TODO: Implement if needed + Ok(()) + } + + /// Writes AV1 5.9.23. Frame reference mode syntax + fn frame_reference_mode(&mut self) -> SynthesizerResult<()> { + if self.obu.frame_is_intra { + if self.obu.reference_select { + self.invalid_element_value("reference_select")?; + } + } else { + self.f(1, self.obu.reference_select)?; + } + + Ok(()) + } + + /// Writes AV1 5.9.23. Frame reference mode syntax + fn global_motion_params(&mut self) -> SynthesizerResult<()> { + if self.obu.frame_is_intra { + return Ok(()); + } + + for ref_ in ReferenceFrameType::Last as usize..=ReferenceFrameType::AltRef as usize { + let is_global = self.obu.global_motion_params.is_global[ref_]; + let is_rot_zoom = self.obu.global_motion_params.is_rot_zoom[ref_]; + let is_translation = self.obu.global_motion_params.is_translation[ref_]; + let gm_type = self.obu.global_motion_params.gm_type[ref_]; + + let expected_type = match (is_global, is_rot_zoom, is_translation) { + (false, _, _) => WarpModelType::Identity, + (true, true, _) => WarpModelType::RotZoom, + (true, false, true) => WarpModelType::Translation, + (true, false, false) => WarpModelType::Affine, + }; + + if expected_type != gm_type { + self.invalid_element_value("GmType")?; + } + + self.f(1, is_global)?; + if is_global { + self.f(1, is_rot_zoom)?; + if is_rot_zoom { + } else { + self.f(1, is_translation)?; + } + } + + if gm_type >= WarpModelType::RotZoom { + self.read_global_param(gm_type, ref_, 2)?; + self.read_global_param(gm_type, ref_, 3)?; + if gm_type == WarpModelType::Affine { + self.read_global_param(gm_type, ref_, 4)?; + self.read_global_param(gm_type, ref_, 5)?; + } + } + + if gm_type >= WarpModelType::Translation { + self.read_global_param(gm_type, ref_, 0)?; + self.read_global_param(gm_type, ref_, 1)?; + } + } + + Ok(()) + } + + /// Writes AV1 5.9.25. Global param syntax + fn read_global_param( + &mut self, + _gm_type: WarpModelType, + _ref_: usize, + _idx: u32, + ) -> SynthesizerResult<()> { + // TODO + log::warn!( + "Syntax element read_global_param() is not currently supported. Use GmType=IDENTITY" + ); + Err(SynthesizerError::Unsupported) + } + + /// Writes AV1 5.9.30. Film grain params syntax + fn film_grain_params(&mut self, sequence: &'o SequenceHeaderObu) -> SynthesizerResult<()> { + if !sequence.film_grain_params_present || (!self.obu.show_frame && !self.obu.showable_frame) + { + return Ok(()); + } + + self.f(1, self.obu.film_grain_params.apply_grain)?; + if !self.obu.film_grain_params.apply_grain { + return Ok(()); + } + + Err(SynthesizerError::Unsupported) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::codec::av1::parser::CdefParams; + use crate::codec::av1::parser::ChromaSamplePosition; + use crate::codec::av1::parser::ColorConfig; + use crate::codec::av1::parser::TileInfo; + use crate::codec::av1::parser::MAX_TILE_COLS; + use crate::codec::av1::parser::MAX_TILE_ROWS; + + #[test] + fn sequence_header_obu_test25fps() { + // Extraced from ./src/codec/av1/test_data/test-25fps.ivf.av1 + const SEQ_HDR_RAW: [u8; 13] = [ + 0x0a, 0x0b, 0x00, 0x00, 0x00, 0x04, 0x3c, 0xff, 0xbd, 0xff, 0xf9, 0x80, 0x40, + ]; + + let seq_hdr = SequenceHeaderObu { + obu_header: ObuHeader { + obu_type: ObuType::SequenceHeader, + extension_flag: false, + has_size_field: true, + temporal_id: 0, + spatial_id: 0, + }, + + seq_profile: Profile::Profile0, + num_planes: 3, + still_picture: false, + reduced_still_picture_header: false, + timing_info_present_flag: false, + initial_display_delay_present_flag: false, + operating_points_cnt_minus_1: 0, + frame_width_bits_minus_1: 8, + frame_height_bits_minus_1: 7, + max_frame_width_minus_1: 319, + max_frame_height_minus_1: 239, + frame_id_numbers_present_flag: false, + use_128x128_superblock: true, + enable_filter_intra: true, + enable_intra_edge_filter: true, + enable_interintra_compound: true, + enable_masked_compound: true, + enable_warped_motion: true, + enable_dual_filter: true, + enable_order_hint: true, + enable_jnt_comp: true, + enable_ref_frame_mvs: true, + seq_choose_screen_content_tools: true, + seq_force_screen_content_tools: SELECT_SCREEN_CONTENT_TOOLS as u32, + seq_choose_integer_mv: true, + seq_force_integer_mv: SELECT_INTEGER_MV as u32, + order_hint_bits_minus_1: 6, + order_hint_bits: 7, + enable_superres: false, + enable_cdef: true, + enable_restoration: true, + color_config: ColorConfig { + high_bitdepth: false, + mono_chrome: false, + color_description_present_flag: false, + color_range: false, + subsampling_x: true, + subsampling_y: true, + chroma_sample_position: ChromaSamplePosition::Unknown, + separate_uv_delta_q: false, + ..Default::default() + }, + film_grain_params_present: false, + + ..Default::default() + }; + + let mut buf = Vec::::new(); + Synthesizer::<'_, SequenceHeaderObu, _>::synthesize(&seq_hdr, &mut buf).unwrap(); + assert_eq!(buf, SEQ_HDR_RAW); + } + + #[test] + fn sequence_header_obu_av1_annexb() { + // Extraced from: ./src/codec/av1/test_data/av1-annexb.ivf.av1 + const SEQ_HDR_RAW: [u8; 12] = [ + 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x02, 0xaf, 0xff, 0xbf, 0xff, 0x30, 0x08, + ]; + + let seq_hdr = SequenceHeaderObu { + obu_header: ObuHeader { + obu_type: ObuType::SequenceHeader, + extension_flag: false, + has_size_field: true, + temporal_id: 0, + spatial_id: 0, + }, + + seq_profile: Profile::Profile0, + num_planes: 3, + still_picture: false, + reduced_still_picture_header: false, + timing_info_present_flag: false, + initial_display_delay_present_flag: false, + operating_points_cnt_minus_1: 0, + frame_width_bits_minus_1: 5, + frame_height_bits_minus_1: 5, + max_frame_width_minus_1: 63, + max_frame_height_minus_1: 63, + frame_id_numbers_present_flag: false, + use_128x128_superblock: true, + enable_filter_intra: true, + enable_intra_edge_filter: true, + enable_interintra_compound: true, + enable_masked_compound: true, + enable_warped_motion: true, + enable_dual_filter: true, + enable_order_hint: true, + enable_jnt_comp: true, + enable_ref_frame_mvs: true, + seq_choose_screen_content_tools: true, + seq_force_screen_content_tools: SELECT_SCREEN_CONTENT_TOOLS as u32, + seq_choose_integer_mv: true, + seq_force_integer_mv: SELECT_INTEGER_MV as u32, + order_hint_bits_minus_1: 6, + order_hint_bits: 7, + enable_superres: false, + enable_cdef: true, + enable_restoration: true, + color_config: ColorConfig { + high_bitdepth: false, + mono_chrome: false, + color_description_present_flag: false, + color_range: false, + subsampling_x: true, + subsampling_y: true, + chroma_sample_position: ChromaSamplePosition::Unknown, + separate_uv_delta_q: false, + ..Default::default() + }, + film_grain_params_present: false, + + ..Default::default() + }; + + let mut buf = Vec::::new(); + Synthesizer::<'_, SequenceHeaderObu, _>::synthesize(&seq_hdr, &mut buf).unwrap(); + assert_eq!(buf, SEQ_HDR_RAW); + } + + #[test] + fn temporal_delim_obu() { + const TD_RAW: [u8; 2] = [0x12, 0x00]; + + let td = TemporalDelimiterObu { + obu_header: ObuHeader { + obu_type: ObuType::TemporalDelimiter, + extension_flag: false, + has_size_field: true, + temporal_id: 0, + spatial_id: 0, + }, + }; + + let mut buf = Vec::::new(); + Synthesizer::<'_, TemporalDelimiterObu, _>::synthesize(&td, &mut buf).unwrap(); + + assert_eq!(buf, TD_RAW); + } + + #[test] + fn frame_header_obu() { + let _ = env_logger::try_init(); + + const WIDTH: u32 = 512; + const HEIGHT: u32 = 512; + + let seq = SequenceHeaderObu { + obu_header: ObuHeader { + obu_type: ObuType::SequenceHeader, + extension_flag: false, + has_size_field: true, + temporal_id: 0, + spatial_id: 0, + }, + + seq_profile: Profile::Profile0, + + frame_width_bits_minus_1: 16 - 1, + frame_height_bits_minus_1: 16 - 1, + max_frame_width_minus_1: (WIDTH - 1) as u16, + max_frame_height_minus_1: (HEIGHT - 1) as u16, + + seq_force_integer_mv: SELECT_INTEGER_MV as u32, + + enable_order_hint: true, + order_hint_bits: 8, + order_hint_bits_minus_1: 7, + num_planes: 3, + + color_config: ColorConfig { + subsampling_x: true, + subsampling_y: true, + ..Default::default() + }, + + ..Default::default() + }; + + let frame = FrameHeaderObu { + obu_header: ObuHeader { + obu_type: ObuType::FrameHeader, + extension_flag: false, + has_size_field: true, + temporal_id: 0, + spatial_id: 0, + }, + + frame_type: FrameType::KeyFrame, + frame_is_intra: true, + primary_ref_frame: PRIMARY_REF_NONE, + refresh_frame_flags: 0xff, + error_resilient_mode: true, + + reduced_tx_set: true, + tx_mode_select: 1, + tx_mode: TxMode::Select, + + tile_info: TileInfo { + uniform_tile_spacing_flag: true, + tile_cols: 1, + tile_rows: 1, + tile_cols_log2: 0, + tile_rows_log2: 0, + width_in_sbs_minus_1: { + let mut value = [0u32; MAX_TILE_COLS]; + value[0] = WIDTH / 64 - 1; + value + }, + height_in_sbs_minus_1: { + let mut value = [0u32; MAX_TILE_ROWS]; + value[0] = HEIGHT / 64 - 1; + value + }, + ..Default::default() + }, + + cdef_params: CdefParams { + cdef_damping: 3, + ..Default::default() + }, + + superres_denom: SUPERRES_NUM as u32, + upscaled_width: WIDTH, + frame_width: WIDTH, + frame_height: HEIGHT, + render_width: WIDTH, + render_height: HEIGHT, + + ..Default::default() + }; + + let mut buf = Vec::::new(); + Synthesizer::<'_, SequenceHeaderObu, _>::synthesize(&seq, &mut buf).unwrap(); + + Synthesizer::<'_, FrameHeaderObu, _>::synthesize(&frame, &seq, &mut buf).unwrap(); + + // TODO actual test + + let write_to_file = std::option_env!("CROS_CODECS_TEST_WRITE_TO_FILE") == Some("true"); + if write_to_file { + let mut out = std::fs::File::create("frame_header_obu.av1").unwrap(); + out.write_all(&buf).unwrap(); + out.flush().unwrap(); + } + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/av1-annexb.ivf.av1 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/av1-annexb.ivf.av1 new file mode 100644 index 00000000..1bdc6b4c Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/av1-annexb.ivf.av1 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/av1-annexb.ivf.av1.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/av1-annexb.ivf.av1.crc new file mode 100644 index 00000000..e69de29b diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/gen_crcs.sh b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/gen_crcs.sh new file mode 100755 index 00000000..007b63e6 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/gen_crcs.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Generates the CRCs for all .av1 files in the current directory using ffmpeg. + +for f in `ls *.av1`; do + ffmpeg -i $f -pix_fmt nv12 -f framehash -hash crc32 - |grep -v '^#' |awk '{print $6}' >$f.crc + ffmpeg -i $f -pix_fmt nv12 -f framehash -hash md5 - |grep -v '^#' |awk '{print $6}' >$f.md5 +done diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.av1.ivf b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.av1.ivf new file mode 100644 index 00000000..83b53b27 Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.av1.ivf differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.av1.ivf.json b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.av1.ivf.json new file mode 100644 index 00000000..195ed7c8 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.av1.ivf.json @@ -0,0 +1,260 @@ +{ + "profile": "AV1PROFILE_PROFILE_MAIN", + "width": 320, + "height": 240, + "frame_rate": 25, + "num_frames": 250, + "num_fragments": 250, + "md5_checksums": [ + "83dab175e49c33a6e3ece5c3758d1bf6", + "cedb4e25453dba430cb6ee830c1643f3", + "ef14d142df162819eee800f3930a9e95", + "11849ccb72cfdabc8f70e33271cd916f", + "7c9856ed61566f399a1eda2243e199ca", + "9394ed10354e986ecdfa2b753b921d2b", + "573005df5ba8f982980c1f427dfb472e", + "a96d9a913b1712f37f73b6c47b577cdf", + "91fc52b076badd1e6cbddb9bca5c5cc3", + "6253dde984bc5282f01005d7ebb55fe9", + "ad88146fe374423e7c597a922049c76c", + "ca7d1ddea7269e476b43a805bd5dd50b", + "e3d1eeec3cbfa363a2222d274890be84", + "fd7902a1b7352e04dc3940a010482a67", + "cada3ee9e33c11c99f89c3f439e2723e", + "1723ee290ae8be930f2904cb5aaf9de4", + "bf48d536e70a6a0a6549e221a833ebe4", + "f13b2e454420b1e0e5ab0adbbf2ce72f", + "31c799b4bb0971b798970e23b906cd25", + "3d925424bf645a225caf64757944a6e5", + "ac55ba724f54dd068350fd2818064dd5", + "a7125b8b0dc1e464e76ee64956897d22", + "f2a00db38d83cc778fa9fbdb3f4515e3", + "c804823f3401558a0283c4e77888af20", + "662d71a06319faef70a0981b664481d2", + "ad3a221a5f1f9d1a615733155a194385", + "8b071335d6cee4b227782415b1cb8a13", + "f3d98bfac8e5083233b88386b539b790", + "739c73e71590e64db0629911039962b4", + "0944990b0ada4012cc686b06833264ef", + "77e306874ec1b0c91668f8df0953831b", + "ca376820c5248cccb221ec8cb4b0eb9e", + "86a72cec3aaf50e393880ac8d4139921", + "b892ef1ff0c169b683b35cbaa9462ee3", + "b5feafe6c294d29adb1b05138803be36", + "3c3b150801f2dc48d300380c9a932890", + "16b06dfb6e426a1b6d88e17de83f5e2c", + "b1633b1c661a645bd1b04317b30fdbfa", + "56d08d66ad8042ba295c3f86025d44cf", + "78f6697fbb3af79dbd614f495c372031", + "afb700399aacde4c8c895def06fd8594", + "e7ae993d18af5c58047196d8369eee3e", + "65d5d77181388229606972ea99f5191a", + "bd336774b22d502d8fdca07d881d737e", + "5cabd4479d94c040f86892fa41860ca9", + "9ab949d5ede2f25889a9612b39f6f158", + "bb54bb8d7782c4b2d63f201a8d338e7d", + "9bf73fc10b4bb19021fd07586a0401d1", + "a37621cda632ac5f58e03272d226e53b", + "5c3ca20aa646d72b69f6e56592fd2f0b", + "b8ff47ec622ce73e70cfc913a3f8116e", + "1d020f74b9d3adf17b3f3da6992fcb52", + "0236c4d4a9aa68ba59eeaeb6b010db26", + "78a8e16aebbe5fc3d1e6051153031a51", + "78a54c1182b8ce007a2c10d17915272d", + "c7ac4f87168bbddd01155f54c79bc132", + "e074be8ece629e08d6d93029b8d34ce2", + "edfba91c624f4cdc558c693b33165542", + "f3d005151909922bfed7a905693de7c0", + "80118fa45af8b3e486928aae49d85b94", + "e7663b97fc9b26348876d5fa64a54d5d", + "69200a2a34bd4beccf4d473b4a4976f3", + "82f6c63a1d87037c08fba3bf16ef9bf8", + "6e3033f25eb56670a3c45c12822f340c", + "6085ac2c582fd3048c240d3dc46e3455", + "b4bbf1a7ae3f57b1d8f0ed65070e010b", + "e7f0aad67af5c7719eb89e457cefe6b4", + "6d5fdc1d68e136ce9095696be59b617b", + "bf15759321de7b21ec3c1bd2cb98ca69", + "fc4d67815a94c2e18e7d8b0b7d866641", + "0d96425a800252d500fd25da7dccb94d", + "5438c9e83cc90f030ee9b1876ec28bdc", + "a88915fd11ad765299cef49c8e65e8ab", + "4995a3442fa74849f3c45888037f9b42", + "82693761e531127d2a98cdc5615cdb92", + "07b9601bebc32dc33fd806f90cee586f", + "d68df0d6b1dae0e4428a17717723dbe0", + "2851422da5bcceebf57ae37a02a1fb57", + "ae9ff5d20ea6ba6105fb5ed3e0109834", + "a0f956663545e54acbaecf684d0841ff", + "23589649e474e155583e6742fa79098c", + "9031fbd94bd7a0508b48f8081018d447", + "b72920bce007e8f0584d615529dda39f", + "0638d4c62dbf43087b4831f2e39314fe", + "7d34a23c9b78a61da7a60c520784e204", + "0a3cf1af9095a7cb2787cf79ed257c59", + "1c05ab5abe793c14ee53ea80e170d6cc", + "02d0b03fe10b1b6675b86390fe317778", + "7639df2f62c4f958a90337b19a591530", + "6080ee3519bd5dd88fbb4050459c126b", + "75ce7282839cda34f7ca6111d6353cdb", + "2214093ad2bb512a223808ed78fe3369", + "eb9bfad6ef08a5f28b531203db3fcb43", + "cf4fd68dd57df907f35cc55fe5930fb8", + "0aaf799fc8509acd1168cd667f789c9c", + "e36ebe2332eb5a9a0ddd03469493d3cb", + "49ec5c818b0fcc02fbe3f5636417a2da", + "8430b81f1a90c325d116cec424d3c32c", + "c67e1e1f7fec9f9ab688f1d54cf2f4d4", + "fd98fabb3db5dc2d841dc50af7395130", + "8af0c8f486057cbae86939fa248c8238", + "e1888b1f32d2991682261f28f860cb27", + "9c0455744df622675a6d5232d790f5b1", + "acc73e6665f54bdaa98acead49be5f65", + "5c6e6167e6c05411a22c348a2c807fc5", + "89d09cb95dc2e212efed266b4b687b35", + "9d35702693b699ac0ccb4bffc559e2e8", + "afbeb04e4e00a1b0a4407b4a34815a63", + "6f3515d4331d83abfd1dbdfb2de78189", + "be6cb0da9149ad5d68269d0ed657cd38", + "9e9f498bc71df3097caa266089a77fba", + "40d9198beea17a254ee6b74544317a7a", + "e2f04358338ae115635e621cf229df56", + "5da22d2d7c5c304fc6198f809206de1d", + "4951c1ad5ea945a098c6a4d7fad163fa", + "5a25300d1ef28b60f9e137b47b2e4364", + "5954dd49f459ea4c0984dba30076db89", + "820a838b57e43271a55f2f17c3e0676f", + "b486477fe004ed8ee213afe8ecdb35c6", + "0715a96ada76fe2d713cdfc2b9abf11d", + "c30544fc39df1c7876043115ceed355f", + "1f7ce18debf0e339db20c68c1f0bf8b8", + "e10229f1f8a95d2e59a086e5d6ac4faa", + "8ad6c73b38d32fd1712a1fd67750d364", + "30fdf5b06ae3fd2c0df9e631ea1e1048", + "0026b1e7db5e25651ecaaf19685b2dc2", + "0b53f3f2ca17d00138b4678580d63ab6", + "cb08de2030a42826ef573082188d3614", + "e14242c776b3a32da936d1f6484e29ba", + "41a43d3ee9021bcda09efa1b218cc65a", + "9b884d243e8b330edb8f569e416960f1", + "5737c4f611cdc9871df094ba51a6a6d5", + "9d47768c1a15c0b20d28545ad5e6ca53", + "8c981af4e6c3432a330b6d8e330bc03a", + "195910783821f51cb3dad54f651c1a75", + "659833dc03dc5f929eb140e188f2b1f6", + "f2f30eaa72c5a093d67cebfda15fac73", + "3c521e3025340933cf826027a994114b", + "469b72a8db2ac77c70a792c8035b0288", + "e2cf1d7930d44341c5c363b0ceaa6c84", + "469d4283cca04e78ac2cb9407531db67", + "02416a6c77174796ecbd61adab5177b9", + "f89077b3dfb54119157e04dff24b79bd", + "2d1a8fc14b649e343288b5972abe1fdf", + "3d059186dab49cd8d13584dc365862db", + "765ef42f457dee16234df6992ec74cd2", + "957589c357d82631477177e6250c713c", + "2473af8395d08e9396216c082f854d6f", + "6a117d16f8e8ffe87aebec05844d6a0a", + "8e467cfcbd66ed80fca648248af56b3f", + "71e39c4304e09688750aa85e9d040f4a", + "3071ee85d87ef8029619d4777c799c2b", + "cc86f09e3f228001e806fd3afe5e1271", + "3828673cf67828240005f7bf9fe37412", + "12700b04cc22fba7688ae1709e48b886", + "a1045aa13b0007d23f22ad8e83c4bdb0", + "a12e6c2d2805336c407417b7503670fe", + "11dd1907c6785eccbedeba6a5708c516", + "3fb8be1276b83db1ccd8206cc69ec736", + "980ce03ae4e6f2eceb314f37290e0dd2", + "c39395d9b6108f1bd4ab1f004c44a5f0", + "a7244c89efd3b00611c92ebaf98793e8", + "fbede5a2957c023216e79cc31ab32946", + "bb40544a3adfe992e07a8eb3e5bf6966", + "0f71f57100699e95fff51dabde86cb42", + "6ba085efefacf385728423ca99bd4629", + "4df2d956234cb2129ebefdd90f28185a", + "d0a0d4263ef4f32d59b004752a5b94af", + "a9deed8bf550f4ee370730e1d993d4dc", + "eca65ba94930bf78854ce991c05e3d9c", + "4eef9fa5bca7ee74e97a0ac6e12e5f53", + "84e97d688ba1922f9ed71d72cea67259", + "407dc974664f93be14c53a2b291cc422", + "31eb9e589470cfbe4e72e1c441d56e3f", + "ea4f696ad2ae150a1f0e622874f67fca", + "7552fea18e053b4771c0490015488291", + "6863cd478581244fff72f6a0021a9fb8", + "9118deacdd49db27c317eeec868cd870", + "50290c87c5ee1c558bd304fc0f4a15ea", + "5aef6d78538151648450fd391c9deaec", + "a7e3feb23e01e0b4556446e01b2d81eb", + "6918645d2b0bd511247c6ed372bf213e", + "2f9661df4d114ab9f3a98a255a8177f2", + "baaea6f9f05a7a2bc10721f1afe6f57b", + "366ab99c236a8e84e658bad69d82a1ba", + "f950a6508c051ef32e4c04be56fcc719", + "9e586c73c3b9daebc608600efbee33b0", + "e9fb7690077e8cbac15d5564130a8d7c", + "ef3c8d2b5376753c26a795bf403cdcc1", + "be46eec37b67dfc8077705ce89588a7c", + "dba1b08cfeddbcf4e8d79621ee343bd4", + "a4a612bcb6c33c7799433c998c5f12d1", + "5f45fa214bc860825510d034f1ff26de", + "1766dd6fe6080c408a4baca135609f1c", + "34dc16d50d7afeb93a874563f1f5e4fd", + "0a5314f217972a9271c4792921f61153", + "7c228f1f5ccc6f7307526531f79a307d", + "188544479f5a44b6a90b6be62e33611a", + "9cb9be5574c6a2c06862a3ed605c68b5", + "845b033cc82472d8e647f2a3bb3eb653", + "2c6b66c68107fd13395a1ed5f0639355", + "0436424e24e562c52ab7e7063b04c129", + "21248608746bb252faa347028f5343d7", + "81607476ae05bbf7386dea9d8eac352b", + "fadc83171e49e779ebe5d81769049cef", + "e768c96238a8cf14caf8d03420981265", + "c57aa4d8e2f7c9101868246de4de1c13", + "b405a01f8eb1d4ffade0e49f593e5a8f", + "76f02bab5b932bf4bcf4ce06bdf0e42c", + "51b09ce7117e15cf654ad76e13263f0c", + "45f650b845e31b87b522574cc7afde80", + "858605e70124ef7130742b713b079ad9", + "5d955329a007742ecab7153d2e69b262", + "c4139450cb6ffcac62b764d62083bc31", + "8b13de43cffa8d12dd17b8e749a375b2", + "b000fbdb48be876302bc336aeb6deba1", + "88e2b7abbc33f70231889113582f8f8c", + "d9f31d4cfeee31da719b3567d5d11a19", + "7c2a6cc9be9ce8cb96b5a1616909941b", + "954c2be78496f0235ab137a98a5ae11a", + "9efd13fe15e8900f67a6c76103c9ce78", + "b04aedbee4993ff955ebba1dcb8a04e1", + "83b045a2a43445f97d82c1faaf33b332", + "1cf416bf7bf4a2124932d4b1bb0aca94", + "ac17d329b587b065512269fa3def8279", + "fcf2b7ab8b7d53741c737f75c3587a6f", + "0aaaaca0aa165a6786e2161043aa1e72", + "8d19a0848fad7b32a15522cc0b24b4d1", + "be0a1be3f90ff47b80be31be021c28f1", + "c9e423c59186d93100c56ad8f14c0fb4", + "052dbb80693750c716acce06f3ad7266", + "6a2c8002df49d3832b7e1abd70af542f", + "92bec3aa91edfd2cc5dee7d1f66ac189", + "579d29110e7a7a71152aedb9a462c79f", + "b0bb435df9dd211dd5da3c547c8887b8", + "fb7a3cf05afd06668287f5dcc9c20a55", + "bbd27b9b555f361478f9516e839077a5", + "a74dc95fe982709b25434bcc53a91973", + "27f82418568440caea8df7bcdf56eae7", + "d5ff54f75c8fa6d60428816945ee5e89", + "4b6677c4cc9866f477ecda5618411c8d", + "d56046f25113a82b090bf9a91291587c", + "cb56aa0f4343c7da821c04e55e8af21a", + "c633ea8a4c20ea4d70c8c241d0293e9b", + "dd2b157ce8a010a61908170ecb3e31b0", + "3c3509c92b03a702ac1f4a28b5abd3d5", + "d1400e54b8b61241cabad5291d6e3a43", + "c29470cd6afb0aee4ae4a8d0622a468d", + "e35764f7bd48746d37478b2d6f3f2755", + "3dcf1fd38fbcc1d45f98574b17ac5710" + ] +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1 new file mode 100644 index 00000000..83b53b27 Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1.crc new file mode 100644 index 00000000..43013f62 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1.crc @@ -0,0 +1,250 @@ +6aea6152 +cb4a90b3 +fd83e35f +074bd081 +216fcc04 +c73ca1e4 +fbfb2a30 +cd587935 +e8bc2912 +051517b7 +f3e9831d +812e7bba +3e2054e6 +7446385e +8b75d043 +f930d9a7 +7bf6b591 +253c5389 +11a25f1f +c5101d08 +ee1c0aae +ce055a9f +0ed4a046 +aa0a72c3 +87f7a598 +aa3a422e +0ee0e533 +5ce3d683 +cc7e88e5 +3e1c7774 +bd6708d3 +0ba6a0ee +ac00d2da +f1222e65 +3c6aadc8 +a28e7327 +fd2d0d8c +e0a791bf +90a33d7e +63ae28ce +26230e31 +215e9021 +6ca1295b +01604116 +15f8d3c9 +6f28a1d4 +8d54633d +08cccd41 +0b082923 +0aaae5a1 +98310e98 +33f12578 +9fd072e0 +36a1220e +a278c894 +828dcf3f +3573505c +861874b7 +dd78b1fd +310ca9a0 +74d670f3 +93c48c50 +814991b1 +fca836a7 +9299e121 +a8d3b267 +661a1c05 +50038462 +bc866fe1 +46e2d826 +7035f78c +00c2ed17 +6b64a0c6 +315f4761 +df4c3ef7 +7ad4996a +3185972f +19af5f17 +01c4e3a3 +3728bd7e +16adc384 +5798878c +d0ac34ee +56f76f8a +2b6f3f47 +4a5ec1a9 +75f0027c +7a1dd1f6 +50c0f14f +978d00e7 +a1c430b8 +7c67432a +4615b4cb +c53c7308 +876d9f98 +b0015dd1 +73288a1e +d843f40a +4127f309 +3adbe013 +9e84104c +9f98680d +380a36e2 +2202f414 +ca931e02 +182b20e1 +815a6be2 +59cc5d53 +0a05d420 +d53b5f2d +98c613aa +e4de4fc1 +2b8f6d29 +48ce9586 +d6e822dd +c8674577 +75dfa0e9 +7ad6ed39 +f7725767 +a44e0f11 +bcd743cd +1ccf1835 +8ceb6153 +0a43af09 +79bab263 +855dd65a +a37d34fe +be063b4d +aec5df0c +7b12399b +c5ad7294 +2f609050 +8bb2dcba +c19aa763 +700eb99b +11bf5174 +e74d5eda +f54e54ef +0173e328 +162372d6 +3c8f18a3 +ce8579c7 +91bdbf86 +745edb27 +df783664 +e7462cfd +30d8d761 +f15474e6 +4c5bb874 +27c36dd2 +c40981c0 +1cc4b7c0 +ebd96a40 +a5f4d31e +cd1a957a +0b0d8e1d +c5d30b99 +d7d6b75e +0ea512b8 +dd12fced +29eab68d +e4bb47d1 +e3060770 +0483e6ca +296b084f +d11abff6 +c7589394 +b187d62e +2e217b3c +b516603b +287c8b71 +2f24a994 +7ea617d0 +7ba0024e +7067ae3e +95650cbd +7b2e7deb +d1b6da6d +76f22a5a +9ce6fac7 +1e5cf6c7 +bbe20b47 +2f2cc450 +3f0297f3 +a2084503 +06487daa +5d06ae48 +02cbba1b +b386b4f4 +47623370 +1acb6e9d +5b55fd00 +70d43ae3 +8a0703c0 +4d62a79b +e358a27d +b89fcec0 +59508160 +f2c9f57d +2b57ec09 +9427c163 +91169e76 +eda7cda1 +289c7bc0 +821dc9d5 +b003d6ce +3d8dbc53 +d6c3324a +76a7b9c9 +a4d4b21b +51ce1f2c +49cc8861 +b4fc2309 +c435862e +3bceba0f +7ee0c219 +cb04facd +b2fad23b +2b8cf4f1 +fcdcbad2 +f3c038c6 +71e639e9 +282325bf +52c64dc8 +9ebe8705 +f8679dc3 +906116a2 +9f740960 +3cad2af8 +c7765bb4 +c8f38650 +1e5d7d36 +3374dc4c +e83476e5 +6a9c0d00 +08b8cb58 +bd74ea9e +399bce4c +1577335d +cdbbea48 +d1373c35 +cc62212a +8a5e59e2 +75f5f297 +8c5f3776 +fce09105 +92b2d17e +416e8530 +968d1450 +d20688a8 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1.md5 new file mode 100644 index 00000000..ad04cf61 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1.md5 @@ -0,0 +1,250 @@ +ce5386b1beaa89f73a668d3720717fb4 +8f5d21bf3d78d9c242609bdd8df6cfd6 +7d75a95c82171c8cf4b09d694d4cbef9 +20d4bebdc51702ee7f40e187747696c7 +9c55dc575db2a189c5576537afca2c54 +68a15a8cdbc0aeaf10dff65db4d1a810 +dfd03584b10852eb16901d52578d8f8e +a9a0cffed57bba344c9366ce91408e26 +9fdf7ac0fc07f2391da56462faf3e2e0 +b83d2da8f82e1a06d26bbb514b471f4b +17c92a58fb68d0b3b02cb5b9105dbb37 +1e5bd3c05fb67f2aefc01611ed43d117 +eb287667a027f3e5325bfb27652021a6 +67e388816af98b99e4b9134516205c52 +35cba13cd8a6f23d6dfd95803af0886b +45966e4a1b83dce6a2b0c259fea10cae +416a95236e46a7bb0f11cf1147631979 +f23ab59d3c4efbb8eb2d80d67a2e5136 +ab7bce374a3468c03483418ccffb52de +bdaaf22c6c4e58d393d7c051d4d0ae8a +6f7de2de3e8d79c726f0f2c2fe52ee58 +d2fc7067797bbffe3765a238ff803b85 +769dbc781b43759287da0b2888e8338b +2abd608af57978290e24c4cd62e4904f +4fb54d7b0a71a4d3ca95b56d7b3d9a81 +262ba91321a0edac67c01e6ddcc2e4f7 +4c9c2d967fb853ecdc6042f223a7d138 +f21ccd6a4eb5f561b0a0eff9cb1956ec +3c206ed277864249e7eaade75e71c815 +7167fed00621472662b19a0215dd00c8 +76e6f142e710ff5d7f6797d3fc1b8dfd +4d814bb1fec0a2bca9d92140d5734ae0 +65b576bf469b182c8954591fd15dd21c +abb7e8f11617aa0c9bf6870e537b008d +02b86ea6b2f8699097d6899ab1f073c4 +610786c84b9fc118679afb09404c15ae +7feb8fae65767f848ebbefa730f15d68 +12677768a4266571e85b750b51ef9fc8 +5b6541b178f6a885d0439b381e501967 +352cc90ecc1f3e26c19970f106c475fa +9c5215a9fce00f0ec30615649171f8e4 +27e593a4b30c0b64939c62e5e0488e5a +d8a5fd54958527ecefc642f6730e4164 +754dfeb4337108e534ba70bab429777a +821ad11c37ffda3ad5438b57a9523e9f +add12a889a5e08a5358e2e0e086fe522 +fa100278f49cbb7f9c9d7dc5e12fc0e1 +ef46a151df454084d0f0bc644adeb47d +c02f8023a4be8a3249bcf6e1b223dffa +41a11da85806e85fa86a9de41e1cd93f +e792d89b5df357d1b03b1812a762832c +90f72aead8124bc6ce51495b4b99da75 +af25c00b1990b778b50f529ae81efa30 +900f66f0e222a8d253153711a23a5ee9 +0850cad30d59e1b5e5e2943e6b916d0a +03ca961e9876ee96b14711c44a9f2a69 +fa6bf9c30dd8618fdab499e9dd217582 +4e09e08e1f99bf210d4757281ab4c836 +dbf2bc5355a6846a028a7b3140fdef34 +b38e1973cabab58f8137cf66fc7fa3aa +0ee4998cda155ca2823f5c9336a62cc2 +4f9918b4782a59a0361619389bd22d54 +c5c82a59505a9888b95e012a068b3254 +5eabc71dca6f57407ba306db416c5031 +41b8b1155bf6144aeb822acbff051e1a +12e7d40ccd4298400cf3b5577370bff8 +2f22b7979183c9c3b26838c7e84a49a8 +68af2e17a487001d9374f6b38ed950af +3a55bc482844ecc366e10544c38cc890 +6a08d19bbecb554837c9922b9818455a +b2f95f48c1214a5dfb8f4528afcbf901 +c3c4fc391a1a040c71cb65a8c654a5b1 +86786c55d138bb9a4a4165522cd15253 +b329ece66a9dab6bebab9e3687629e7c +7d6d818e7fe9a9b335c72ee6aedf9980 +317956c14cb119eda584de03dbf089cc +d095660e8dd16899be9e916e0ff7946c +12571880bd56bed1bbbd034094fabbaa +7f702375e8c43036e36ddc07b543afb2 +e254d4c9854b28db694fb5477144551e +24a4b250bc0461ad4894334838fbb645 +16b3dca0bfcad463498a62bf8a3f06a8 +77dbe27a373d87a6fc1012caa166a0a5 +1b304ea04baf90e1bf11d6296f64f933 +42c94efb8ab8ceb6ff98497ef0981c66 +1074398b6c210e833b4135c7d2caeb91 +3bce8712a7ba7b037a2cc85e4f72aea4 +42c1bc84350e82cbead374b41732d8d5 +76f8fc8e26d1b64e8afb8f263ec3f795 +ef9f8b042eb888f5408ccb500c9a157f +d46505a331887bf49e157f8de4c17a98 +0be7df49394337bb277b63a6b9147520 +deb977415048b0b7fa1fd02878c1b10e +a0395e32b4cc279f8545b791cee5d394 +c7dcd8741840799886a1f3b5208c0b36 +02b1297f58b9e699f232b9ed7e8bbcd0 +48852b209e405d7aaab50c5810448579 +d8a5598b2e7183d72cd3a81e54f301ce +dfe2e4af7518f0d1f04fd825069d575b +e1a70679c95173e6a1504b7c52dd0d3c +16ae515dfb1268052bfe330a1cbbbffa +f17c090f2140cfd3060558ceefe4d1bd +58b1ba1fbbd76670c9218a846a8f6baf +f543d8380c2d095f9b62d4ffaaee7467 +1b16d3e1d688a5a905ff22680ad803cb +12e16d58422d5a5899379f11c6ca5b9a +f1986c3e4a4545356656f76c27db0ae7 +324a3d45f631aed6c758888254ae07ad +49532c06798f685e2a41dc683b081f79 +95934c5a6e2cc8de08c8b8342936cf38 +67d89790392f18908c24cc36bbe50362 +c4f963cd490f9241f4959cca6a6d35ce +b21db693ba10898ec3205cb0cb343a02 +72e63227e74f2473a44da365fa9bc02f +220d247b6c0f9512256dfcdf95582148 +86c290279ffa61ca615f1f8f8877e807 +6651d56c548fd4c5e8b2c86354c28ca3 +d863321ec53cd582d34a8ee5e88a5576 +f262bda775cf67026d8d226620c9d4aa +15816d939647890021ac31aa1c3b2f75 +229b295a4bc44d8522580dd4383812d0 +833afe87641e34e60f2d127f44a7cbeb +62cc76ec3a853878ebfff04ebbc8addc +40fca93e2d6a976de641cdbfb56257b5 +65d654e9145527f8fcd24608d64f68f1 +1b2b0c68b6d9305f46642109b92a234b +85aa859b29f2caeb60343cf53718e451 +3aa590c6c8a4dee128f1e7fcb2b82a2a +699cb26687934746b722e2dfbc1d2c82 +9cc9b7f51cbdbaf15bc8eec9c1a1cf7c +b1be900fe6a6eb21a563754420c5f38e +47976686aff65b42661ae0fb2685f462 +6b3f96a1be97c4c2a371d5935672096a +ac9b60c754c95ccbaf56f68544712dd6 +7dab91b68788d2ed61cd40d3073091a8 +35b367c591e71f11ddcd9dcc75be79e7 +d3321d793265630e81e6d52b20e58f44 +9d97047182efd2ba0594a569f6f2ce04 +14c772a342a28bd6785bb722e67fc767 +288802e107c373c9d28eeb3f8726214e +1c9d8ff04c346a59ea4f3a349dc4b891 +2c857a65df4619d06f424ddcccbd2221 +10a0b3e4e7825d21fc22bcff34a3b26c +78c8a5ee6e6880a3af222bfc27de62a1 +9e16c4346a69ff9127bb6c9287d98859 +1f1d95fa30c9c3a58f41d9657aa0e6ec +5e8c6b3b4690da7f4175abe288d0c509 +0bf315da2ec1e135963cc4c9cf73ea1a +2992fdf295438ec9f8dc7979b6d4e5ff +a3d43afab0119aa3b6e38a81d244c388 +889d049dc64457637ad089e18e718a23 +46016c611b9323fe8f4206bd393a6741 +f555b79be75ba2a1a741f467192b693e +242d4b4c323c7c1e9648de52ec8d7c2c +383d2ee4d28919f193f28448ba28ddc6 +2c7cd221bd1025547fc654ca6717553d +e16de72e05b548758832102ffb39d242 +5cecf3fc2823225f0f1748a1fedadf35 +4e709cb904317a43ffd94739281d32bc +b6dfd4657668ed2c1ebaff9fa92702ab +3c618cd95706cc25a0565fc0ed00e352 +dca8dbc91708a94525f16c064454a2ce +fe3fbff45e30dcc3fa4f57ae78114f7f +0f7bc1eab5cf9faeaa127c0243675253 +19b6fca6e3f0d039074753265436d2c5 +fce950fc2ec2526c0b29b6869ff58dee +4c9c0b19668dc20a9fac84e13c590deb +8fe06eb38ee870ffd7cb9f065476ac78 +515ce9eaa9a6970f809475e416325123 +6d58868e827dd9dca698c5b24380fea7 +072951a0e1d06aa829d716a86ae498fe +07f23add152c44f754eac930093e6b59 +e98798eb29c162ddfff7ec72aa7b65dc +62f4e1ca0294a3944224fb4a4280ed56 +28fc910d3b7a926f63e7bfaf18878cc1 +40065fe5c0e32a16622988bc1485a038 +b2623ec75309e28c6c0a4b179e4acd6d +3ea6b991014bde2185be7a6c92745c28 +5fde25c7fe79a9c0e2b86eb1878de02c +5bf42dc3006f4c601d01ee4820e44d38 +44500b37f18b2eb499894bc1ff1efdcc +f5bfe3352c4281083083e51ef1fe0a72 +abf9c6ba639f68d9b8f8c0d9d97a19d3 +75f6dc9ccbc6a1381785bf9440a95744 +4db0fd27cac20f59718fe00c5dc13202 +9e0d611017698f2d1716f8d9d81a8bf7 +13c7015f10b37b3dbe1618cde5edc2ee +70e2a0182471290a46961a0d62611053 +47ba942fc3396104b3731ea40eb97339 +bf10238458969233c0062b09a68f22a7 +115b7375a9aa156b8203dd88acdc5672 +1b39aa22702c993aa1e7606e9afad9c5 +c85e67c08e02538f151c6276662ba69b +89dcf41c5adb944bcd46ca3f67666a75 +90fd81f9b572ede873313f4faa58da4b +00badd56e7d5b2dddb10ef7de05be515 +a13c3404ac033c9fce8a6435f2a4a416 +af83e8f7af9785c96ff61c354417aab4 +5176b27945cc7686520b1425144dc4d3 +e5f248be2c0e83ad0a6bfd2eff759f6e +cd693cce87898dafa5c68dbaeeb151fa +2a47245fc3da1229e3dc288f216c4e20 +c6f8e88fbf5566000f8f86b0d4bf9020 +3e88c5f7b571a292c998cac33b41215b +2a2e8271ecba2cec040ef343317e2ae9 +8e693282602bde3452061affde3ce514 +992acab07cf4d6aa5b95b4c4cc33cf0c +6db1ad3494be9c3c9758b00d956bd2cb +824d130a16d7bc2c6d6b26c31c2a0e05 +d286bd8f5cdfd65b695815ff68bb191f +4c5bbeecda37bf4fb6dd79f266aac3d6 +c4f64e991e13dfc3913246f730c31e87 +459d8fd53975479aa977c61dc3ef1b9e +ece59558539b52684433b54bf2c7e214 +4362122d7ed40dd5ad0886cdf901539e +66ffe4fb31a5e4fa9462a3aa57224476 +86586a04e75ebde21903f407e6037e50 +93b2e3af91cb24ffafa6418c76cc1ce7 +41ca4598aac764fa944d8f578b2ab5ab +6ef7ce05836d63520925817c220a5274 +c4e902503f3d5602ba2454907d7809f7 +8f3cb447682bebd3a230740e11d35dff +4340ec4c3e62118b2749264393fc78ee +8178fbeab83126f2111ab74c23d241c9 +db298bcb2a999b7ac2037991ef078d32 +f586895d6dca606e2d62dfdc92cc2021 +b5aaf4b91dd7b6cda3a2635c603c4500 +0f024b59afa957b59f5ebb9b63ff1b1a +41acc378b08b5d9b7da2f644b79aebdf +cd173062829e6e86eb094b8d40d16579 +ef55ddf0fa94dcb4f275033bb55e8466 +48a3efa1f9d8291d99ce434fc5474fb6 +731a649c764fa964d6e2ce640b34c25a +5f9ac7b1dca56c8dab9ce1f92341094b +e9b0ee7355299aaf55cce1e240050125 +8a285fc7c728cdfa1f4b651c64eab7e5 +05823b86e463ad514a4cd7eacd9c01bb +0ce34318119eb0e8921b5e798789bea0 +e46f4c793b42136b1eefbece7b867052 +1331c7fcae627f290299b978299a9b48 +285c5783029a7b23374e91cce19df0de +670fba4fad181308c15778f38e619646 +46a3350a39c21f45bafed75d339ed101 +b1d6f3230e58d2647d1aa4a5446061ed +f6eb5936ec9bb3212b14716da8329839 +1ecb00343900a550ad33ba10777df23d +4b9f70244adda8e0a42f4da7341b8d84 +6caafeb26793a011ebef352ef1d1aa34 +723ab0797281e7ee36109b53140a324d +7a5e9be8c7c8307aa22c8b48708424a2 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/writer.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/writer.rs new file mode 100644 index 00000000..fcc8fa4d --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/writer.rs @@ -0,0 +1,203 @@ +// Copyright 2024 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::fmt; +use std::io::Write; + +use crate::bitstream_utils::BitWriter; +use crate::bitstream_utils::BitWriterError; + +#[derive(Debug)] +pub enum ObuWriterError { + BitWriterError(BitWriterError), + UnalignedLeb128, +} + +impl fmt::Display for ObuWriterError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ObuWriterError::BitWriterError(x) => write!(f, "{}", x.to_string()), + ObuWriterError::UnalignedLeb128 => { + write!(f, "attempted to write leb128 on unaligned position") + } + } + } +} + +impl From for ObuWriterError { + fn from(err: BitWriterError) -> Self { + ObuWriterError::BitWriterError(err) + } +} + +pub type ObuWriterResult = std::result::Result; + +pub struct ObuWriter(BitWriter); + +impl ObuWriter { + pub fn new(writer: W) -> Self { + Self(BitWriter::new(writer)) + } + + /// Writes fixed bit size integer. Corresponds to `f(n)` in AV1 spec defined in 4.10.2. + pub fn write_f>(&mut self, bits: usize, value: T) -> ObuWriterResult { + self.0 + .write_f(bits, value) + .map_err(ObuWriterError::BitWriterError) + } + + /// Writes variable length unsigned n-bit number. Corresponds to `uvlc()` in AV1 spec + /// defined in 4.10.3. + pub fn write_uvlc>(&mut self, value: T) -> ObuWriterResult { + let value: u32 = value.into(); + if value == u32::MAX { + return self.write_f(32, 0u32); + } + + let value = value + 1; + let leading_zeros = (32 - value.leading_zeros()) as usize; + + Ok(self.write_f(leading_zeros - 1, 0u32)? + self.write_f(leading_zeros, value)?) + } + + /// Writes unsigned little-endian n-byte integer. Corresponds to `le(n)` in AV1 spec + /// defined in 4.10.4. + pub fn write_le>(&mut self, n: usize, value: T) -> ObuWriterResult { + let value: u32 = value.into(); + let mut value = value.to_le(); + + for _ in 0..n { + self.write_f(4, value & 0xff)?; + value >>= 8; + } + + Ok(n) + } + + /// Writes unsigned integer represented by a variable number of little-endian bytes. + /// Corresponds to `leb128()` in AV1 spec defined in 4.10.4. + /// + /// Note: Despite the name, the AV1 4.10.4 limits the value to [`u32::MAX`] = (1 << 32) - 1. + pub fn write_leb128>( + &mut self, + value: T, + min_bytes: usize, + ) -> ObuWriterResult { + if !self.aligned() { + return Err(ObuWriterError::UnalignedLeb128); + } + + let value: u32 = value.into(); + let mut value: u32 = value.to_le(); + let mut bytes = 0; + + for _ in 0..8 { + bytes += 1; + + if value >= 0x7f || bytes < min_bytes { + self.write_f(8, 0x80 | (value & 0x7f))?; + value >>= 7; + } else { + self.write_f(8, value & 0x7f)?; + break; + } + } + + assert!(value < 0x7f); + + Ok(bytes) + } + + pub fn write_su>(&mut self, bits: usize, value: T) -> ObuWriterResult { + let mut value: i32 = value.into(); + if value < 0 { + value += 1 << bits; + } + + assert!(value >= 0); + self.write_f(bits, value.unsigned_abs()) + } + + pub fn aligned(&self) -> bool { + !self.0.has_data_pending() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::codec::av1::reader::Reader; + + const TEST_VECTOR: &[u32] = &[ + // some random test values + u32::MAX, + 1, + 2, + 3, + 4, + 10, + 20, + 7312, + 8832, + 10123, + 47457, + 21390213, + u32::MIN, + u32::MAX - 1, + ]; + + #[test] + fn test_uvlc() { + for &value in TEST_VECTOR { + let mut buf = Vec::::new(); + + ObuWriter::new(&mut buf).write_uvlc(value).unwrap(); + + if value == u32::MAX { + // force stop uvlc + buf.push(0x80); + } + + let read = Reader::new(&buf).read_uvlc().unwrap(); + + assert_eq!(read, value, "failed testing {}", value); + } + } + + #[test] + fn test_leb128() { + for &value in TEST_VECTOR { + let mut buf = Vec::::new(); + + ObuWriter::new(&mut buf).write_leb128(value, 0).unwrap(); + let read = Reader::new(&buf).read_leb128().unwrap(); + + assert_eq!(read, value, "failed testing {}", value); + } + } + + #[test] + fn test_su() { + let vector = TEST_VECTOR + .iter() + .map(|e| *e as i32) + .chain(TEST_VECTOR.iter().map(|e| -(*e as i32))); + + for value in vector { + let bits = 32 - value.abs().leading_zeros() as usize + 1; // For sign + if bits >= 32 { + // Skip too big nubmers + continue; + } + + let mut buf = Vec::::new(); + + ObuWriter::new(&mut buf).write_su(bits, value).unwrap(); + + let read = Reader::new(&buf).read_su(bits as usize).unwrap(); + + assert_eq!(read, value, "failed testing {}", value); + } + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264.rs new file mode 100644 index 00000000..fc13025e --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264.rs @@ -0,0 +1,10 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +pub mod dpb; +pub mod nalu; +pub mod nalu_writer; +pub mod parser; +pub mod picture; +pub mod synthesizer; diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/dpb.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/dpb.rs new file mode 100644 index 00000000..e9b8ac39 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/dpb.rs @@ -0,0 +1,1308 @@ +// Copyright 2022 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::cell::Ref; +use std::cell::RefMut; +use std::fmt; +use std::rc::Rc; + +use log::debug; + +use crate::codec::h264::parser::MaxLongTermFrameIdx; +use crate::codec::h264::parser::RefPicMarkingInner; +use crate::codec::h264::parser::Sps; +use crate::codec::h264::picture::Field; +use crate::codec::h264::picture::FieldRank; +use crate::codec::h264::picture::IsIdr; +use crate::codec::h264::picture::PictureData; +use crate::codec::h264::picture::RcPictureData; +use crate::codec::h264::picture::Reference; + +pub type DpbPicRefList<'a, H> = Vec<&'a DpbEntry>; + +/// All the reference picture lists used to decode a picture. +#[derive(Default)] +pub struct ReferencePicLists { + /// Reference picture list for P slices. Retains the same meaning as in the + /// specification. Points into the pictures stored in the DPB. Derived once + /// per picture. + pub ref_pic_list_p0: Vec, + /// Reference picture list 0 for B slices. Retains the same meaning as in + /// the specification. Points into the pictures stored in the DPB. Derived + /// once per picture. + pub ref_pic_list_b0: Vec, + /// Reference picture list 1 for B slices. Retains the same meaning as in + /// the specification. Points into the pictures stored in the DPB. Derived + /// once per picture. + pub ref_pic_list_b1: Vec, +} + +/// A single entry in the DPB. +#[derive(Clone)] +pub struct DpbEntry { + /// `PictureData` of the frame in this entry. + pub pic: RcPictureData, + /// Reference to the decoded frame, ensuring that it doesn't get reused while in the DPB. + pub reference: Option, + /// Decoded frame promise. It will be set when the frame enters the DPB, and taken during the + /// bump process. + pub decoded_frame: Option, + /// Whether the picture is still waiting to be bumped and displayed. + needed_for_output: bool, +} + +impl DpbEntry { + /// Returns `true` is the entry is eligible to be bumped. + /// + /// An entry can be bumped if its `needed_for_output` flag is true and it is the first field of + /// a frame which fields are all decoded. + fn is_bumpable(&self) -> bool { + if !self.needed_for_output { + return false; + } + + let pic = self.pic.borrow(); + match pic.field { + // Progressive frames in the DPB are fully decoded. + Field::Frame => true, + // Only return the first field of fully decoded interlaced frames. + Field::Top | Field::Bottom => matches!(pic.field_rank(), FieldRank::First(..)), + } + } +} + +pub struct Dpb { + /// List of `PictureData` and backend handles to decoded pictures. + entries: Vec>, + /// The maximum number of pictures that can be stored. + max_num_pics: usize, + /// Indicates an upper bound for the number of frames buffers, in the + /// decoded picture buffer (DPB), that are required for storing frames, + /// complementary field pairs, and non-paired fields before output. It is a + /// requirement of bitstream conformance that the maximum number of frames, + /// complementary field pairs, or non-paired fields that precede any frame, + /// complementary field pair, or non-paired field in the coded video + /// sequence in decoding order and follow it in output order shall be less + /// than or equal to max_num_reorder_frames. + max_num_reorder_frames: usize, + /// Whether we're decoding in interlaced mode. Interlaced support is + /// inspired by the GStreamer implementation, in which frames are split if + /// interlaced=1. This makes reference marking easier. We also decode both + /// fields to the same frame, and this frame with both fields is outputted + /// only once. + interlaced: bool, +} + +#[derive(Debug)] +pub enum StorePictureError { + DpbIsFull, +} + +impl fmt::Display for StorePictureError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "DPB is full") + } +} + +impl std::error::Error for StorePictureError {} + +#[derive(Debug)] +pub enum MmcoError { + NoShortTermPic, + ExpectedMarked, + ExpectedExisting, + UnknownMmco(u8), +} + +impl fmt::Display for MmcoError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MmcoError::NoShortTermPic => { + write!(f, "could not find ShortTerm picture to mark in the DPB") + } + MmcoError::ExpectedMarked => { + write!( + f, + "a ShortTerm picture was expected to be marked for MMCO=3" + ) + } + MmcoError::ExpectedExisting => { + write!(f, "picture cannot be marked as nonexisting for MMCO=3") + } + MmcoError::UnknownMmco(x) => write!(f, "unknown MMCO: {}", x), + } + } +} + +impl std::error::Error for MmcoError {} + +impl Dpb { + /// Returns an iterator over the underlying H264 pictures stored in the + /// DPB. + fn pictures(&self) -> impl Iterator> { + self.entries.iter().map(|h| h.pic.borrow()) + } + + /// Returns a mutable iterator over the underlying H264 pictures stored in + /// the DPB. + fn pictures_mut(&mut self) -> impl Iterator> { + self.entries.iter().map(|h| h.pic.borrow_mut()) + } + + /// Returns the length of the DPB. + pub fn len(&self) -> usize { + self.entries.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Get a reference to the whole DPB entries. + pub fn entries(&self) -> &Vec> { + &self.entries + } + + /// Set the DPB's limits in terms of maximum number or pictures. + pub fn set_limits(&mut self, max_num_pics: usize, max_num_reorder_frames: usize) { + self.max_num_pics = max_num_pics; + self.max_num_reorder_frames = max_num_reorder_frames; + } + + /// Get a reference to the dpb's max num pics. + pub fn max_num_pics(&self) -> usize { + self.max_num_pics + } + + // Returns the number of reference frames, counting the first field only if + // dealing with interlaced content. + pub fn num_ref_frames(&self) -> usize { + self.pictures() + .filter(|p| p.is_ref() && !p.is_second_field()) + .count() + } + + /// Get a reference to the dpb's interlaced mode. + pub fn interlaced(&self) -> bool { + self.interlaced + } + + /// Set the dpb's interlaced mode. + pub fn set_interlaced(&mut self, interlaced: bool) { + self.interlaced = interlaced; + } + + /// Find the short term reference picture with the lowest `frame_num_wrap` + /// value. + pub fn find_short_term_lowest_frame_num_wrap(&self) -> Option<&DpbEntry> { + let lowest = self + .entries + .iter() + .filter(|h| { + let p = h.pic.borrow(); + matches!(p.reference(), Reference::ShortTerm) + }) + .min_by_key(|h| { + let p = h.pic.borrow(); + p.frame_num_wrap + }); + + lowest + } + + /// Mark all pictures in the DPB as unused for reference. + pub fn mark_all_as_unused_for_ref(&mut self) { + for mut picture in self.pictures_mut() { + picture.set_reference(Reference::None, false); + } + } + + /// Remove unused pictures from the DPB. A picture is not going to be used + /// anymore if it's a) not a reference and b) not needed for output + fn remove_unused(&mut self) { + self.entries.retain(|entry| { + let pic = entry.pic.borrow(); + let discard = !pic.is_ref() && !entry.needed_for_output; + + if discard { + log::debug!("Removing unused picture {:#?}", pic); + } + + !discard + }); + } + + /// Find a short term reference picture with the given `pic_num` value. + fn find_short_term_with_pic_num_pos(&self, pic_num: i32) -> Option { + let position = self + .pictures() + .position(|p| matches!(p.reference(), Reference::ShortTerm) && p.pic_num == pic_num); + + log::debug!( + "find_short_term_with_pic_num: {}, found position {:?}", + pic_num, + position + ); + + position + } + + /// Find a short term reference picture with the given `pic_num` value. + pub fn find_short_term_with_pic_num(&self, pic_num: i32) -> Option<&DpbEntry> { + let position = self.find_short_term_with_pic_num_pos(pic_num)?; + Some(&self.entries[position]) + } + + /// Find a long term reference picture with the given `long_term_pic_num` + /// value. + fn find_long_term_with_long_term_pic_num_pos(&self, long_term_pic_num: u32) -> Option { + let position = self.pictures().position(|p| { + matches!(p.reference(), Reference::LongTerm) && p.long_term_pic_num == long_term_pic_num + }); + + log::debug!( + "find_long_term_with_long_term_pic_num: {}, found position {:?}", + long_term_pic_num, + position + ); + + position + } + + /// Find a long term reference picture with the given `long_term_pic_num` + /// value. + pub fn find_long_term_with_long_term_pic_num( + &self, + long_term_pic_num: u32, + ) -> Option<&DpbEntry> { + let position = self.find_long_term_with_long_term_pic_num_pos(long_term_pic_num)?; + Some(&self.entries[position]) + } + + /// Store `picture` and its backend handle in the DPB. + pub fn store_picture( + &mut self, + picture: RcPictureData, + handle: Option, + ) -> Result<(), StorePictureError> { + let max_pics = if self.interlaced { + self.max_num_pics * 2 + } else { + self.max_num_pics + }; + + if self.entries.len() >= max_pics { + return Err(StorePictureError::DpbIsFull); + } + + let pic = picture.borrow(); + + // C.4.2. Decoding of gaps in frame_num and storage of "non-existing" + // pictures + let needed_for_output = !pic.nonexisting; + + debug!( + "Stored picture POC {:?}, field {:?}, the DPB length is {:?}", + pic.pic_order_cnt, + pic.field, + self.entries.len() + ); + drop(pic); + + self.entries.push(DpbEntry { + pic: picture, + reference: handle.clone(), + decoded_frame: handle, + needed_for_output, + }); + + Ok(()) + } + + /// Whether the DPB has an empty slot for a new picture. + pub fn has_empty_frame_buffer(&self) -> bool { + let count = if !self.interlaced { + self.entries.len() + } else { + self.pictures() + .filter(|pic| { + matches!(pic.field_rank(), FieldRank::First(..)) + || (matches!(pic.field_rank(), FieldRank::Single) + && pic.field == Field::Frame) + }) + .count() + }; + + count < self.max_num_pics + } + + /// Whether the DPB needs bumping, as described by clauses 1, 4, 5, 6 of + /// C.4.5.3 "Bumping" process. + pub fn needs_bumping(&self, to_insert: &PictureData) -> bool { + // In C.4.5.3 we handle clauses 2 and 3 separately. All other clauses + // check for an empty frame buffer first. Here we handle: + // - There is no empty frame buffer and a empty frame buffer is + // needed for storage of an inferred "non-existing" frame. + // + // - There is no empty frame buffer and an empty frame buffer is + // needed for storage of a decoded (non-IDR) reference picture. + // + // - There is no empty frame buffer and the current picture is a non- + // reference picture that is not the second field of a complementary + // non-reference field pair and there are pictures in the DPB that + // are marked as "needed for output" that precede the current + // non-reference picture in output order. + // + // Clauses 2 and 3 are handled by H264Codec::handle_picture and + // H264Codec::finish_picture, respectively. + if self.has_empty_frame_buffer() { + return false; + } + + if to_insert.nonexisting { + return true; + } + + let non_idr_ref = to_insert.is_ref() && matches!(to_insert.is_idr, IsIdr::No); + if non_idr_ref { + return true; + } + + let lowest_poc = match self.find_lowest_poc_for_bumping() { + Some(handle) => handle.pic.borrow().pic_order_cnt, + None => return false, + }; + + !to_insert.is_second_field_of_complementary_ref_pair() + && to_insert.pic_order_cnt > lowest_poc + } + + /// Find the lowest POC in the DPB that can be bumped. + fn find_lowest_poc_for_bumping(&self) -> Option<&DpbEntry> { + self.entries + .iter() + .filter(|entry| entry.is_bumpable()) + .min_by_key(|handle| handle.pic.borrow().pic_order_cnt) + } + + /// Find the lowest POC in the DPB that can be bumped and return a mutable reference. + fn find_lowest_poc_for_bumping_mut(&mut self) -> Option<&mut DpbEntry> { + self.entries + .iter_mut() + .filter(|entry| entry.is_bumpable()) + .min_by_key(|handle| handle.pic.borrow().pic_order_cnt) + } + + /// Bump the dpb, returning a picture as per the bumping process described in C.4.5.3. + /// Note that this picture will still be referenced by its pair, if any. + fn bump(&mut self) -> Option> { + let dpb_entry = self.find_lowest_poc_for_bumping_mut()?; + let handle = dpb_entry.decoded_frame.take(); + let pic = dpb_entry.pic.borrow(); + + debug!("Bumping picture {:#?} from the dpb", pic); + + dpb_entry.needed_for_output = false; + // Lookup the second field entry and flip as well. + // `find_lowest_poc_for_bumping_mut` always returns the first field, never the second. + if let FieldRank::First(second_field) = pic.field_rank() { + let second_field = second_field.upgrade(); + drop(pic); + if let Some(second_field) = + second_field.and_then(|f| self.entries.iter_mut().find(|e| Rc::ptr_eq(&f, &e.pic))) + { + second_field.needed_for_output = false; + } + } + + Some(handle) + } + + /// Drains the DPB by continuously invoking the bumping process. + pub fn drain(&mut self) -> Vec> { + debug!("Draining the DPB."); + + let mut pics = vec![]; + + while let Some(pic) = self.bump() { + pics.push(pic); + } + + self.clear(); + + pics + } + + /// Clears the DPB, dropping all the pictures. + pub fn clear(&mut self) { + debug!("Clearing the DPB"); + + let max_num_pics = self.max_num_pics; + let interlaced = self.interlaced; + + *self = Default::default(); + + self.max_num_pics = max_num_pics; + self.interlaced = interlaced; + } + + /// Returns an iterator of short term refs. + pub fn short_term_refs_iter(&self) -> impl Iterator> { + self.entries + .iter() + .filter(|&handle| matches!(handle.pic.borrow().reference(), Reference::ShortTerm)) + } + + /// Returns an iterator of long term refs. + pub fn long_term_refs_iter(&self) -> impl Iterator> { + self.entries + .iter() + .filter(|&handle| matches!(handle.pic.borrow().reference(), Reference::LongTerm)) + } + + pub fn update_pic_nums( + &mut self, + frame_num: u32, + max_frame_num: u32, + current_pic: &PictureData, + ) { + for mut pic in self.pictures_mut() { + if !pic.is_ref() { + continue; + } + + if *pic.reference() == Reference::LongTerm { + pic.long_term_pic_num = if current_pic.field == Field::Frame { + pic.long_term_frame_idx + } else if current_pic.field == pic.field { + 2 * pic.long_term_frame_idx + 1 + } else { + 2 * pic.long_term_frame_idx + }; + } else { + pic.frame_num_wrap = if pic.frame_num > frame_num { + pic.frame_num as i32 - max_frame_num as i32 + } else { + pic.frame_num as i32 + }; + + pic.pic_num = if current_pic.field == Field::Frame { + pic.frame_num_wrap + } else if pic.field == current_pic.field { + 2 * pic.frame_num_wrap + 1 + } else { + 2 * pic.frame_num_wrap + }; + } + } + } + + /// Bumps the DPB if needed. DPB bumping is described on C.4.5.3. + pub fn bump_as_needed(&mut self, current_pic: &PictureData) -> Vec> { + let mut pics = vec![]; + while self.needs_bumping(current_pic) && self.len() >= self.max_num_reorder_frames { + match self.bump() { + Some(pic) => pics.push(pic), + None => return pics, + } + self.remove_unused(); + } + + pics + } + + // 8.2.5.3 + pub fn sliding_window_marking(&mut self, pic: &mut PictureData, sps: &Sps) { + // If the current picture is a coded field that is the second field in + // decoding order of a complementary reference field pair, and the first + // field has been marked as "used for short-term reference", the current + // picture and the complementary reference field pair are also marked as + // "used for short-term reference". + if let FieldRank::Second(other_field) = pic.field_rank() { + if matches!(other_field.borrow().reference(), Reference::ShortTerm) { + pic.set_reference(Reference::ShortTerm, false); + return; + } + } + + let mut num_ref_pics = self.num_ref_frames(); + let max_num_ref_frames = std::cmp::max(1, sps.max_num_ref_frames as usize); + + if num_ref_pics < max_num_ref_frames { + return; + } + + while num_ref_pics >= max_num_ref_frames { + if let Some(to_unmark) = self.find_short_term_lowest_frame_num_wrap() { + to_unmark + .pic + .borrow_mut() + .set_reference(Reference::None, true); + num_ref_pics -= 1; + } else { + log::warn!("could not find a ShortTerm picture to unmark in the DPB"); + break; + } + } + + self.remove_unused(); + } + + pub fn mmco_op_1( + &mut self, + pic: &PictureData, + marking: &RefPicMarkingInner, + ) -> Result<(), MmcoError> { + let pic_num_x = pic.pic_num - (marking.difference_of_pic_nums_minus1 as i32 + 1); + + log::debug!("MMCO op 1 for pic_num_x {}", pic_num_x); + log::trace!("Dpb state before MMCO=1: {:#?}", self); + + let to_mark = self + .find_short_term_with_pic_num(pic_num_x) + .ok_or(MmcoError::NoShortTermPic)?; + + to_mark + .pic + .borrow_mut() + .set_reference(Reference::None, matches!(pic.field, Field::Frame)); + + Ok(()) + } + + pub fn mmco_op_2( + &mut self, + pic: &PictureData, + marking: &RefPicMarkingInner, + ) -> Result<(), MmcoError> { + log::debug!( + "MMCO op 2 for long_term_pic_num {}", + marking.long_term_pic_num + ); + + log::trace!("Dpb state before MMCO=2: {:#?}", self); + + let to_mark = self + .find_long_term_with_long_term_pic_num(marking.long_term_pic_num) + .ok_or(MmcoError::NoShortTermPic)?; + + to_mark + .pic + .borrow_mut() + .set_reference(Reference::None, matches!(pic.field, Field::Frame)); + + Ok(()) + } + + pub fn mmco_op_3( + &mut self, + pic: &PictureData, + marking: &RefPicMarkingInner, + ) -> Result<(), MmcoError> { + let pic_num_x = pic.pic_num - (marking.difference_of_pic_nums_minus1 as i32 + 1); + + log::debug!("MMCO op 3 for pic_num_x {}", pic_num_x); + log::trace!("Dpb state before MMCO=3: {:#?}", self); + + let to_mark_as_long_pos = self + .find_short_term_with_pic_num_pos(pic_num_x) + .ok_or(MmcoError::NoShortTermPic)?; + let to_mark_as_long = &self.entries[to_mark_as_long_pos].pic; + + if !matches!(to_mark_as_long.borrow().reference(), Reference::ShortTerm) { + return Err(MmcoError::ExpectedMarked); + } + + if to_mark_as_long.borrow().nonexisting { + return Err(MmcoError::ExpectedExisting); + } + + let to_mark_as_long_ptr = to_mark_as_long.as_ptr(); + let to_mark_as_long_other_field_ptr = + to_mark_as_long.borrow().other_field().map(|f| f.as_ptr()); + + let long_term_frame_idx = marking.long_term_frame_idx; + + for mut picture in self.pictures_mut() { + let long_already_assigned = matches!(picture.reference(), Reference::LongTerm) + && picture.long_term_frame_idx == long_term_frame_idx; + + if long_already_assigned { + let is_frame = matches!(picture.field, Field::Frame); + + let is_complementary_field_pair = picture + .other_field() + .map(|f| { + let pic = f.borrow(); + matches!(pic.reference(), Reference::LongTerm) + && pic.long_term_frame_idx == long_term_frame_idx + }) + .unwrap_or(false); + + // When LongTermFrameIdx equal to + // long_term_frame_idx is already assigned to a + // long-term reference frame or a long-term + // complementary reference field pair, that frame or + // complementary field pair and both of its fields + // are marked as "unused for reference" + if is_frame || is_complementary_field_pair { + picture.set_reference(Reference::None, true); + break; + } + + // When LongTermFrameIdx is already assigned to a + // reference field, and that reference field is not + // part of a complementary field pair that includes + // the picture specified by picNumX, that field is + // marked as "unused for reference". + let reference_field_is_not_part_of_pic_x = match picture.other_field() { + None => true, + Some(other_field) => { + // Check that the fields do not reference one another. + !std::ptr::eq(other_field.as_ptr(), to_mark_as_long_ptr) + && to_mark_as_long_other_field_ptr + .map(|p| !std::ptr::eq(p, &(*picture))) + .unwrap_or(true) + } + }; + + if reference_field_is_not_part_of_pic_x { + picture.set_reference(Reference::None, false); + break; + } + } + } + + let is_frame = matches!(pic.field, Field::Frame); + let to_mark_as_long = &self.entries[to_mark_as_long_pos].pic; + to_mark_as_long + .borrow_mut() + .set_reference(Reference::LongTerm, is_frame); + to_mark_as_long.borrow_mut().long_term_frame_idx = long_term_frame_idx; + + if let Some(other_field) = to_mark_as_long.borrow().other_field() { + let mut other_field = other_field.borrow_mut(); + if matches!(other_field.reference(), Reference::LongTerm) { + other_field.long_term_frame_idx = long_term_frame_idx; + + log::debug!( + "Assigned long_term_frame_idx {} to other_field {:#?}", + long_term_frame_idx, + &other_field + ); + } + } + + Ok(()) + } + + /// Returns the new `max_long_term_frame_idx`. + pub fn mmco_op_4(&mut self, marking: &RefPicMarkingInner) -> MaxLongTermFrameIdx { + log::debug!( + "MMCO op 4, max_long_term_frame_idx: {:?}", + marking.max_long_term_frame_idx + ); + + log::trace!("Dpb state before MMCO=4: {:#?}", self); + + for mut dpb_pic in self + .pictures_mut() + .filter(|pic| matches!(pic.reference(), Reference::LongTerm)) + .filter(|pic| marking.max_long_term_frame_idx < pic.long_term_frame_idx) + { + dpb_pic.set_reference(Reference::None, false); + } + + marking.max_long_term_frame_idx + } + + /// Returns the new `max_long_term_frame_idx`. + pub fn mmco_op_5(&mut self, pic: &mut PictureData) -> MaxLongTermFrameIdx { + log::debug!("MMCO op 5, marking all pictures in the DPB as unused for reference"); + log::trace!("Dpb state before MMCO=5: {:#?}", self); + + self.mark_all_as_unused_for_ref(); + + pic.has_mmco_5 = true; + + // A picture including a memory_management_control_operation equal to 5 + // shall have frame_num constraints as described above and, after the + // decoding of the current picture and the processing of the memory + // management control operations, the picture shall be inferred to have + // had frame_num equal to 0 for all subsequent use in the decoding + // process, except as specified in clause 7.4.1.2.4. + pic.frame_num = 0; + + // When the current picture includes a + // memory_management_control_operation equal to 5, after the decoding of + // the current picture, tempPicOrderCnt is set equal to PicOrderCnt( + // CurrPic ), TopFieldOrderCnt of the current picture (if any) is set + // equal to TopFieldOrderCnt − tempPicOrderCnt, and BottomFieldOrderCnt + // of the current picture (if any) is set equal to BottomFieldOrderCnt − + // tempPicOrderCnt + match pic.field { + Field::Top => { + pic.top_field_order_cnt = 0; + pic.pic_order_cnt = 0; + } + Field::Bottom => { + pic.bottom_field_order_cnt = 0; + pic.pic_order_cnt = 0; + } + Field::Frame => { + pic.top_field_order_cnt -= pic.pic_order_cnt; + pic.bottom_field_order_cnt -= pic.pic_order_cnt; + pic.pic_order_cnt = + std::cmp::min(pic.top_field_order_cnt, pic.bottom_field_order_cnt); + } + } + + MaxLongTermFrameIdx::NoLongTermFrameIndices + } + + pub fn mmco_op_6(&mut self, pic: &mut PictureData, marking: &RefPicMarkingInner) { + let long_term_frame_idx = marking.long_term_frame_idx; + + log::debug!("MMCO op 6, long_term_frame_idx: {}", long_term_frame_idx); + log::trace!("Dpb state before MMCO=6: {:#?}", self); + + for mut dpb_pic in self.pictures_mut() { + // When a variable LongTermFrameIdx equal to long_term_frame_idx is + // already assigned to a long-term reference frame or a long-term + // complementary reference field pair, that frame or complementary + // field pair and both of its fields are marked as "unused for + // reference". When LongTermFrameIdx is already assigned to a + // reference field, and that reference field is not part of a + // complementary field pair that includes the current picture, that + // field is marked as "unused for reference". + if matches!(dpb_pic.reference(), Reference::LongTerm) + && dpb_pic.long_term_frame_idx == long_term_frame_idx + { + let is_frame = matches!(dpb_pic.field, Field::Frame); + + let is_complementary_ref_field_pair = dpb_pic + .other_field() + .map(|f| { + let pic = f.borrow(); + matches!(pic.reference(), Reference::LongTerm) + && pic.long_term_frame_idx == long_term_frame_idx + }) + .unwrap_or(false); + + dpb_pic.set_reference(Reference::None, is_frame || is_complementary_ref_field_pair); + + break; + } + } + + let is_frame = matches!(pic.field, Field::Frame); + + let is_second_ref_field = match pic.field_rank() { + FieldRank::Second(first_field) + if *first_field.borrow().reference() == Reference::LongTerm => + { + first_field.borrow_mut().long_term_frame_idx = long_term_frame_idx; + true + } + _ => false, + }; + + pic.set_reference(Reference::LongTerm, is_frame || is_second_ref_field); + pic.long_term_frame_idx = long_term_frame_idx; + } + + #[cfg(debug_assertions)] + fn debug_ref_list_p(ref_pic_list: &[&DpbEntry], field_pic: bool) { + debug!( + "ref_list_p0: (ShortTerm|LongTerm, pic_num) {:?}", + ref_pic_list + .iter() + .map(|h| { + let p = h.pic.borrow(); + let reference = match p.reference() { + Reference::None => panic!("Not a reference."), + Reference::ShortTerm => "ShortTerm", + Reference::LongTerm => "LongTerm", + }; + + let field = if !p.is_second_field() { + "First field" + } else { + "Second field" + }; + + let field = format!("{}, {:?}", field, p.field); + + let inner = match (field_pic, p.reference()) { + (false, _) => ("pic_num", p.pic_num, field), + (true, Reference::ShortTerm) => ("frame_num_wrap", p.frame_num_wrap, field), + (true, Reference::LongTerm) => { + ("long_term_frame_idx", p.long_term_frame_idx as i32, field) + } + + _ => panic!("Not a reference."), + }; + (reference, inner) + }) + .collect::>() + ); + } + + #[cfg(debug_assertions)] + fn debug_ref_list_b(ref_pic_list: &[&DpbEntry], ref_pic_list_name: &str) { + debug!( + "{:?}: (ShortTerm|LongTerm, (POC|LongTermPicNum)) {:?}", + ref_pic_list_name, + ref_pic_list + .iter() + .map(|h| { + let p = h.pic.borrow(); + let reference = match p.reference() { + Reference::None => panic!("Not a reference."), + Reference::ShortTerm => "ShortTerm", + Reference::LongTerm => "LongTerm", + }; + + let field = if !p.is_second_field() { + "First field" + } else { + "Second field" + }; + + let field = format!("{}, {:?}", field, p.field); + + let inner = match p.reference() { + Reference::ShortTerm => ("POC", p.pic_order_cnt, field), + Reference::LongTerm => { + ("LongTermPicNum", p.long_term_pic_num as i32, field) + } + _ => panic!("Not a reference!"), + }; + (reference, inner) + }) + .collect::>() + ); + } + + fn sort_pic_num_descending(pics: &mut [&DpbEntry]) { + pics.sort_by_key(|h| std::cmp::Reverse(h.pic.borrow().pic_num)); + } + + fn sort_frame_num_wrap_descending(pics: &mut [&DpbEntry]) { + pics.sort_by_key(|h| std::cmp::Reverse(h.pic.borrow().frame_num_wrap)); + } + + fn sort_long_term_pic_num_ascending(pics: &mut [&DpbEntry]) { + pics.sort_by_key(|h| h.pic.borrow().long_term_pic_num); + } + + fn sort_long_term_frame_idx_ascending(pics: &mut [&DpbEntry]) { + pics.sort_by_key(|h| h.pic.borrow().long_term_frame_idx); + } + + fn sort_poc_descending(pics: &mut [&DpbEntry]) { + pics.sort_by_key(|h| std::cmp::Reverse(h.pic.borrow().pic_order_cnt)); + } + + fn sort_poc_ascending(pics: &mut [&DpbEntry]) { + pics.sort_by_key(|h| h.pic.borrow().pic_order_cnt); + } + + // When the reference picture list RefPicList1 has more than one entry + // and RefPicList1 is identical to the reference picture list + // RefPicList0, the first two entries RefPicList1[0] and RefPicList1[1] + // are switched. + fn swap_b1_if_needed(b0: &DpbPicRefList, b1: &mut DpbPicRefList) { + if b1.len() > 1 && b0.len() == b1.len() { + let mut equals = true; + for (x1, x2) in b0.iter().zip(b1.iter()) { + if !Rc::ptr_eq(&x1.pic, &x2.pic) { + equals = false; + break; + } + } + + if equals { + b1.swap(0, 1); + } + } + } + + /// Copies from refFrameList(XShort|Long)Term into RefPicListX as per 8.2.4.2.5. Used when + /// building the reference list for fields in interlaced decoding. + fn init_ref_field_pic_list<'a>( + mut field: Field, + reference_type: Reference, + ref_frame_list: &mut DpbPicRefList<'a, T>, + ref_pic_list: &mut DpbPicRefList<'a, T>, + ) { + // When one field of a reference frame was not decoded or is not marked as "used for + // (short|long)-term reference", the missing field is ignored and instead the next + // available stored reference field of the chosen parity from the ordered list of frames + // refFrameListX(Short|Long)Term is inserted into RefPicListX. + ref_frame_list.retain(|h| { + let p = h.pic.borrow(); + let skip = p.nonexisting || *p.reference() != reference_type; + !skip + }); + + while let Some(position) = ref_frame_list.iter().position(|h| { + let p = h.pic.borrow(); + let found = p.field == field; + + if found { + field = field.opposite(); + } + + found + }) { + let pic = ref_frame_list.remove(position); + ref_pic_list.push(pic); + } + + ref_pic_list.append(ref_frame_list); + } + + /// 8.2.4.2.1 Initialization process for the reference picture list for P + /// and SP slices in frames + fn build_ref_pic_list_p(&self) -> DpbPicRefList { + let mut ref_pic_list_p0: Vec<_> = self + .short_term_refs_iter() + .filter(|h| !h.pic.borrow().is_second_field()) + .collect(); + + Self::sort_pic_num_descending(&mut ref_pic_list_p0); + + let num_short_term_refs = ref_pic_list_p0.len(); + + ref_pic_list_p0.extend( + self.long_term_refs_iter() + .filter(|h| !h.pic.borrow().is_second_field()), + ); + Self::sort_long_term_pic_num_ascending(&mut ref_pic_list_p0[num_short_term_refs..]); + + #[cfg(debug_assertions)] + Self::debug_ref_list_p(&ref_pic_list_p0, false); + + ref_pic_list_p0 + } + + /// 8.2.4.2.2 Initialization process for the reference picture list for P + /// and SP slices in fields + fn build_ref_field_pic_list_p(&self, cur_pic: &PictureData) -> DpbPicRefList { + let mut ref_pic_list_p0 = vec![]; + + let mut ref_frame_list_0_short_term: Vec<_> = self.short_term_refs_iter().collect(); + Self::sort_frame_num_wrap_descending(&mut ref_frame_list_0_short_term); + + let mut ref_frame_list_long_term: Vec<_> = self.long_term_refs_iter().collect(); + Self::sort_long_term_pic_num_ascending(&mut ref_frame_list_long_term); + + // 8.2.4.2.5 + Self::init_ref_field_pic_list( + cur_pic.field, + Reference::ShortTerm, + &mut ref_frame_list_0_short_term, + &mut ref_pic_list_p0, + ); + Self::init_ref_field_pic_list( + cur_pic.field, + Reference::LongTerm, + &mut ref_frame_list_long_term, + &mut ref_pic_list_p0, + ); + + #[cfg(debug_assertions)] + Self::debug_ref_list_p(&ref_pic_list_p0, true); + + ref_pic_list_p0 + } + + // 8.2.4.2.3 Initialization process for reference picture lists for B slices + // in frames + fn build_ref_pic_list_b(&self, cur_pic: &PictureData) -> (DpbPicRefList, DpbPicRefList) { + let mut short_term_refs: Vec<_> = self + .short_term_refs_iter() + .filter(|h| !h.pic.borrow().is_second_field()) + .collect(); + + // When pic_order_cnt_type is equal to 0, reference pictures that are + // marked as "non-existing" as specified in clause 8.2.5.2 are not + // included in either RefPicList0 or RefPicList1. + if cur_pic.pic_order_cnt_type == 0 { + short_term_refs.retain(|h| !h.pic.borrow().nonexisting); + } + + let mut ref_pic_list_b0 = vec![]; + let mut ref_pic_list_b1 = vec![]; + let mut remaining = vec![]; + // b0 contains three inner lists of pictures, i.e. [[0] [1] [2]] + // [0]: short term pictures with POC < current, sorted by descending POC. + // [1]: short term pictures with POC > current, sorted by ascending POC. + // [2]: long term pictures sorted by ascending long_term_pic_num + for &handle in &short_term_refs { + let pic = handle.pic.borrow(); + + if pic.pic_order_cnt < cur_pic.pic_order_cnt { + ref_pic_list_b0.push(handle); + } else { + remaining.push(handle); + } + } + + Self::sort_poc_descending(&mut ref_pic_list_b0); + Self::sort_poc_ascending(&mut remaining); + ref_pic_list_b0.append(&mut remaining); + + let mut long_term_refs: Vec<_> = self + .long_term_refs_iter() + .filter(|h| !h.pic.borrow().nonexisting) + .filter(|h| !h.pic.borrow().is_second_field()) + .collect(); + Self::sort_long_term_pic_num_ascending(&mut long_term_refs); + + ref_pic_list_b0.extend(long_term_refs.clone()); + + // b1 contains three inner lists of pictures, i.e. [[0] [1] [2]] + // [0]: short term pictures with POC > current, sorted by ascending POC. + // [1]: short term pictures with POC < current, sorted by descending POC. + // [2]: long term pictures sorted by ascending long_term_pic_num + for &handle in &short_term_refs { + let pic = handle.pic.borrow(); + + if pic.pic_order_cnt > cur_pic.pic_order_cnt { + ref_pic_list_b1.push(handle); + } else { + remaining.push(handle); + } + } + + Self::sort_poc_ascending(&mut ref_pic_list_b1); + Self::sort_poc_descending(&mut remaining); + + ref_pic_list_b1.extend(remaining); + ref_pic_list_b1.extend(long_term_refs); + + // When the reference picture list RefPicList1 has more than one entry + // and RefPicList1 is identical to the reference picture list + // RefPicList0, the first two entries RefPicList1[0] and RefPicList1[1] + // are switched. + Self::swap_b1_if_needed(&ref_pic_list_b0, &mut ref_pic_list_b1); + + #[cfg(debug_assertions)] + Self::debug_ref_list_b(&ref_pic_list_b0, "ref_pic_list_b0"); + #[cfg(debug_assertions)] + Self::debug_ref_list_b(&ref_pic_list_b1, "ref_pic_list_b1"); + + (ref_pic_list_b0, ref_pic_list_b1) + } + + /// 8.2.4.2.4 Initialization process for reference picture lists for B + /// slices in fields + fn build_ref_field_pic_list_b( + &self, + cur_pic: &PictureData, + ) -> (DpbPicRefList, DpbPicRefList) { + let mut ref_pic_list_b0 = vec![]; + let mut ref_pic_list_b1 = vec![]; + let mut ref_frame_list_0_short_term = vec![]; + let mut ref_frame_list_1_short_term = vec![]; + + let mut remaining = vec![]; + + let mut short_term_refs: Vec<_> = self.short_term_refs_iter().collect(); + + // When pic_order_cnt_type is equal to 0, reference pictures that are + // marked as "non-existing" as specified in clause 8.2.5.2 are not + // included in either RefPicList0 or RefPicList1. + if cur_pic.pic_order_cnt_type == 0 { + short_term_refs.retain(|h| !h.pic.borrow().nonexisting); + } + + // refFrameList0ShortTerm is comprised of two inner lists, [[0] [1]] + // [0]: short term pictures with POC <= current, sorted by descending POC + // [1]: short term pictures with POC > current, sorted by ascending POC + // NOTE 3 – When the current field follows in decoding order a coded + // field fldPrev with which together it forms a complementary reference + // field pair, fldPrev is included into the list refFrameList0ShortTerm + // using PicOrderCnt( fldPrev ) and the ordering method described in the + // previous sentence is applied. + for &handle in &short_term_refs { + let pic = handle.pic.borrow(); + + if pic.pic_order_cnt <= cur_pic.pic_order_cnt { + ref_frame_list_0_short_term.push(handle); + } else { + remaining.push(handle); + } + } + + Self::sort_poc_descending(&mut ref_frame_list_0_short_term); + Self::sort_poc_ascending(&mut remaining); + ref_frame_list_0_short_term.append(&mut remaining); + + // refFrameList1ShortTerm is comprised of two inner lists, [[0] [1]] + // [0]: short term pictures with POC > current, sorted by ascending POC + // [1]: short term pictures with POC <= current, sorted by descending POC + // NOTE 4 – When the current field follows in decoding order a coded + // field fldPrev with which together it forms a complementary reference + // field pair, fldPrev is included into the list refFrameList1ShortTerm + // using PicOrderCnt( fldPrev ) and the ordering method described in the + // previous sentence is applied. + + for &handle in &short_term_refs { + let pic = handle.pic.borrow(); + + if pic.pic_order_cnt > cur_pic.pic_order_cnt { + ref_frame_list_1_short_term.push(handle); + } else { + remaining.push(handle); + } + } + + Self::sort_poc_ascending(&mut ref_frame_list_1_short_term); + Self::sort_poc_descending(&mut remaining); + ref_frame_list_1_short_term.append(&mut remaining); + + // refFrameListLongTerm: long term pictures sorted by ascending + // LongTermFrameIdx. + // NOTE 5 – When the current picture is the second field of a + // complementary field pair and the first field of the complementary + // field pair is marked as "used for long-term reference", the first + // field is included into the list refFrameListLongTerm. A reference + // entry in which only one field is marked as "used for long-term + // reference" is included into the list refFrameListLongTerm + let mut ref_frame_list_long_term: Vec<_> = self + .long_term_refs_iter() + .filter(|h| !h.pic.borrow().nonexisting) + .collect(); + + Self::sort_long_term_frame_idx_ascending(&mut ref_frame_list_long_term); + + #[cfg(debug_assertions)] + Self::debug_ref_list_b(&ref_frame_list_0_short_term, "ref_frame_list_0_short_term"); + #[cfg(debug_assertions)] + Self::debug_ref_list_b(&ref_frame_list_1_short_term, "ref_frame_list_1_short_term"); + #[cfg(debug_assertions)] + Self::debug_ref_list_b(&ref_frame_list_long_term, "ref_frame_list_long_term"); + + // 8.2.4.2.5 + let field = cur_pic.field; + Self::init_ref_field_pic_list( + field, + Reference::ShortTerm, + &mut ref_frame_list_0_short_term, + &mut ref_pic_list_b0, + ); + Self::init_ref_field_pic_list( + field, + Reference::LongTerm, + &mut ref_frame_list_long_term, + &mut ref_pic_list_b0, + ); + + Self::init_ref_field_pic_list( + field, + Reference::ShortTerm, + &mut ref_frame_list_1_short_term, + &mut ref_pic_list_b1, + ); + Self::init_ref_field_pic_list( + field, + Reference::LongTerm, + &mut ref_frame_list_long_term, + &mut ref_pic_list_b1, + ); + + // When the reference picture list RefPicList1 has more than one entry + // and RefPicList1 is identical to the reference picture list + // RefPicList0, the first two entries RefPicList1[0] and RefPicList1[1] + // are switched. + Self::swap_b1_if_needed(&ref_pic_list_b0, &mut ref_pic_list_b1); + + #[cfg(debug_assertions)] + Self::debug_ref_list_b(&ref_pic_list_b0, "ref_pic_list_b0"); + #[cfg(debug_assertions)] + Self::debug_ref_list_b(&ref_pic_list_b1, "ref_pic_list_b1"); + + (ref_pic_list_b0, ref_pic_list_b1) + } + + /// Returns the lists of reference pictures for `pic`. + pub fn build_ref_pic_lists(&self, pic: &PictureData) -> ReferencePicLists { + let num_refs = self + .pictures() + .filter(|p| p.is_ref() && !p.nonexisting) + .count(); + + // 8.2.4.2.1 ~ 8.2.4.2.4: When this process is invoked, there shall be + // at least one reference frame or complementary reference field pair + // that is currently marked as "used for reference" (i.e., as "used for + // short-term reference" or "used for long-term reference") and is not + // marked as "non-existing". + if num_refs == 0 { + return Default::default(); + } + + let (ref_pic_list_p0, (ref_pic_list_b0, ref_pic_list_b1)) = + if matches!(pic.field, Field::Frame) { + (self.build_ref_pic_list_p(), self.build_ref_pic_list_b(pic)) + } else { + ( + self.build_ref_field_pic_list_p(pic), + self.build_ref_field_pic_list_b(pic), + ) + }; + + // punktfunk deviation (PROVENANCE.md #5): upstream computed these indices with an + // unsafe `offset_from` against the entries base pointer. Same pointer-identity + // mapping, expressed safely — the DPB holds at most 16 entries, so the linear + // `position` is noise, and the crate stays `#![forbid(unsafe_code)]`. + let refs_to_index = |refs: Vec<&DpbEntry>| { + refs.into_iter() + .map(|r| { + self.entries + .iter() + .position(|e| std::ptr::eq(e, r)) + .expect("every reference list entry comes from this DPB") + }) + .collect() + }; + + ReferencePicLists { + ref_pic_list_p0: refs_to_index(ref_pic_list_p0), + ref_pic_list_b0: refs_to_index(ref_pic_list_b0), + ref_pic_list_b1: refs_to_index(ref_pic_list_b1), + } + } +} + +impl Default for Dpb { + fn default() -> Self { + // See https://github.com/rust-lang/rust/issues/26925 on why this can't + // be derived. + Self { + entries: Default::default(), + max_num_pics: Default::default(), + max_num_reorder_frames: Default::default(), + interlaced: Default::default(), + } + } +} + +impl std::fmt::Debug for Dpb { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let pics = self + .entries + .iter() + .map(|h| &h.pic) + .enumerate() + .collect::>(); + f.debug_struct("Dpb") + .field("pictures", &pics) + .field("max_num_pics", &self.max_num_pics) + .field("interlaced", &self.interlaced) + .finish() + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/nalu.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/nalu.rs new file mode 100644 index 00000000..364c47ad --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/nalu.rs @@ -0,0 +1,124 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::borrow::Cow; +use std::fmt::Debug; +use std::io::Cursor; +use std::io::Seek; +use std::io::SeekFrom; + +#[allow(clippy::len_without_is_empty)] +pub trait Header: Sized { + /// Parse the NALU header, returning it. + fn parse>(cursor: &mut Cursor) -> Result; + /// Whether this header type indicates EOS. + fn is_end(&self) -> bool; + /// The length of the header. + fn len(&self) -> usize; +} + +#[derive(Debug)] +pub struct Nalu<'a, U> { + pub header: U, + /// The mapping that backs this NALU. Possibly shared with the other NALUs + /// in the Access Unit. + pub data: Cow<'a, [u8]>, + + pub size: usize, + pub offset: usize, +} + +impl<'a, U> Nalu<'a, U> +where + U: Debug + Header, +{ + /// Find the next Annex B encoded NAL unit. + pub fn next(cursor: &mut Cursor<&'a [u8]>) -> Result, String> { + let bitstream = cursor.clone().into_inner(); + let pos = usize::try_from(cursor.position()).map_err(|err| err.to_string())?; + + // Find the start code for this NALU + let current_nalu_offset = match Nalu::<'a, U>::find_start_code(cursor, pos) { + Some(offset) => offset, + None => return Err("No NAL found".into()), + }; + + let mut start_code_offset = pos + current_nalu_offset; + + // If the preceding byte is 00, then we actually have a four byte SC, + // i.e. 00 00 00 01 Where the first 00 is the "zero_byte()" + if start_code_offset > 0 && cursor.get_ref()[start_code_offset - 1] == 00 { + start_code_offset -= 1; + } + + // The NALU offset is its offset + 3 bytes to skip the start code. + let nalu_offset = pos + current_nalu_offset + 3; + + // Set the bitstream position to the start of the current NALU + cursor.set_position(u64::try_from(nalu_offset).map_err(|err| err.to_string())?); + + let hdr = U::parse(cursor)?; + + // Find the start of the subsequent NALU. + let mut next_nalu_offset = match Nalu::<'a, U>::find_start_code(cursor, nalu_offset) { + Some(offset) => offset, + None => { + let cur_pos = cursor.position(); + let end_pos = cursor + .seek(SeekFrom::End(0)) + .map_err(|err| err.to_string())?; + let _ = cursor + .seek(SeekFrom::Start(cur_pos)) + .map_err(|err| err.to_string())?; + (end_pos - cur_pos) as usize + } // Whatever data is left must be part of the current NALU + }; + + while next_nalu_offset > 0 && cursor.get_ref()[nalu_offset + next_nalu_offset - 1] == 00 { + // Discard trailing_zero_8bits + next_nalu_offset -= 1; + } + + let nal_size = if hdr.is_end() { + // the NALU is comprised of only the header + hdr.len() + } else { + next_nalu_offset + }; + + Ok(Nalu { + header: hdr, + data: Cow::from(&bitstream[start_code_offset..nalu_offset + nal_size]), + size: nal_size, + offset: nalu_offset - start_code_offset, + }) + } +} + +impl<'a, U> Nalu<'a, U> +where + U: Debug, +{ + fn find_start_code(data: &mut Cursor<&'a [u8]>, offset: usize) -> Option { + // discard all zeroes until the start code pattern is found + data.get_ref()[offset..] + .windows(3) + .position(|window| window == [0x00, 0x00, 0x01]) + } + + pub fn into_owned(self) -> Nalu<'static, U> { + Nalu { + header: self.header, + size: self.size, + offset: self.offset, + data: Cow::Owned(self.data.into_owned()), + } + } +} + +impl<'a, U> AsRef<[u8]> for Nalu<'a, U> { + fn as_ref(&self) -> &[u8] { + &self.data[self.offset..self.offset + self.size] + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/nalu_writer.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/nalu_writer.rs new file mode 100644 index 00000000..4a6d4f86 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/nalu_writer.rs @@ -0,0 +1,311 @@ +// Copyright 2024 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +use std::fmt; +use std::io::Write; + +use crate::bitstream_utils::BitWriter; +use crate::bitstream_utils::BitWriterError; + +/// Internal wrapper over [`std::io::Write`] for possible emulation prevention +struct EmulationPrevention { + out: W, + prev_bytes: [Option; 2], + + /// Emulation prevention enabled. + ep_enabled: bool, +} + +impl EmulationPrevention { + fn new(writer: W, ep_enabled: bool) -> Self { + Self { + out: writer, + prev_bytes: [None; 2], + ep_enabled, + } + } + + fn write_byte(&mut self, curr_byte: u8) -> std::io::Result<()> { + if self.prev_bytes[1] == Some(0x00) && self.prev_bytes[0] == Some(0x00) && curr_byte <= 0x03 + { + self.out.write_all(&[0x00, 0x00, 0x03, curr_byte])?; + self.prev_bytes = [None; 2]; + } else { + if let Some(byte) = self.prev_bytes[1] { + self.out.write_all(&[byte])?; + } + + self.prev_bytes[1] = self.prev_bytes[0]; + self.prev_bytes[0] = Some(curr_byte); + } + + Ok(()) + } + + /// Writes a H.264 NALU header. + fn write_header(&mut self, idc: u8, type_: u8) -> NaluWriterResult<()> { + self.out.write_all(&[ + 0x00, + 0x00, + 0x00, + 0x01, + (idc & 0b11) << 5 | (type_ & 0b11111), + ])?; + + Ok(()) + } + + fn has_data_pending(&self) -> bool { + self.prev_bytes[0].is_some() || self.prev_bytes[1].is_some() + } +} + +impl Write for EmulationPrevention { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + if !self.ep_enabled { + self.out.write_all(buf)?; + return Ok(buf.len()); + } + + for byte in buf { + self.write_byte(*byte)?; + } + + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + if let Some(byte) = self.prev_bytes[1].take() { + self.out.write_all(&[byte])?; + } + + if let Some(byte) = self.prev_bytes[0].take() { + self.out.write_all(&[byte])?; + } + + self.out.flush() + } +} + +impl Drop for EmulationPrevention { + fn drop(&mut self) { + if let Err(e) = self.flush() { + log::error!("Unable to flush pending bytes {e:?}"); + } + } +} + +#[derive(Debug)] +pub enum NaluWriterError { + Overflow, + Io(std::io::Error), + BitWriterError(BitWriterError), +} + +impl fmt::Display for NaluWriterError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + NaluWriterError::Overflow => write!(f, "value increment caused value overflow"), + NaluWriterError::Io(x) => write!(f, "{}", x.to_string()), + NaluWriterError::BitWriterError(x) => write!(f, "{}", x.to_string()), + } + } +} + +impl From for NaluWriterError { + fn from(err: std::io::Error) -> Self { + NaluWriterError::Io(err) + } +} + +impl From for NaluWriterError { + fn from(err: BitWriterError) -> Self { + NaluWriterError::BitWriterError(err) + } +} + +pub type NaluWriterResult = std::result::Result; + +/// A writer for H.264 bitstream. It is capable of outputing bitstream with +/// emulation-prevention. +pub struct NaluWriter(BitWriter>); + +impl NaluWriter { + pub fn new(writer: W, ep_enabled: bool) -> Self { + Self(BitWriter::new(EmulationPrevention::new(writer, ep_enabled))) + } + + /// Writes fixed bit size integer (up to 32 bit) output with emulation + /// prevention if enabled. Corresponds to `f(n)` in H.264 spec. + pub fn write_f>(&mut self, bits: usize, value: T) -> NaluWriterResult { + self.0 + .write_f(bits, value) + .map_err(NaluWriterError::BitWriterError) + } + + /// An alias to [`Self::write_f`] Corresponds to `n(n)` in H.264 spec. + pub fn write_u>(&mut self, bits: usize, value: T) -> NaluWriterResult { + self.write_f(bits, value) + } + + /// Writes a number in exponential golumb format. + pub fn write_exp_golumb(&mut self, value: u32) -> NaluWriterResult<()> { + let value = value.checked_add(1).ok_or(NaluWriterError::Overflow)?; + let bits = 32 - value.leading_zeros() as usize; + let zeros = bits - 1; + + self.write_f(zeros, 0u32)?; + self.write_f(bits, value)?; + + Ok(()) + } + + /// Writes a unsigned integer in exponential golumb format. + /// Coresponds to `ue(v)` in H.264 spec. + pub fn write_ue>(&mut self, value: T) -> NaluWriterResult<()> { + let value = value.into(); + + self.write_exp_golumb(value) + } + + /// Writes a signed integer in exponential golumb format. + /// Coresponds to `se(v)` in H.264 spec. + pub fn write_se>(&mut self, value: T) -> NaluWriterResult<()> { + let value: i32 = value.into(); + let abs_value: u32 = value.unsigned_abs(); + + if value <= 0 { + self.write_ue(2 * abs_value) + } else { + self.write_ue(2 * abs_value - 1) + } + } + + /// Returns `true` if ['Self`] hold data that wasn't written to [`std::io::Write`] + pub fn has_data_pending(&self) -> bool { + self.0.has_data_pending() || self.0.inner().has_data_pending() + } + + /// Writes a H.264 NALU header. + pub fn write_header(&mut self, idc: u8, _type: u8) -> NaluWriterResult<()> { + self.0.flush()?; + self.0.inner_mut().write_header(idc, _type)?; + Ok(()) + } + + /// Returns `true` if next bits will be aligned to 8 + pub fn aligned(&self) -> bool { + !self.0.has_data_pending() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::bitstream_utils::BitReader; + + #[test] + fn simple_bits() { + let mut buf = Vec::::new(); + { + let mut writer = NaluWriter::new(&mut buf, false); + writer.write_f(1, true).unwrap(); + writer.write_f(1, false).unwrap(); + writer.write_f(1, false).unwrap(); + writer.write_f(1, false).unwrap(); + writer.write_f(1, true).unwrap(); + writer.write_f(1, true).unwrap(); + writer.write_f(1, true).unwrap(); + writer.write_f(1, true).unwrap(); + } + assert_eq!(buf, vec![0b10001111u8]); + } + + #[test] + fn simple_first_few_ue() { + fn single_ue(value: u32) -> Vec { + let mut buf = Vec::::new(); + { + let mut writer = NaluWriter::new(&mut buf, false); + writer.write_ue(value).unwrap(); + } + buf + } + + assert_eq!(single_ue(0), vec![0b10000000u8]); + assert_eq!(single_ue(1), vec![0b01000000u8]); + assert_eq!(single_ue(2), vec![0b01100000u8]); + assert_eq!(single_ue(3), vec![0b00100000u8]); + assert_eq!(single_ue(4), vec![0b00101000u8]); + assert_eq!(single_ue(5), vec![0b00110000u8]); + assert_eq!(single_ue(6), vec![0b00111000u8]); + assert_eq!(single_ue(7), vec![0b00010000u8]); + assert_eq!(single_ue(8), vec![0b00010010u8]); + assert_eq!(single_ue(9), vec![0b00010100u8]); + } + + #[test] + fn writer_reader() { + let mut buf = Vec::::new(); + { + let mut writer = NaluWriter::new(&mut buf, false); + writer.write_ue(10u32).unwrap(); + writer.write_se(-42).unwrap(); + writer.write_se(3).unwrap(); + writer.write_ue(5u32).unwrap(); + } + + let mut reader = BitReader::new(&buf, true); + + assert_eq!(reader.read_ue::().unwrap(), 10); + assert_eq!(reader.read_se::().unwrap(), -42); + assert_eq!(reader.read_se::().unwrap(), 3); + assert_eq!(reader.read_ue::().unwrap(), 5); + + let mut buf = Vec::::new(); + { + let mut writer = NaluWriter::new(&mut buf, false); + writer.write_se(30).unwrap(); + writer.write_ue(100u32).unwrap(); + writer.write_se(-402).unwrap(); + writer.write_ue(50u32).unwrap(); + } + + let mut reader = BitReader::new(&buf, true); + + assert_eq!(reader.read_se::().unwrap(), 30); + assert_eq!(reader.read_ue::().unwrap(), 100); + assert_eq!(reader.read_se::().unwrap(), -402); + assert_eq!(reader.read_ue::().unwrap(), 50); + } + + #[test] + fn writer_emulation_prevention() { + fn test(input: &[u8], bitstream: &[u8]) { + let mut buf = Vec::::new(); + { + let mut writer = NaluWriter::new(&mut buf, true); + for byte in input { + writer.write_f(8, *byte).unwrap(); + } + } + assert_eq!(buf, bitstream); + { + let mut reader = BitReader::new(&buf, true); + for byte in input { + assert_eq!(*byte, reader.read_bits::(8).unwrap()); + } + } + } + + test(&[0x00, 0x00, 0x00], &[0x00, 0x00, 0x03, 0x00]); + test(&[0x00, 0x00, 0x01], &[0x00, 0x00, 0x03, 0x01]); + test(&[0x00, 0x00, 0x02], &[0x00, 0x00, 0x03, 0x02]); + test(&[0x00, 0x00, 0x03], &[0x00, 0x00, 0x03, 0x03]); + + test(&[0x00, 0x00, 0x00, 0x00], &[0x00, 0x00, 0x03, 0x00, 0x00]); + test(&[0x00, 0x00, 0x00, 0x01], &[0x00, 0x00, 0x03, 0x00, 0x01]); + test(&[0x00, 0x00, 0x00, 0x02], &[0x00, 0x00, 0x03, 0x00, 0x02]); + test(&[0x00, 0x00, 0x00, 0x03], &[0x00, 0x00, 0x03, 0x00, 0x03]); + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/parser.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/parser.rs new file mode 100644 index 00000000..aff12430 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/parser.rs @@ -0,0 +1,3062 @@ +// Copyright 2022 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Can't reasonably expect client code to consume everything that has been parsed. +#![allow(dead_code)] + +use std::collections::BTreeMap; +use std::io::Cursor; +use std::io::Read; +use std::io::Seek; +use std::io::SeekFrom; +use std::rc::Rc; + +use crate::bitstream_utils::BitReader; +use crate::codec::h264::nalu; +use crate::codec::h264::nalu::Header; +use crate::codec::h264::picture::Field; + +pub type Nalu<'a> = nalu::Nalu<'a, NaluHeader>; + +pub(super) const DEFAULT_4X4_INTRA: [u8; 16] = [ + 6, 13, 13, 20, 20, 20, 28, 28, 28, 28, 32, 32, 32, 37, 37, 42, +]; + +pub(super) const DEFAULT_4X4_INTER: [u8; 16] = [ + 10, 14, 14, 20, 20, 20, 24, 24, 24, 24, 27, 27, 27, 30, 30, 34, +]; + +pub(super) const DEFAULT_8X8_INTRA: [u8; 64] = [ + 6, 10, 10, 13, 11, 13, 16, 16, 16, 16, 18, 18, 18, 18, 18, 23, 23, 23, 23, 23, 23, 25, 25, 25, + 25, 25, 25, 25, 27, 27, 27, 27, 27, 27, 27, 27, 29, 29, 29, 29, 29, 29, 29, 31, 31, 31, 31, 31, + 31, 33, 33, 33, 33, 33, 36, 36, 36, 36, 38, 38, 38, 40, 40, 42, +]; + +pub(super) const DEFAULT_8X8_INTER: [u8; 64] = [ + 9, 13, 13, 15, 13, 15, 17, 17, 17, 17, 19, 19, 19, 19, 19, 21, 21, 21, 21, 21, 21, 22, 22, 22, + 22, 22, 22, 22, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 27, 27, 27, 27, 27, + 27, 28, 28, 28, 28, 28, 30, 30, 30, 30, 32, 32, 32, 33, 33, 35, +]; + +const MAX_PPS_COUNT: u16 = 256; +const MAX_SPS_COUNT: u8 = 32; + +/// The maximum number of pictures in the DPB, as per A.3.1, clause h) +const DPB_MAX_SIZE: usize = 16; + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct Point { + pub x: T, + pub y: T, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct Rect { + pub min: Point, + pub max: Point, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum NaluType { + Unknown = 0, + Slice = 1, + SliceDpa = 2, + SliceDpb = 3, + SliceDpc = 4, + SliceIdr = 5, + Sei = 6, + Sps = 7, + Pps = 8, + AuDelimiter = 9, + SeqEnd = 10, + StreamEnd = 11, + FillerData = 12, + SpsExt = 13, + PrefixUnit = 14, + SubsetSps = 15, + DepthSps = 16, + SliceAux = 19, + SliceExt = 20, + SliceDepth = 21, +} + +impl TryFrom for NaluType { + type Error = String; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(NaluType::Unknown), + 1 => Ok(NaluType::Slice), + 2 => Ok(NaluType::SliceDpa), + 3 => Ok(NaluType::SliceDpb), + 4 => Ok(NaluType::SliceDpc), + 5 => Ok(NaluType::SliceIdr), + 6 => Ok(NaluType::Sei), + 7 => Ok(NaluType::Sps), + 8 => Ok(NaluType::Pps), + 9 => Ok(NaluType::AuDelimiter), + 10 => Ok(NaluType::SeqEnd), + 11 => Ok(NaluType::StreamEnd), + 12 => Ok(NaluType::FillerData), + 13 => Ok(NaluType::SpsExt), + 14 => Ok(NaluType::PrefixUnit), + 15 => Ok(NaluType::SubsetSps), + 16 => Ok(NaluType::DepthSps), + 19 => Ok(NaluType::SliceAux), + 20 => Ok(NaluType::SliceExt), + 21 => Ok(NaluType::SliceDepth), + _ => Err(format!("Invalid NaluType {}", value)), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RefPicListModification { + pub modification_of_pic_nums_idc: u8, + /* if modification_of_pic_nums_idc == 0 || 1 */ + pub abs_diff_pic_num_minus1: u32, + /* if modification_of_pic_nums_idc == 2 */ + pub long_term_pic_num: u32, + /* if modification_of_pic_nums_idc == 4 || 5 */ + pub abs_diff_view_idx_minus1: u32, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct PredWeightTable { + pub luma_log2_weight_denom: u8, + pub chroma_log2_weight_denom: u8, + + pub luma_weight_l0: [i16; 32], + pub luma_offset_l0: [i8; 32], + + /* if seq->ChromaArrayType != 0 */ + pub chroma_weight_l0: [[i16; 2]; 32], + pub chroma_offset_l0: [[i8; 2]; 32], + + /* if slice->slice_type % 5 == 1 */ + pub luma_weight_l1: [i16; 32], + pub luma_offset_l1: [i16; 32], + + /* and if seq->ChromaArrayType != 0 */ + pub chroma_weight_l1: [[i16; 2]; 32], + pub chroma_offset_l1: [[i8; 2]; 32], +} + +/// Representation of `MaxLongTermFrameIdx`. +/// +/// `MaxLongTermFrameIdx` is derived from `max_long_term_frame_idx_plus1`, an unsigned integer with +/// a special value indicating "no long-term frame indices". This type allows easy conversion +/// between the actual and "plus1" representation, while ensuring that the special value is always +/// handled by the code. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum MaxLongTermFrameIdx { + #[default] + NoLongTermFrameIndices, + Idx(u32), +} + +impl MaxLongTermFrameIdx { + /// Create a value from `max_long_term_frame_idx_plus1`. + pub fn from_value_plus1(max_long_term_frame_idx_plus1: u32) -> Self { + match max_long_term_frame_idx_plus1 { + 0 => Self::NoLongTermFrameIndices, + i @ 1.. => Self::Idx(i - 1), + } + } + + /// Convert this value to the representation used by `max_long_term_frame_idx_plus1`. + pub fn to_value_plus1(self) -> u32 { + match self { + Self::NoLongTermFrameIndices => 0, + Self::Idx(i) => i + 1, + } + } +} + +impl PartialEq for MaxLongTermFrameIdx { + fn eq(&self, other: &u32) -> bool { + match self { + MaxLongTermFrameIdx::NoLongTermFrameIndices => false, + MaxLongTermFrameIdx::Idx(idx) => idx.eq(other), + } + } +} + +impl PartialOrd for MaxLongTermFrameIdx { + fn partial_cmp(&self, other: &u32) -> Option { + match self { + MaxLongTermFrameIdx::NoLongTermFrameIndices => Some(std::cmp::Ordering::Less), + MaxLongTermFrameIdx::Idx(idx) => Some(idx.cmp(other)), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RefPicMarkingInner { + /// Specifies a control operation to be applied to affect the reference + /// picture marking. The `memory_management_control_operation` syntax element + /// is followed by data necessary for the operation specified by the value + /// of `memory_management_control_operation`. The values and control + /// operations associated with `memory_management_control_operation` are + /// specified in Table 7-9 + pub memory_management_control_operation: u8, + + /// Used (with memory_management_control_operation equal to 3 or 1) to + /// assign a long-term frame index to a short-term reference picture or to + /// mark a short-term reference picture as "unused for reference". + pub difference_of_pic_nums_minus1: u32, + + /// Used (with memory_management_control_operation equal to 2) to mark a + /// long-term reference picture as "unused for reference". + pub long_term_pic_num: u32, + + /// Used (with memory_management_control_operation equal to 3 or 6) to + /// assign a long-term frame index to a picture. + pub long_term_frame_idx: u32, + + /// Specifies the maximum value of long-term frame index allowed for + /// long-term reference pictures (until receipt of another value of + /// `max_long_term_frame_idx_plus1`). + pub max_long_term_frame_idx: MaxLongTermFrameIdx, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RefPicMarking { + /// Specifies how the previously-decoded pictures in the decoded picture + /// buffer are treated after decoding of an IDR picture. See Annex C. + pub no_output_of_prior_pics_flag: bool, + + /// If unset, specifies that the MaxLongTermFrameIdx variable is set equal + /// to "no long-term frame indices" and that the IDR picture is marked as + /// "used for short-term reference". If set, specifies that the + /// MaxLongTermFrameIdx variable is set equal to 0 and that the current IDR + /// picture is marked "used for long-term reference" and is assigned + /// LongTermFrameIdx equal to 0. + pub long_term_reference_flag: bool, + + /// Selects the reference picture marking mode of the currently decoded + /// picture as specified in Table 7-8. + pub adaptive_ref_pic_marking_mode_flag: bool, + + /// An Vec with additional data used in the marking process. + pub inner: Vec, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SliceHeader { + /// Specifies the address of the first macroblock in the slice. + pub first_mb_in_slice: u32, + + /// Specifies the coding type of the slice according to Table 7-6. + pub slice_type: SliceType, + + // Specifies the picture parameter set in use + pub pic_parameter_set_id: u8, + + /// Specifies the colour plane associated with the current slice RBSP when + /// `separate_colour_plane_flag` is set. + pub colour_plane_id: u8, + + /// Used as an identifier for pictures and shall be represented by + /// `log2_max_frame_num_minus4 + 4` bits in the bitstream. + pub frame_num: u16, + + /// If set, specifies that the slice is a slice of a coded field. If not + /// set, specifies that the slice is a slice of a coded frame. + pub field_pic_flag: bool, + + /// If set, specifies that the slice is part of a coded bottom field. If not + /// set, specifies that the picture is a coded top field. + pub bottom_field_flag: bool, + + /// Identifies an IDR picture. The values of `idr_pic_id` in all the slices + /// of an IDR picture shall remain unchanged. When two consecutive access + /// units in decoding order are both IDR access units, the value of + /// `idr_pic_id` in the slices of the first such IDR access unit shall + /// differ from the `idr_pic_id` in the second such IDR access unit + pub idr_pic_id: u16, + + /// Specifies the picture order count modulo `MaxPicOrderCntLsb` for the top + /// field of a coded frame or for a coded field. The length of the + /// `pic_order_cnt_lsb` syntax element is + /// `log2_max_pic_order_cnt_lsb_minus4` + 4 bits. + pub pic_order_cnt_lsb: u16, + + /// Specifies the picture order count difference between the bottom field + /// and the top field of a coded frame as follows + pub delta_pic_order_cnt_bottom: i32, + + /// The first entry specifies the picture order count difference from the + /// expected picture order count for the top field of a coded frame or for a + /// coded field as specified in clause 8.2.1 The second entry specifies the + /// picture order count difference from the expected picture order count for + /// the bottom field of a coded frame specified in clause 8.2.1. + pub delta_pic_order_cnt: [i32; 2], + + /// This value is required by V4L2 stateless decode params so it is calculated + /// by parser while processing slice header. + pub pic_order_cnt_bit_size: usize, + + /// Shall be equal to 0 for slices and slice data partitions belonging to + /// the primary coded picture. The value of `redundant_pic_cnt shall` be + /// greater than 0 for coded slices or coded slice data partitions of a + /// redundant coded picture + pub redundant_pic_cnt: u8, + + /// Specifies the method used in the decoding process to derive motion + /// vectors and reference indices for inter prediction > + pub direct_spatial_mv_pred_flag: bool, + + /// If set, specifies that the syntax element `num_ref_idx_l0_active_minus1` + /// is present for P, SP, and B slices and that the syntax element + /// `num_ref_idx_l1_active_minus1` is present for B slices. If not set, + /// specifies that the syntax elements `num_ref_idx_l0_active_minus1` and + /// `num_ref_idx_l1_active_minus1` are not present. + pub num_ref_idx_active_override_flag: bool, + + /// Specifies the maximum reference index for reference picture list 0 that + /// shall be used to decode the slice. + pub num_ref_idx_l0_active_minus1: u8, + + /// Specifies the maximum reference index for reference picture list 1 that + /// shall be used to decode the slice. + pub num_ref_idx_l1_active_minus1: u8, + + /// If set, specifies that the syntax element `modification_of_pic_nums_idc` + /// is present for specifying reference picture list 0. If not set, + /// specifies that this syntax element is not present. + pub ref_pic_list_modification_flag_l0: bool, + + /// Reference picture list 0 modification as parsed with the + /// `ref_pic_list_modification()` process. + pub ref_pic_list_modification_l0: Vec, + + /// If set, specifies that the syntax element `modification_of_pic_nums_idc` + /// is present for specifying reference picture list 1. If not set, + /// specifies that this syntax element is not present. + pub ref_pic_list_modification_flag_l1: bool, + + /// Reference picture list 1 modification as parsed with the + /// `ref_pic_list_modification()` process. + pub ref_pic_list_modification_l1: Vec, + + /// Prediction weight table as parsed using 7.3.3.2 + pub pred_weight_table: PredWeightTable, + + /// Decoded reference picture marking parsed using 7.3.3.3 + pub dec_ref_pic_marking: RefPicMarking, + + /// This value is required by V4L2 stateless decode params so it is calculated + /// by parser while processing slice header. + pub dec_ref_pic_marking_bit_size: usize, + + /// Specifies the index for determining the initialization table used in the + /// initialization process for context variables. + pub cabac_init_idc: u8, + + /// Specifies the initial value of QP Y to be used for all the macroblocks + /// in the slice until modified by the value of `mb_qp_delta` in the + /// macroblock layer. The initial QPY quantization parameter for the slice + /// is computed using 7-30. + pub slice_qp_delta: i8, + + /// Specifies the decoding process to be used to decode P macroblocks in an + /// SP slice. + pub sp_for_switch_flag: bool, + + /// Specifies the value of QSY for all the macroblocks in SP and SI slices. + /// The QSY quantization parameter for the slice is computed using 7-31. + pub slice_qs_delta: i8, + + /// Specifies whether the operation of the deblocking filter shall be + /// disabled across some block edges of the slice and specifies for which + /// edges the filtering is disabled. + pub disable_deblocking_filter_idc: u8, + + /// Specifies the offset used in accessing the α and tC0 deblocking filter + /// tables for filtering operations controlled by the macroblocks within the + /// slice. From this value, the offset that shall be applied when addressing + /// these tables shall be computed using 7-32. + pub slice_alpha_c0_offset_div2: i8, + + /// Specifies the offset used in accessing the β deblocking filter table for + /// filtering operations controlled by the macroblocks within the slice. + /// From this value, the offset that is applied when addressing the β table + /// of the deblocking filter shall be computed using 7-33. + pub slice_beta_offset_div2: i8, + + /// Same as `MaxPicNum` in the specification. + pub max_pic_num: u32, + + /// Size of the slice_header() in bits + pub header_bit_size: usize, + + /// Number of emulation prevention bytes (EPB) in this slice_header() + pub n_emulation_prevention_bytes: usize, +} + +impl SliceHeader { + /// Returns the field that is coded by this header. + pub fn field(&self) -> Field { + if self.field_pic_flag { + if self.bottom_field_flag { + Field::Bottom + } else { + Field::Top + } + } else { + Field::Frame + } + } +} + +pub struct SliceHeaderBuilder(SliceHeader); + +impl SliceHeaderBuilder { + pub fn new(pps: &Pps) -> Self { + SliceHeaderBuilder(SliceHeader { + pic_parameter_set_id: pps.pic_parameter_set_id, + ..Default::default() + }) + } + + pub fn slice_type(mut self, type_: SliceType) -> Self { + self.0.slice_type = type_; + self + } + + pub fn first_mb_in_slice(mut self, value: u32) -> Self { + self.0.first_mb_in_slice = value; + self + } + + pub fn pic_order_cnt_lsb(mut self, value: u16) -> Self { + self.0.pic_order_cnt_lsb = value; + self + } + + pub fn idr_pic_id(mut self, value: u16) -> Self { + self.0.idr_pic_id = value; + self + } + + pub fn num_ref_idx_active_override_flag(mut self, value: bool) -> Self { + self.0.num_ref_idx_active_override_flag = value; + self + } + + pub fn num_ref_idx_l0_active_minus1(mut self, value: u8) -> Self { + self = self.num_ref_idx_active_override_flag(true); + self.0.num_ref_idx_l0_active_minus1 = value; + self + } + + pub fn num_ref_idx_l0_active(self, value: u8) -> Self { + self.num_ref_idx_l0_active_minus1(value - 1) + } + + pub fn num_ref_idx_l1_active_minus1(mut self, value: u8) -> Self { + self = self.num_ref_idx_active_override_flag(true); + self.0.num_ref_idx_l1_active_minus1 = value; + self + } + + pub fn num_ref_idx_l1_active(self, value: u8) -> Self { + self.num_ref_idx_l1_active_minus1(value - 1) + } + + pub fn build(self) -> SliceHeader { + self.0 + } +} + +/// A H264 slice. An integer number of macroblocks or macroblock pairs ordered +/// consecutively in the raster scan within a particular slice group +pub struct Slice<'a> { + /// The slice header. + pub header: SliceHeader, + /// The NAL unit backing this slice. + pub nalu: Nalu<'a>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// See table 7-6 in the specification. +pub enum SliceType { + P = 0, + B = 1, + I = 2, + Sp = 3, + Si = 4, +} + +impl TryFrom for SliceType { + type Error = String; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(SliceType::P), + 1 => Ok(SliceType::B), + 2 => Ok(SliceType::I), + 3 => Ok(SliceType::Sp), + 4 => Ok(SliceType::Si), + _ => Err(format!("Invalid SliceType {}", value)), + } + } +} + +impl SliceType { + /// Whether this is a P slice. See table 7-6 in the specification. + pub fn is_p(&self) -> bool { + matches!(self, SliceType::P) + } + + /// Whether this is a B slice. See table 7-6 in the specification. + pub fn is_b(&self) -> bool { + matches!(self, SliceType::B) + } + + /// Whether this is an I slice. See table 7-6 in the specification. + pub fn is_i(&self) -> bool { + matches!(self, SliceType::I) + } + + /// Whether this is a SP slice. See table 7-6 in the specification. + pub fn is_sp(&self) -> bool { + matches!(self, SliceType::Sp) + } + + /// Whether this is a SI slice. See table 7-6 in the specification. + pub fn is_si(&self) -> bool { + matches!(self, SliceType::Si) + } +} + +impl Default for SliceType { + fn default() -> Self { + Self::P + } +} + +#[derive(Clone, Copy)] +#[repr(u8)] +pub enum Profile { + Baseline = 66, + Main = 77, + Extended = 88, + High = 100, + High10 = 110, + High422P = 122, +} + +impl TryFrom for Profile { + type Error = String; + + fn try_from(value: u8) -> Result { + match value { + 66 => Ok(Profile::Baseline), + 77 => Ok(Profile::Main), + 88 => Ok(Profile::Extended), + 100 => Ok(Profile::High), + 110 => Ok(Profile::High10), + 122 => Ok(Profile::High422P), + _ => Err(format!("Invalid Profile {}", value)), + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum Level { + #[default] + L1 = 10, + L1B = 9, + L1_1 = 11, + L1_2 = 12, + L1_3 = 13, + L2_0 = 20, + L2_1 = 21, + L2_2 = 22, + L3 = 30, + L3_1 = 31, + L3_2 = 32, + L4 = 40, + L4_1 = 41, + L4_2 = 42, + L5 = 50, + L5_1 = 51, + L5_2 = 52, + L6 = 60, + L6_1 = 61, + L6_2 = 62, +} + +impl TryFrom for Level { + type Error = String; + + fn try_from(value: u8) -> Result { + match value { + 10 => Ok(Level::L1), + 9 => Ok(Level::L1B), + 11 => Ok(Level::L1_1), + 12 => Ok(Level::L1_2), + 13 => Ok(Level::L1_3), + 20 => Ok(Level::L2_0), + 21 => Ok(Level::L2_1), + 22 => Ok(Level::L2_2), + 30 => Ok(Level::L3), + 31 => Ok(Level::L3_1), + 32 => Ok(Level::L3_2), + 40 => Ok(Level::L4), + 41 => Ok(Level::L4_1), + 42 => Ok(Level::L4_2), + 50 => Ok(Level::L5), + 51 => Ok(Level::L5_1), + 52 => Ok(Level::L5_2), + 60 => Ok(Level::L6), + 61 => Ok(Level::L6_1), + 62 => Ok(Level::L6_2), + _ => Err(format!("Invalid Level {}", value)), + } + } +} + +/// A H264 Sequence Parameter Set. A syntax structure containing syntax elements +/// that apply to zero or more entire coded video sequences as determined by the +/// content of a seq_parameter_set_id syntax element found in the picture +/// parameter set referred to by the pic_parameter_set_id syntax element found +/// in each slice header. +#[derive(Debug, PartialEq, Eq)] +pub struct Sps { + /// Identifies the sequence parameter set that is referred to by the picture + /// parameter set + pub seq_parameter_set_id: u8, + + /// Profile to which the coded video sequence conforms + pub profile_idc: u8, + + /// Retains the same meaning as in the specification. See 7.4.2.1.1 + pub constraint_set0_flag: bool, + /// Retains the same meaning as in the specification. See 7.4.2.1.1 + pub constraint_set1_flag: bool, + /// Retains the same meaning as in the specification. See 7.4.2.1.1 + pub constraint_set2_flag: bool, + /// Retains the same meaning as in the specification. See 7.4.2.1.1 + pub constraint_set3_flag: bool, + /// Retains the same meaning as in the specification. See 7.4.2.1.1 + pub constraint_set4_flag: bool, + /// Retains the same meaning as in the specification. See 7.4.2.1.1 + pub constraint_set5_flag: bool, + + /// Level to which the coded video sequence conforms + pub level_idc: Level, + + /// Specifies the chroma sampling relative to the luma sampling as specified + /// in clause 6.2. + pub chroma_format_idc: u8, + + /// Specifies whether the three colour components of the 4:4:4 chroma format + /// are coded separately. + pub separate_colour_plane_flag: bool, + + /// Specifies the bit depth of the samples of the luma array and the value + /// of the luma quantization parameter range offset QpBdOffsetY. See 7-3 and + /// 7-4. + pub bit_depth_luma_minus8: u8, + + /// Specifies the bit depth of the samples of the chroma arrays and the + /// value of the chroma quantization parameter range offset QpBdOffsetC. See + /// 7-5 and 7-6. + pub bit_depth_chroma_minus8: u8, + + /// qpprime_y_zero_transform_bypass_flag equal to 1 specifies that, when + /// QP′Y is equal to 0, a transform bypass operation for the transform + /// coefficient decoding process and picture construction process prior to + /// deblocking filter process as specified in clause 8.5 shall be applied. + /// qpprime_y_zero_transform_bypass_flag equal to 0 specifies that the + /// transform coefficient decoding process and picture construction process + /// prior to deblocking filter process shall not use the transform bypass + /// operation + /// QP′Y is defined in 7-38 as QP′Y = QPY + QpBdOffsetY + pub qpprime_y_zero_transform_bypass_flag: bool, + + /// Whether `seq_scaling_list_present_flag[i]` for i = 0..7 or i = 0..11 is + /// present or whether the sequence level scaling list shall be specified by + /// Flat_4x4_16 for i = 0..5 and flat_8x8_16 for i = 6..11 + pub seq_scaling_matrix_present_flag: bool, + + /// 4x4 Scaling list as read with 7.3.2.1.1.1 + pub scaling_lists_4x4: [[u8; 16]; 6], + /// 8x8 Scaling list as read with 7.3.2.1.1.1 + pub scaling_lists_8x8: [[u8; 64]; 6], + + /// Specifies the value of the variable MaxFrameNum that is used in + /// frame_num related derivations as follows: MaxFrameNum = 2 ^ + /// (log2_max_frame_num_minus4 + 4 ) + pub log2_max_frame_num_minus4: u8, + + /// Specifies the method to decode picture order count (as specified in + /// clause 8.2.1) + pub pic_order_cnt_type: u8, + + /// Specifies the value of the variable MaxPicOrderCntLsb that is used in + /// the decoding process for picture order count as specified in clause + /// 8.2.1 as follows: MaxPicOrderCntLsb = 2 ^ ( + /// log2_max_pic_order_cnt_lsb_minus4 + 4 ). + pub log2_max_pic_order_cnt_lsb_minus4: u8, + + /// If true, specifies that `delta_pic_order_cnt[0]` and + /// `delta_pic_order_cnt[1]` are not present in the slice headers of the + /// sequence and shall be inferred to be equal to 0. + /// If false, specifies that `delta_pic_order_cnt[0]` is present in the + /// slice headers of the sequence and `delta_pic_order_cnt[1]` may be + /// present in the slice headers of the sequence. + pub delta_pic_order_always_zero_flag: bool, + + /// Used to calculate the picture order count of a non-reference picture as + /// specified in clause 8.2.1. + pub offset_for_non_ref_pic: i32, + + /// Used to calculate the picture order count of a bottom field as specified + /// in clause 8.2.1. + pub offset_for_top_to_bottom_field: i32, + + /// Used in the decoding process for picture order count as specified in + /// clause 8.2.1 + pub num_ref_frames_in_pic_order_cnt_cycle: u8, + + /// An element of a list of num_ref_frames_in_pic_order_cnt_cycle values + /// used in the decoding process for picture order count as specified in + /// clause 8.2. + pub offset_for_ref_frame: [i32; 255], + + /// Specifies the maximum number of short-term and long-term reference + /// frames, complementary reference field pairs, and non-paired reference + /// fields that may be used by the decoding process for inter prediction of + /// any picture in the coded video sequence. Also + /// determines the size of the sliding window operation as specified in + /// clause 8.2.5.3. + pub max_num_ref_frames: u8, + + /// Specifies the allowed values of frame_num as specified in clause 7.4.3 + /// and the decoding process in case of an inferred gap between values of + /// frame_num as specified in clause 8.2.5.2 + pub gaps_in_frame_num_value_allowed_flag: bool, + + /// Plus 1 specifies the width of each decoded picture in units of + /// macroblocks. + pub pic_width_in_mbs_minus1: u16, + /// Plus 1 specifies the height in slice group map units of a decoded frame + /// or field. + pub pic_height_in_map_units_minus1: u16, + + /// If true, specifies that every coded picture of the coded video sequence + /// is a coded frame containing only frame macroblocks, else specifies that + /// coded pictures of the coded video sequence may either be coded fields or + /// coded frames. + pub frame_mbs_only_flag: bool, + + /// If true, specifies the possible use of switching between frame and field + /// macroblocks within frames, else, specifies no switching between frame + /// and field macroblocks within a picture. + pub mb_adaptive_frame_field_flag: bool, + + /// Specifies the method used in the derivation process for luma motion + /// vectors for B_Skip, B_Direct_16x16 and B_Direct_8x8 as specified in + /// clause 8.4.1.2. + pub direct_8x8_inference_flag: bool, + + /// If true, specifies that the frame cropping offset parameters follow next + /// in the sequence parameter, else specifies that the frame cropping offset + /// parameters are not present + pub frame_cropping_flag: bool, + + /// Specify the samples of the pictures in the coded video sequence that are + /// output from the decoding process, in terms of a rectangular region + /// specified in frame coordinates for output. + pub frame_crop_left_offset: u32, + /// Specify the samples of the pictures in the coded video sequence that are + /// output from the decoding process, in terms of a rectangular region + /// specified in frame coordinates for output. + pub frame_crop_right_offset: u32, + /// Specify the samples of the pictures in the coded video sequence that are + /// output from the decoding process, in terms of a rectangular region + /// specified in frame coordinates for output. + pub frame_crop_top_offset: u32, + /// Specify the samples of the pictures in the coded video sequence that are + /// output from the decoding process, in terms of a rectangular region + /// specified in frame coordinates for output. + pub frame_crop_bottom_offset: u32, + + // Calculated + /// Same as ExpectedDeltaPerPicOrderCntCycle, see 7-12 in the specification. + pub expected_delta_per_pic_order_cnt_cycle: i32, + + pub vui_parameters_present_flag: bool, + pub vui_parameters: VuiParams, +} + +impl Sps { + /// Returns the coded width of the stream. + /// + /// See 7-13 through 7-17 in the specification. + pub const fn width(&self) -> u32 { + (self.pic_width_in_mbs_minus1 as u32 + 1) * 16 + } + + /// Returns the coded height of the stream. + /// + /// See 7-13 through 7-17 in the specification. + pub const fn height(&self) -> u32 { + (self.pic_height_in_map_units_minus1 as u32 + 1) + * 16 + * (2 - self.frame_mbs_only_flag as u32) + } + + /// Returns `ChromaArrayType`, as computed in the specification. + pub const fn chroma_array_type(&self) -> u8 { + match self.separate_colour_plane_flag { + false => self.chroma_format_idc, + true => 0, + } + } + + /// Returns `SubWidthC` and `SubHeightC`. + /// + /// See table 6-1 in the specification. + fn sub_width_height_c(&self) -> (u32, u32) { + match (self.chroma_format_idc, self.separate_colour_plane_flag) { + (1, false) => (2, 2), + (2, false) => (2, 1), + (3, false) => (1, 1), + // undefined. + _ => (1, 1), + } + } + + /// Returns `CropUnitX` and `CropUnitY`. + /// + /// See 7-19 through 7-22 in the specification. + fn crop_unit_x_y(&self) -> (u32, u32) { + match self.chroma_array_type() { + 0 => (1, 2 - u32::from(self.frame_mbs_only_flag)), + _ => { + let (sub_width_c, sub_height_c) = self.sub_width_height_c(); + ( + sub_width_c, + sub_height_c * (2 - u32::from(self.frame_mbs_only_flag)), + ) + } + } + } + + /// Same as MaxFrameNum. See 7-10 in the specification. + pub fn max_frame_num(&self) -> u32 { + 1 << (self.log2_max_frame_num_minus4 + 4) + } + + pub fn visible_rectangle(&self) -> Rect { + if !self.frame_cropping_flag { + return Rect { + min: Point { x: 0, y: 0 }, + max: Point { + x: self.width(), + y: self.height(), + }, + }; + } + + let (crop_unit_x, crop_unit_y) = self.crop_unit_x_y(); + + let crop_left = crop_unit_x * self.frame_crop_left_offset; + let crop_right = crop_unit_x * self.frame_crop_right_offset; + let crop_top = crop_unit_y * self.frame_crop_top_offset; + let crop_bottom = crop_unit_y * self.frame_crop_bottom_offset; + + Rect { + min: Point { + x: crop_left, + y: crop_top, + }, + max: Point { + x: self.width() - crop_left - crop_right, + y: self.height() - crop_top - crop_bottom, + }, + } + } + + pub fn max_dpb_frames(&self) -> usize { + let profile = self.profile_idc; + let mut level = self.level_idc; + + // A.3.1 and A.3.2: Level 1b for Baseline, Constrained Baseline and Main + // profile if level_idc == 11 and constraint_set3_flag == 1 + if matches!(level, Level::L1_1) + && (profile == Profile::Baseline as u8 || profile == Profile::Main as u8) + && self.constraint_set3_flag + { + level = Level::L1B; + }; + + // Table A.1 + let max_dpb_mbs = match level { + Level::L1 => 396, + Level::L1B => 396, + Level::L1_1 => 900, + Level::L1_2 => 2376, + Level::L1_3 => 2376, + Level::L2_0 => 2376, + Level::L2_1 => 4752, + Level::L2_2 => 8100, + Level::L3 => 8100, + Level::L3_1 => 18000, + Level::L3_2 => 20480, + Level::L4 => 32768, + Level::L4_1 => 32768, + Level::L4_2 => 34816, + Level::L5 => 110400, + Level::L5_1 => 184320, + Level::L5_2 => 184320, + Level::L6 => 696320, + Level::L6_1 => 696320, + Level::L6_2 => 696320, + }; + + let width_mb = self.width() / 16; + let height_mb = self.height() / 16; + + let max_dpb_frames = + std::cmp::min(max_dpb_mbs / (width_mb * height_mb), DPB_MAX_SIZE as u32) as usize; + + let mut max_dpb_frames = std::cmp::max(max_dpb_frames, self.max_num_ref_frames as usize); + + if self.vui_parameters_present_flag && self.vui_parameters.bitstream_restriction_flag { + max_dpb_frames = std::cmp::max(1, self.vui_parameters.max_dec_frame_buffering as usize); + } + + max_dpb_frames + } + + pub fn max_num_order_frames(&self) -> u32 { + let vui = &self.vui_parameters; + let present = self.vui_parameters_present_flag && vui.bitstream_restriction_flag; + + if present { + vui.max_num_reorder_frames + } else { + let profile = self.profile_idc; + if (profile == 44 + || profile == 86 + || profile == 100 + || profile == 110 + || profile == 122 + || profile == 244) + && self.constraint_set3_flag + { + 0 + } else { + self.max_dpb_frames() as u32 + } + } + } +} + +// TODO: Replace with builder +impl Default for Sps { + fn default() -> Self { + Self { + scaling_lists_4x4: [[0; 16]; 6], + scaling_lists_8x8: [[0; 64]; 6], + offset_for_ref_frame: [0; 255], + seq_parameter_set_id: Default::default(), + profile_idc: Default::default(), + constraint_set0_flag: Default::default(), + constraint_set1_flag: Default::default(), + constraint_set2_flag: Default::default(), + constraint_set3_flag: Default::default(), + constraint_set4_flag: Default::default(), + constraint_set5_flag: Default::default(), + level_idc: Default::default(), + chroma_format_idc: Default::default(), + separate_colour_plane_flag: Default::default(), + bit_depth_luma_minus8: Default::default(), + bit_depth_chroma_minus8: Default::default(), + qpprime_y_zero_transform_bypass_flag: Default::default(), + seq_scaling_matrix_present_flag: Default::default(), + log2_max_frame_num_minus4: Default::default(), + pic_order_cnt_type: Default::default(), + log2_max_pic_order_cnt_lsb_minus4: Default::default(), + delta_pic_order_always_zero_flag: Default::default(), + offset_for_non_ref_pic: Default::default(), + offset_for_top_to_bottom_field: Default::default(), + num_ref_frames_in_pic_order_cnt_cycle: Default::default(), + max_num_ref_frames: Default::default(), + gaps_in_frame_num_value_allowed_flag: Default::default(), + pic_width_in_mbs_minus1: Default::default(), + pic_height_in_map_units_minus1: Default::default(), + frame_mbs_only_flag: Default::default(), + mb_adaptive_frame_field_flag: Default::default(), + direct_8x8_inference_flag: Default::default(), + frame_cropping_flag: Default::default(), + frame_crop_left_offset: Default::default(), + frame_crop_right_offset: Default::default(), + frame_crop_top_offset: Default::default(), + frame_crop_bottom_offset: Default::default(), + expected_delta_per_pic_order_cnt_cycle: Default::default(), + vui_parameters_present_flag: Default::default(), + vui_parameters: Default::default(), + } + } +} + +#[derive(Default)] +pub struct SpsBuilder(Sps); + +impl SpsBuilder { + pub fn new() -> Self { + Default::default() + } + + pub fn seq_parameter_set_id(mut self, value: u8) -> Self { + self.0.seq_parameter_set_id = value; + self + } + + pub fn profile_idc(mut self, value: Profile) -> Self { + self.0.profile_idc = value as u8; + self + } + + pub fn level_idc(mut self, value: Level) -> Self { + self.0.level_idc = value; + self + } + + pub fn frame_crop_offsets(mut self, top: u32, bottom: u32, left: u32, right: u32) -> Self { + self.0.frame_cropping_flag = true; + self.0.frame_crop_top_offset = top; + self.0.frame_crop_bottom_offset = bottom; + self.0.frame_crop_left_offset = left; + self.0.frame_crop_right_offset = right; + self + } + + pub fn frame_crop(self, top: u32, bottom: u32, left: u32, right: u32) -> Self { + let sub_width_c = if self.0.chroma_format_idc > 2 { 1 } else { 2 }; + let sub_height_c = if self.0.chroma_format_idc > 1 { 1 } else { 2 }; + + let crop_unit_x = sub_width_c; + let crop_unit_y = sub_height_c * (if self.0.frame_mbs_only_flag { 1 } else { 2 }); + + self.frame_crop_offsets( + top / crop_unit_y, + bottom / crop_unit_y, + left / crop_unit_x, + right / crop_unit_x, + ) + } + + pub fn resolution(mut self, width: u32, height: u32) -> Self { + const MB_SIZE: u32 = 16; + + let mb_width = (width + MB_SIZE - 1) / MB_SIZE; + let mb_height = (height + MB_SIZE - 1) / MB_SIZE; + + self.0.pic_width_in_mbs_minus1 = (mb_width - 1) as u16; + self.0.pic_height_in_map_units_minus1 = (mb_height - 1) as u16; + + let compressed_width = mb_width * MB_SIZE; + let compressed_height = mb_height * MB_SIZE; + + if compressed_width != width || compressed_height != height { + self = self.frame_crop(0, compressed_height - height, 0, compressed_width - width); + } + + self + } + + pub fn chroma_format_idc(mut self, value: u8) -> Self { + self.0.chroma_format_idc = value; + self + } + + pub fn max_num_ref_frames(mut self, value: u8) -> Self { + self.0.max_num_ref_frames = value; + self + } + + pub fn frame_mbs_only_flag(mut self, value: bool) -> Self { + self.0.frame_mbs_only_flag = value; + self + } + + pub fn mb_adaptive_frame_field_flag(mut self, value: bool) -> Self { + self.0.mb_adaptive_frame_field_flag = value; + self + } + + pub fn seq_scaling_matrix_present_flag(mut self, value: bool) -> Self { + self.0.seq_scaling_matrix_present_flag = value; + self + } + + pub fn direct_8x8_inference_flag(mut self, value: bool) -> Self { + self.0.direct_8x8_inference_flag = value; + self + } + + pub fn vui_parameters_present(mut self) -> Self { + if self.0.vui_parameters_present_flag { + return self; + } + + self.0.vui_parameters_present_flag = true; + // Disable all options at default + self.0.vui_parameters.aspect_ratio_info_present_flag = false; + self.0.vui_parameters.overscan_info_present_flag = false; + self.0.vui_parameters.video_signal_type_present_flag = false; + self.0.vui_parameters.colour_description_present_flag = false; + self.0.vui_parameters.chroma_loc_info_present_flag = false; + self.0.vui_parameters.timing_info_present_flag = false; + self.0.vui_parameters.nal_hrd_parameters_present_flag = false; + self.0.vui_parameters.vcl_hrd_parameters_present_flag = false; + self.0.vui_parameters.pic_struct_present_flag = false; + self.0.vui_parameters.bitstream_restriction_flag = false; + self + } + + pub fn aspect_ratio_idc(mut self, value: u8) -> Self { + self = self.vui_parameters_present(); + self.0.vui_parameters.aspect_ratio_info_present_flag = true; + self.0.vui_parameters.aspect_ratio_idc = value; + self + } + + pub fn sar_resolution(mut self, width: u16, height: u16) -> Self { + self = self.aspect_ratio_idc(255); + self.0.vui_parameters.sar_width = width; + self.0.vui_parameters.sar_height = height; + self + } + + pub fn aspect_ratio(self, width_ratio: u16, height_ratio: u16) -> Self { + // H.264 Table E-1 + match (width_ratio, height_ratio) { + (1, 1) => self.aspect_ratio_idc(1), + (12, 11) => self.aspect_ratio_idc(2), + (10, 11) => self.aspect_ratio_idc(3), + (16, 11) => self.aspect_ratio_idc(4), + (40, 33) => self.aspect_ratio_idc(5), + (24, 11) => self.aspect_ratio_idc(6), + (20, 11) => self.aspect_ratio_idc(7), + (32, 11) => self.aspect_ratio_idc(8), + (80, 33) => self.aspect_ratio_idc(9), + (18, 11) => self.aspect_ratio_idc(10), + (15, 11) => self.aspect_ratio_idc(11), + (64, 33) => self.aspect_ratio_idc(12), + (160, 99) => self.aspect_ratio_idc(13), + (4, 3) => self.aspect_ratio_idc(14), + (3, 2) => self.aspect_ratio_idc(15), + (2, 1) => self.aspect_ratio_idc(16), + + _ => self.sar_resolution(width_ratio, height_ratio), + } + } + + pub fn timing_info( + mut self, + num_units_in_tick: u32, + time_scale: u32, + fixed_frame_rate_flag: bool, + ) -> Self { + self = self.vui_parameters_present(); + self.0.vui_parameters.timing_info_present_flag = true; + self.0.vui_parameters.num_units_in_tick = num_units_in_tick; + self.0.vui_parameters.time_scale = time_scale; + self.0.vui_parameters.fixed_frame_rate_flag = fixed_frame_rate_flag; + self + } + + pub fn log2_max_frame_num_minus4(mut self, value: u8) -> Self { + self.0.log2_max_frame_num_minus4 = value; + self + } + + pub fn max_frame_num(self, value: u32) -> Self { + self.log2_max_frame_num_minus4(value.ilog2() as u8 - 4u8) + } + + pub fn pic_order_cnt_type(mut self, value: u8) -> Self { + self.0.pic_order_cnt_type = value; + self + } + + pub fn log2_max_pic_order_cnt_lsb_minus4(mut self, value: u8) -> Self { + self.0.log2_max_pic_order_cnt_lsb_minus4 = value; + self + } + + pub fn max_pic_order_cnt_lsb(self, value: u32) -> Self { + self.log2_max_pic_order_cnt_lsb_minus4(value.ilog2() as u8 - 4u8) + } + + pub fn delta_pic_order_always_zero_flag(mut self, value: bool) -> Self { + self.0.delta_pic_order_always_zero_flag = value; + self + } + + pub fn bit_depth_chroma_minus8(mut self, value: u8) -> Self { + self.0.bit_depth_chroma_minus8 = value; + self + } + + pub fn bit_depth_chroma(self, value: u8) -> Self { + self.bit_depth_luma_minus8(value - 8u8) + } + + pub fn bit_depth_luma_minus8(mut self, value: u8) -> Self { + self.0.bit_depth_luma_minus8 = value; + self + } + + pub fn bit_depth_luma(self, value: u8) -> Self { + self.bit_depth_luma_minus8(value - 8u8) + } + + pub fn build(self) -> Rc { + Rc::new(self.0) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct HrdParams { + /// Plus 1 specifies the number of alternative CPB specifications in the + /// bitstream. The value of `cpb_cnt_minus1` shall be in the range of 0 to 31, + /// inclusive + pub cpb_cnt_minus1: u8, + /// Together with `bit_rate_value_minus1[ SchedSelIdx ]` specifies the + /// maximum input bit rate of the `SchedSelIdx`-th CPB. + pub bit_rate_scale: u8, + /// Together with `cpb_size_value_minus1[ SchedSelIdx ]` specifies the CPB + /// size of the SchedSelIdx-th CPB. + pub cpb_size_scale: u8, + + /// `[ SchedSelIdx ]` (together with bit_rate_scale) specifies the maximum + /// input bit rate for the SchedSelIdx-th CPB. + pub bit_rate_value_minus1: [u32; 32], + /// `[ SchedSelIdx ]` is used together with cpb_size_scale to specify the + /// SchedSelIdx-th CPB size. + pub cpb_size_value_minus1: [u32; 32], + /// `[ SchedSelIdx ]` equal to 0 specifies that to decode this bitstream by + /// the HRD using the `SchedSelIdx`-th CPB specification, the hypothetical + /// stream delivery scheduler (HSS) operates in an intermittent bit rate + /// mode. `cbr_flag[ SchedSelIdx ]` equal to 1 specifies that the HSS operates + /// in a constant bit rate (CBR) mode + pub cbr_flag: [bool; 32], + + /// Specifies the length in bits of the `initial_cpb_removal_delay[ + /// SchedSelIdx ]` and `initial_cpb_removal_delay_offset[ SchedSelIdx ]` syntax + /// elements of the buffering period SEI message. + pub initial_cpb_removal_delay_length_minus1: u8, + /// Specifies the length in bits of the `cpb_removal_delay` syntax element. + pub cpb_removal_delay_length_minus1: u8, + /// Specifies the length in bits of the `dpb_output_delay` syntax element. + pub dpb_output_delay_length_minus1: u8, + /// If greater than 0, specifies the length in bits of the `time_offset` + /// syntax element. `time_offset_length` equal to 0 specifies that the + /// `time_offset` syntax element is not present + pub time_offset_length: u8, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VuiParams { + /// Specifies whether `aspect_ratio_idc` is present. + pub aspect_ratio_info_present_flag: bool, + /// Specifies the value of the sample aspect ratio of the luma samples. + /// Table E-1 shows the meaning of the code. When aspect_ratio_idc indicates + /// Extended_SAR, the sample aspect ratio is represented by sar_width : + /// sar_height. When the aspect_ratio_idc syntax element is not present, + /// aspect_ratio_idc value shall be inferred to be equal to 0 + pub aspect_ratio_idc: u8, + + /* if aspect_ratio_idc == 255 */ + /// Indicates the horizontal size of the sample aspect ratio (in arbitrary + /// units) + pub sar_width: u16, + /// Indicates the vertical size of the sample aspect ratio (in the same + /// arbitrary units as sar_width). + pub sar_height: u16, + + /// If true specifies that the overscan_appropriate_flag is present. Else, + /// the preferred display method for the video signal is unspecified + pub overscan_info_present_flag: bool, + /* if overscan_info_present_flag */ + /// If true, indicates that the cropped decoded pictures output are suitable + /// for display using overscan. Else, indicates that the cropped decoded + /// pictures output contain visually important information in the entire + /// region out to the edges of the cropping rectangle of the picture, such + /// that the cropped decoded pictures output should not be displayed using + /// overscan. + pub overscan_appropriate_flag: bool, + + /// Specifies that video_format, video_full_range_flag and + /// colour_description_present_flag are present + pub video_signal_type_present_flag: bool, + /// Indicates the representation of the pictures as specified in Table E-2, + /// before being coded in accordance with this Recommendation | + /// International Standard. When the video_format syntax element is not + /// present, video_format value shall be inferred to be equal to 5. + pub video_format: u8, + /// Indicates the black level and range of the luma and chroma signals as + /// derived from E′Y, E′PB, and E′PR or E′ R, E′G, and E′B real-valued + /// component signals. + pub video_full_range_flag: bool, + /// Specifies that colour_primaries, transfer_characteristics and + /// matrix_coefficients are present. + pub colour_description_present_flag: bool, + /// Indicates the chromaticity coordinates of the source primaries as + /// specified in Table E-3 in terms of the CIE 1931 definition of x and y as + /// specified by ISO 11664-1. + pub colour_primaries: u8, + /// Retains same meaning as in the specification. + pub transfer_characteristics: u8, + /// Describes the matrix coefficients used in deriving luma and chroma + /// signals from the green, blue, and red, or Y, Z, and X primaries, as + /// specified in Table E-5. + pub matrix_coefficients: u8, + + /// Specifies that chroma_sample_loc_type_top_field and + /// chroma_sample_loc_type_bottom_field are present + pub chroma_loc_info_present_flag: bool, + /// Specify the location of chroma samples. See the spec for more details. + pub chroma_sample_loc_type_top_field: u8, + /// Specify the location of chroma samples. See the spec for more details. + pub chroma_sample_loc_type_bottom_field: u8, + + /// Specifies that num_units_in_tick, time_scale and fixed_frame_rate_flag + /// are present in the bitstream + pub timing_info_present_flag: bool, + /* if timing_info_present_flag */ + /// The number of time units of a clock operating at the frequency + /// time_scale Hz that corresponds to one increment (called a clock tick) of + /// a clock tick counter + pub num_units_in_tick: u32, + /// The number of time units that pass in one second. For example, a time + /// coordinate system that measures time using a 27 MHz clock has a + /// time_scale of 27 000 000. time_scale shall be greater than 0. + pub time_scale: u32, + /// Retains the same meaning as the specification. + pub fixed_frame_rate_flag: bool, + + /// Specifies that NAL HRD parameters (pertaining to Type II bitstream + /// conformance) are present. + pub nal_hrd_parameters_present_flag: bool, + /* if nal_hrd_parameters_present_flag */ + /// The NAL HDR parameters + pub nal_hrd_parameters: HrdParams, + /// Specifies that VCL HRD parameters (pertaining to all bitstream + /// conformance) are present. + pub vcl_hrd_parameters_present_flag: bool, + /* if vcl_hrd_parameters_present_flag */ + /// The VCL HRD parameters + pub vcl_hrd_parameters: HrdParams, + + /// Specifies the HRD operational mode as specified in Annex C. + pub low_delay_hrd_flag: bool, + + /// Specifies that picture timing SEI messages (clause D.2.3) are present + /// that include the pic_struct syntax element. + pub pic_struct_present_flag: bool, + + /// Specifies that the following coded video sequence bitstream restriction + /// parameters are present + pub bitstream_restriction_flag: bool, + /* if bitstream_restriction_flag */ + /// If false, indicates that no sample outside the picture boundaries and no + /// sample at a fractional sample position for which the sample value is + /// derived using one or more samples outside the picture boundaries is used + /// for inter prediction of any sample. If true, indicates that one or more + /// samples outside picture boundaries may be used in inter prediction. When + /// the motion_vectors_over_pic_boundaries_flag syntax element is not + /// present, motion_vectors_over_pic_boundaries_flag value shall be inferred + /// to be true. + pub motion_vectors_over_pic_boundaries_flag: bool, + /// Indicates a number of bytes not exceeded by the sum of the sizes of the + /// VCL NAL units associated with any coded picture in the coded video + /// sequence. + pub max_bytes_per_pic_denom: u32, + /// Indicates an upper bound for the number of coded bits of + /// macroblock_layer( ) data for any macroblock in any picture of the coded + /// video sequence + pub max_bits_per_mb_denom: u32, + /// Retains the same meaning as the specification. + pub log2_max_mv_length_horizontal: u32, + /// Retains the same meaning as the specification. + pub log2_max_mv_length_vertical: u32, + /// Indicates an upper bound for the number of frames buffers, in the + /// decoded picture buffer (DPB), that are required for storing frames, + /// complementary field pairs, and non-paired fields before output. It is a + /// requirement of bitstream conformance that the maximum number of frames, + /// complementary field pairs, or non-paired fields that precede any frame, + /// complementary field pair, or non-paired field in the coded video + /// sequence in decoding order and follow it in output order shall be less + /// than or equal to max_num_reorder_frames. The value of + /// max_num_reorder_frames shall be in the range of 0 to + /// max_dec_frame_buffering, inclusive. + /// + /// When the max_num_reorder_frames syntax element is not present, the value + /// of max_num_reorder_frames value shall be inferred as follows: + /// If profile_idc is equal to 44, 86, 100, 110, 122, or 244 and + /// constraint_set3_flag is equal to 1, the value of max_num_reorder_frames + /// shall be inferred to be equal to 0. + /// + /// Otherwise (profile_idc is not equal to 44, 86, 100, 110, 122, or 244 or + /// constraint_set3_flag is equal to 0), the value of max_num_reorder_frames + /// shall be inferred to be equal to MaxDpbFrames. + pub max_num_reorder_frames: u32, + /// Specifies the required size of the HRD decoded picture buffer (DPB) in + /// units of frame buffers. It is a requirement of bitstream conformance + /// that the coded video sequence shall not require a decoded picture buffer + /// with size of more than Max( 1, max_dec_frame_buffering ) frame buffers + /// to enable the output of decoded pictures at the output times specified + /// by dpb_output_delay of the picture timing SEI messages. The value of + /// max_dec_frame_buffering shall be greater than or equal to + /// max_num_ref_frames. An upper bound for the value of + /// max_dec_frame_buffering is specified by the level limits in clauses + /// A.3.1, A.3.2, G.10.2.1, and H.10.2. + /// + /// When the max_dec_frame_buffering syntax element is not present, the + /// value of max_dec_frame_buffering shall be inferred as follows: + /// + /// If profile_idc is equal to 44, 86, 100, 110, 122, or 244 and + /// constraint_set3_flag is equal to 1, the value of max_dec_frame_buffering + /// shall be inferred to be equal to 0. + /// + /// Otherwise (profile_idc is not equal to 44, 86, 100, 110, 122, or 244 or + /// constraint_set3_flag is equal to 0), the value of + /// max_dec_frame_buffering shall be inferred to be equal to MaxDpbFrames. + pub max_dec_frame_buffering: u32, +} + +impl Default for VuiParams { + fn default() -> Self { + Self { + aspect_ratio_info_present_flag: Default::default(), + aspect_ratio_idc: Default::default(), + sar_width: Default::default(), + sar_height: Default::default(), + overscan_info_present_flag: Default::default(), + overscan_appropriate_flag: Default::default(), + video_signal_type_present_flag: Default::default(), + video_format: 5, + video_full_range_flag: Default::default(), + colour_description_present_flag: Default::default(), + colour_primaries: 2, + transfer_characteristics: 2, + matrix_coefficients: 2, + chroma_loc_info_present_flag: Default::default(), + chroma_sample_loc_type_top_field: Default::default(), + chroma_sample_loc_type_bottom_field: Default::default(), + timing_info_present_flag: Default::default(), + num_units_in_tick: Default::default(), + time_scale: Default::default(), + fixed_frame_rate_flag: Default::default(), + nal_hrd_parameters_present_flag: Default::default(), + nal_hrd_parameters: Default::default(), + vcl_hrd_parameters_present_flag: Default::default(), + vcl_hrd_parameters: Default::default(), + low_delay_hrd_flag: Default::default(), + pic_struct_present_flag: Default::default(), + bitstream_restriction_flag: Default::default(), + motion_vectors_over_pic_boundaries_flag: Default::default(), + max_bytes_per_pic_denom: Default::default(), + max_bits_per_mb_denom: Default::default(), + log2_max_mv_length_horizontal: Default::default(), + log2_max_mv_length_vertical: Default::default(), + max_num_reorder_frames: Default::default(), + max_dec_frame_buffering: Default::default(), + } + } +} + +/// A H264 Picture Parameter Set. A syntax structure containing syntax elements +/// that apply to zero or more entire coded pictures as determined by the +/// `pic_parameter_set_id` syntax element found in each slice header. +#[derive(Debug, PartialEq, Eq)] +pub struct Pps { + /// Identifies the picture parameter set that is referred to in the slice header. + pub pic_parameter_set_id: u8, + + /// Refers to the active sequence parameter set. + pub seq_parameter_set_id: u8, + + /// Selects the entropy decoding method to be applied for the syntax + /// elements for which two descriptors appear in the syntax tables as + /// follows: If `entropy_coding_mode_flag` is false, the method specified by + /// the left descriptor in the syntax table is applied (Exp-Golomb coded, + /// see clause 9.1 or CAVLC, see clause 9.2). Otherwise + /// (`entropy_coding_mode_flag` is true), the method specified by the right + /// descriptor in the syntax table is applied (CABAC, see clause 9.3). + pub entropy_coding_mode_flag: bool, + + /// If true, specifies that the syntax elements delta_pic_order_cnt_bottom + /// (when `pic_order_cnt_type` is equal to 0) or `delta_pic_order_cnt[1]` + /// (when `pic_order_cnt_type` is equal to 1), which are related to picture + /// order counts for the bottom field of a coded frame, are present in the + /// slice headers for coded frames as specified in clause 7.3.3. Otherwise, + /// specifies that the syntax elements `delta_pic_order_cnt_bottom` and + /// `delta_pic_order_cnt[1]` are not present in the slice headers. + pub bottom_field_pic_order_in_frame_present_flag: bool, + + /// Plus 1 specifies the number of slice groups for a picture. When + /// `num_slice_groups_minus1` is equal to 0, all slices of the picture + /// belong to the same slice group. The allowed range of + /// `num_slice_groups_minus1` is specified in Annex A. + pub num_slice_groups_minus1: u32, + + /// Specifies how `num_ref_idx_l0_active_minus1` is inferred for P, SP, and + /// B slices with `num_ref_idx_active_override_flag` not set. + pub num_ref_idx_l0_default_active_minus1: u8, + + /// Specifies how `num_ref_idx_l1_active_minus1` is inferred for B slices + /// with `num_ref_idx_active_override_flag` not set. + pub num_ref_idx_l1_default_active_minus1: u8, + + /// If not set, specifies that the default weighted prediction shall be + /// applied to P and SP slices. If set, specifies that explicit weighted + /// prediction shall be applied to P and SP slices. + pub weighted_pred_flag: bool, + + /// `weighted_bipred_idc` equal to 0 specifies that the default weighted + /// prediction shall be applied to B slices. `weighted_bipred_idc` equal to + /// 1 specifies that explicit weighted prediction shall be applied to B + /// slices. `weighted_bipred_idc` equal to 2 specifies that implicit + /// weighted prediction shall be applied to B slices + pub weighted_bipred_idc: u8, + + /// Specifies the initial value minus 26 of SliceQPY for each slice. The + /// initial value is modified at the slice layer when a non-zero value of + /// `slice_qp_delta` is decoded, and is modified further when a non-zero + /// value of `mb_qp_delta` is decoded at the macroblock layer. + pub pic_init_qp_minus26: i8, + + /// Specifies the initial value minus 26 of SliceQSY for all macroblocks in + /// SP or SI slices. The initial value is modified at the slice layer when a + /// non-zero value of `slice_qs_delta` is decoded. + pub pic_init_qs_minus26: i8, + + /// Specifies the offset that shall be added to QP Y and QSY for addressing + /// the table of QPC values for the Cb chroma component. + pub chroma_qp_index_offset: i8, + + /// If set, specifies that a set of syntax elements controlling the + /// characteristics of the deblocking filter is present in the slice header. + /// If not set, specifies that the set of syntax elements controlling the + /// characteristics of the deblocking filter is not present in the slice + /// headers and their inferred values are in effect. + pub deblocking_filter_control_present_flag: bool, + + /// If not set, specifies that intra prediction allows usage of residual + /// data and decoded samples of neighbouring macroblocks coded using Inter + /// macroblock prediction modes for the prediction of macroblocks coded + /// using Intra macroblock prediction modes. If set, specifies constrained + /// intra prediction, in which case prediction of macroblocks coded using + /// Intra macroblock prediction modes only uses residual data and decoded + /// samples from I or SI macroblock types. + pub constrained_intra_pred_flag: bool, + + /// If not set, specifies that the `redundant_pic_cnt` syntax element is not + /// present in slice headers, coded slice data partition B NAL units, and + /// coded slice data partition C NAL units that refer (either directly or by + /// association with a corresponding coded slice data partition A NAL unit) + /// to the picture parameter set. If set, specifies that the + /// `redundant_pic_cnt` syntax element is present in all slice headers, + /// coded slice data partition B NAL units, and coded slice data partition C + /// NAL units that refer (either directly or by association with a + /// corresponding coded slice data partition A NAL unit) to the picture + /// parameter set. + pub redundant_pic_cnt_present_flag: bool, + + /// If set, specifies that the 8x8 transform decoding process may be in use + /// (see clause 8.5). If not set, specifies that the 8x8 transform decoding + /// process is not in use. + pub transform_8x8_mode_flag: bool, + + /// If set, specifies that parameters are present to modify the scaling + /// lists specified in the sequence parameter set. If not set, specifies + /// that the scaling lists used for the picture shall be inferred to be + /// equal to those specified by the sequence parameter set. + pub pic_scaling_matrix_present_flag: bool, + + /// 4x4 Scaling list as read with 7.3.2.1.1.1 + pub scaling_lists_4x4: [[u8; 16]; 6], + /// 8x8 Scaling list as read with 7.3.2.1.1.1 + pub scaling_lists_8x8: [[u8; 64]; 6], + + /// Specifies the offset that shall be added to QPY and QSY for addressing + /// the table of QPC values for the Cr chroma component. When + /// `second_chroma_qp_index_offset` is not present, it shall be inferred to be + /// equal to `chroma_qp_index_offset`. + pub second_chroma_qp_index_offset: i8, + + /// The SPS referenced by this PPS. + pub sps: Rc, +} + +pub struct PpsBuilder(Pps); + +impl PpsBuilder { + pub fn new(sps: Rc) -> Self { + PpsBuilder(Pps { + pic_parameter_set_id: 0, + seq_parameter_set_id: sps.seq_parameter_set_id, + entropy_coding_mode_flag: false, + bottom_field_pic_order_in_frame_present_flag: false, + num_slice_groups_minus1: 0, + num_ref_idx_l0_default_active_minus1: 0, + num_ref_idx_l1_default_active_minus1: 0, + weighted_pred_flag: false, + weighted_bipred_idc: 0, + pic_init_qp_minus26: 0, + pic_init_qs_minus26: 0, + chroma_qp_index_offset: 0, + deblocking_filter_control_present_flag: false, + constrained_intra_pred_flag: false, + redundant_pic_cnt_present_flag: false, + transform_8x8_mode_flag: false, + pic_scaling_matrix_present_flag: false, + scaling_lists_4x4: [[0; 16]; 6], + scaling_lists_8x8: [[0; 64]; 6], + second_chroma_qp_index_offset: 0, + sps, + }) + } + + pub fn pic_parameter_set_id(mut self, value: u8) -> Self { + self.0.pic_parameter_set_id = value; + self + } + + pub fn pic_init_qp_minus26(mut self, value: i8) -> Self { + self.0.pic_init_qp_minus26 = value; + self + } + + pub fn pic_init_qp(self, value: u8) -> Self { + self.pic_init_qp_minus26(value as i8 - 26) + } + + pub fn deblocking_filter_control_present_flag(mut self, value: bool) -> Self { + self.0.deblocking_filter_control_present_flag = value; + self + } + + pub fn num_ref_idx_l0_default_active_minus1(mut self, value: u8) -> Self { + self.0.num_ref_idx_l0_default_active_minus1 = value; + self + } + + pub fn num_ref_idx_l0_default_active(self, value: u8) -> Self { + self.num_ref_idx_l0_default_active_minus1(value - 1) + } + + pub fn num_ref_idx_l1_default_active_minus1(mut self, value: u8) -> Self { + self.0.num_ref_idx_l1_default_active_minus1 = value; + self + } + + pub fn num_ref_idx_l1_default_active(self, value: u8) -> Self { + self.num_ref_idx_l1_default_active_minus1(value - 1) + } + + pub fn build(self) -> Rc { + Rc::new(self.0) + } +} + +#[derive(Debug, Default)] +pub struct Parser { + active_spses: BTreeMap>, + active_ppses: BTreeMap>, +} + +impl Parser { + fn fill_default_scaling_list_4x4(scaling_list4x4: &mut [u8; 16], i: usize) { + // See table 7.2 in the spec. + assert!(i < 6); + if i < 3 { + *scaling_list4x4 = DEFAULT_4X4_INTRA; + } else if i < 6 { + *scaling_list4x4 = DEFAULT_4X4_INTER; + } + } + + fn fill_default_scaling_list_8x8(scaling_list8x8: &mut [u8; 64], i: usize) { + assert!(i < 6); + if i % 2 == 0 { + *scaling_list8x8 = DEFAULT_8X8_INTRA; + } else { + *scaling_list8x8 = DEFAULT_8X8_INTER; + } + } + + fn fill_fallback_scaling_list_4x4( + scaling_list4x4: &mut [[u8; 16]; 6], + i: usize, + default_scaling_list_intra: &[u8; 16], + default_scaling_list_inter: &[u8; 16], + ) { + // See table 7.2 in the spec. + scaling_list4x4[i] = match i { + 0 => *default_scaling_list_intra, + 1 => scaling_list4x4[0], + 2 => scaling_list4x4[1], + 3 => *default_scaling_list_inter, + 4 => scaling_list4x4[3], + 5 => scaling_list4x4[4], + _ => panic!("Unexpected value {}", i), + } + } + + fn fill_fallback_scaling_list_8x8( + scaling_list8x8: &mut [[u8; 64]; 6], + i: usize, + default_scaling_list_intra: &[u8; 64], + default_scaling_list_inter: &[u8; 64], + ) { + // See table 7.2 in the spec. + scaling_list8x8[i] = match i { + 0 => *default_scaling_list_intra, + 1 => *default_scaling_list_inter, + 2 => scaling_list8x8[0], + 3 => scaling_list8x8[1], + 4 => scaling_list8x8[2], + 5 => scaling_list8x8[3], + _ => panic!("Unexpected value {}", i), + } + } + + fn fill_scaling_list_flat( + scaling_list4x4: &mut [[u8; 16]; 6], + scaling_list8x8: &mut [[u8; 64]; 6], + ) { + // (7-8) in the spec. + for outer in scaling_list4x4 { + for inner in outer { + *inner = 16; + } + } + + // (7-9) in the spec. + for outer in scaling_list8x8 { + for inner in outer { + *inner = 16; + } + } + } + + fn parse_scaling_list>( + r: &mut BitReader, + scaling_list: &mut U, + use_default: &mut bool, + ) -> Result<(), String> { + // 7.3.2.1.1.1 + let mut last_scale = 8u8; + let mut next_scale = 8u8; + + for j in 0..scaling_list.as_mut().len() { + if next_scale != 0 { + let delta_scale = r.read_se::()?; + next_scale = ((last_scale as i32 + delta_scale + 256) % 256) as u8; + *use_default = j == 0 && next_scale == 0; + if *use_default { + return Ok(()); + } + } + + scaling_list.as_mut()[j] = if next_scale == 0 { + last_scale + } else { + next_scale + }; + + last_scale = scaling_list.as_mut()[j]; + } + + Ok(()) + } + + fn parse_sps_scaling_lists(r: &mut BitReader, sps: &mut Sps) -> Result<(), String> { + let scaling_lists4x4 = &mut sps.scaling_lists_4x4; + let scaling_lisst8x8 = &mut sps.scaling_lists_8x8; + + // Parse scaling_list4x4 + for i in 0..6 { + let seq_scaling_list_present_flag = r.read_bit()?; + if seq_scaling_list_present_flag { + let mut use_default = false; + + Parser::parse_scaling_list(r, &mut scaling_lists4x4[i], &mut use_default)?; + + if use_default { + Parser::fill_default_scaling_list_4x4(&mut scaling_lists4x4[i], i); + } + } else { + Parser::fill_fallback_scaling_list_4x4( + scaling_lists4x4, + i, + &DEFAULT_4X4_INTRA, + &DEFAULT_4X4_INTER, + ); + } + } + + // Parse scaling_list8x8 + let num_8x8 = if sps.chroma_format_idc != 3 { 2 } else { 6 }; + for i in 0..num_8x8 { + let seq_scaling_list_present_flag = r.read_bit()?; + if seq_scaling_list_present_flag { + let mut use_default = false; + Parser::parse_scaling_list(r, &mut scaling_lisst8x8[i], &mut use_default)?; + + if use_default { + Parser::fill_default_scaling_list_8x8(&mut scaling_lisst8x8[i], i); + } + } else { + Parser::fill_fallback_scaling_list_8x8( + scaling_lisst8x8, + i, + &DEFAULT_8X8_INTRA, + &DEFAULT_8X8_INTER, + ); + } + } + Ok(()) + } + + fn parse_pps_scaling_lists(r: &mut BitReader, pps: &mut Pps, sps: &Sps) -> Result<(), String> { + let scaling_lists4x4 = &mut pps.scaling_lists_4x4; + let scaling_lists8x8 = &mut pps.scaling_lists_8x8; + + for i in 0..6 { + let pic_scaling_list_present_flag = r.read_bit()?; + if pic_scaling_list_present_flag { + let mut use_default = false; + + Parser::parse_scaling_list(r, &mut scaling_lists4x4[i], &mut use_default)?; + + if use_default { + Parser::fill_default_scaling_list_4x4(&mut scaling_lists4x4[i], i); + } + } else if !sps.seq_scaling_matrix_present_flag { + // Table 7-2: Fallback rule A + Parser::fill_fallback_scaling_list_4x4( + scaling_lists4x4, + i, + &DEFAULT_4X4_INTRA, + &DEFAULT_4X4_INTER, + ); + } else { + // Table 7-2: Fallback rule B + Parser::fill_fallback_scaling_list_4x4( + scaling_lists4x4, + i, + &sps.scaling_lists_4x4[0], + &sps.scaling_lists_4x4[3], + ); + } + } + + if pps.transform_8x8_mode_flag { + let num8x8 = if sps.chroma_format_idc != 3 { 2 } else { 6 }; + + for i in 0..num8x8 { + let pic_scaling_list_present_flag = r.read_bit()?; + if pic_scaling_list_present_flag { + let mut use_default = false; + + Parser::parse_scaling_list(r, &mut scaling_lists8x8[i], &mut use_default)?; + + if use_default { + Parser::fill_default_scaling_list_8x8(&mut scaling_lists8x8[i], i); + } + } else if !sps.seq_scaling_matrix_present_flag { + // Table 7-2: Fallback rule A + Parser::fill_fallback_scaling_list_8x8( + scaling_lists8x8, + i, + &DEFAULT_8X8_INTRA, + &DEFAULT_8X8_INTER, + ); + } else { + // Table 7-2: Fallback rule B + Parser::fill_fallback_scaling_list_8x8( + scaling_lists8x8, + i, + &sps.scaling_lists_8x8[0], + &sps.scaling_lists_8x8[1], + ); + } + } + } + + Ok(()) + } + + fn parse_hrd(r: &mut BitReader, hrd: &mut HrdParams) -> Result<(), String> { + hrd.cpb_cnt_minus1 = r.read_ue_max(31)?; + hrd.bit_rate_scale = r.read_bits(4)?; + hrd.cpb_size_scale = r.read_bits(4)?; + + for sched_sel_idx in 0..=usize::from(hrd.cpb_cnt_minus1) { + hrd.bit_rate_value_minus1[sched_sel_idx] = r.read_ue()?; + hrd.cpb_size_value_minus1[sched_sel_idx] = r.read_ue()?; + hrd.cbr_flag[sched_sel_idx] = r.read_bit()?; + } + + hrd.initial_cpb_removal_delay_length_minus1 = r.read_bits(5)?; + hrd.cpb_removal_delay_length_minus1 = r.read_bits(5)?; + hrd.dpb_output_delay_length_minus1 = r.read_bits(5)?; + hrd.time_offset_length = r.read_bits(5)?; + Ok(()) + } + + fn parse_vui(r: &mut BitReader, sps: &mut Sps) -> Result<(), String> { + let vui = &mut sps.vui_parameters; + + vui.aspect_ratio_info_present_flag = r.read_bit()?; + if vui.aspect_ratio_info_present_flag { + vui.aspect_ratio_idc = r.read_bits(8)?; + if vui.aspect_ratio_idc == 255 { + vui.sar_width = r.read_bits(16)?; + vui.sar_height = r.read_bits(16)?; + } + } + + vui.overscan_info_present_flag = r.read_bit()?; + if vui.overscan_info_present_flag { + vui.overscan_appropriate_flag = r.read_bit()?; + } + + vui.video_signal_type_present_flag = r.read_bit()?; + if vui.video_signal_type_present_flag { + vui.video_format = r.read_bits(3)?; + vui.video_full_range_flag = r.read_bit()?; + vui.colour_description_present_flag = r.read_bit()?; + if vui.colour_description_present_flag { + vui.colour_primaries = r.read_bits(8)?; + vui.transfer_characteristics = r.read_bits(8)?; + vui.matrix_coefficients = r.read_bits(8)?; + } + } + + vui.chroma_loc_info_present_flag = r.read_bit()?; + if vui.chroma_loc_info_present_flag { + vui.chroma_sample_loc_type_top_field = r.read_ue_max(5)?; + vui.chroma_sample_loc_type_bottom_field = r.read_ue_max(5)?; + } + + vui.timing_info_present_flag = r.read_bit()?; + if vui.timing_info_present_flag { + vui.num_units_in_tick = r.read_bits::(31)? << 1; + vui.num_units_in_tick |= r.read_bit()? as u32; + if vui.num_units_in_tick == 0 { + return Err("num_units_in_tick == 0, which is not allowed by E.2.1".into()); + } + + vui.time_scale = r.read_bits::(31)? << 1; + vui.time_scale |= r.read_bit()? as u32; + if vui.time_scale == 0 { + return Err("time_scale == 0, which is not allowed by E.2.1".into()); + } + + vui.fixed_frame_rate_flag = r.read_bit()?; + } + + vui.nal_hrd_parameters_present_flag = r.read_bit()?; + if vui.nal_hrd_parameters_present_flag { + Parser::parse_hrd(r, &mut vui.nal_hrd_parameters)?; + } + + vui.vcl_hrd_parameters_present_flag = r.read_bit()?; + if vui.vcl_hrd_parameters_present_flag { + Parser::parse_hrd(r, &mut vui.vcl_hrd_parameters)?; + } + + if vui.nal_hrd_parameters_present_flag || vui.vcl_hrd_parameters_present_flag { + vui.low_delay_hrd_flag = r.read_bit()?; + } + + vui.pic_struct_present_flag = r.read_bit()?; + vui.bitstream_restriction_flag = r.read_bit()?; + + if vui.bitstream_restriction_flag { + vui.motion_vectors_over_pic_boundaries_flag = r.read_bit()?; + vui.max_bytes_per_pic_denom = r.read_ue()?; + vui.max_bits_per_mb_denom = r.read_ue_max(16)?; + vui.log2_max_mv_length_horizontal = r.read_ue_max(16)?; + vui.log2_max_mv_length_vertical = r.read_ue_max(16)?; + vui.max_num_reorder_frames = r.read_ue()?; + vui.max_dec_frame_buffering = r.read_ue()?; + } + + Ok(()) + } + + /// Parse a SPS and add it to the list of active SPSes. + /// + /// Returns a reference to the new SPS. + pub fn parse_sps(&mut self, nalu: &Nalu) -> Result<&Rc, String> { + if !matches!(nalu.header.type_, NaluType::Sps) { + return Err(format!( + "Invalid NALU type, expected {:?}, got {:?}", + NaluType::Sps, + nalu.header.type_ + )); + } + + let data = nalu.as_ref(); + // Skip the header + let mut r = BitReader::new(&data[nalu.header.len()..], true); + let mut sps = Sps { + profile_idc: r.read_bits(8)?, + constraint_set0_flag: r.read_bit()?, + constraint_set1_flag: r.read_bit()?, + constraint_set2_flag: r.read_bit()?, + constraint_set3_flag: r.read_bit()?, + constraint_set4_flag: r.read_bit()?, + constraint_set5_flag: r.read_bit()?, + ..Default::default() + }; + + // skip reserved_zero_2bits + r.skip_bits(2)?; + + let level: u8 = r.read_bits(8)?; + sps.level_idc = Level::try_from(level)?; + sps.seq_parameter_set_id = r.read_ue_max(31)?; + + if sps.profile_idc == 100 + || sps.profile_idc == 110 + || sps.profile_idc == 122 + || sps.profile_idc == 244 + || sps.profile_idc == 44 + || sps.profile_idc == 83 + || sps.profile_idc == 86 + || sps.profile_idc == 118 + || sps.profile_idc == 128 + || sps.profile_idc == 138 + || sps.profile_idc == 139 + || sps.profile_idc == 134 + || sps.profile_idc == 135 + { + sps.chroma_format_idc = r.read_ue_max(3)?; + if sps.chroma_format_idc == 3 { + sps.separate_colour_plane_flag = r.read_bit()?; + } + + sps.bit_depth_luma_minus8 = r.read_ue_max(6)?; + sps.bit_depth_chroma_minus8 = r.read_ue_max(6)?; + sps.qpprime_y_zero_transform_bypass_flag = r.read_bit()?; + sps.seq_scaling_matrix_present_flag = r.read_bit()?; + + if sps.seq_scaling_matrix_present_flag { + Parser::parse_sps_scaling_lists(&mut r, &mut sps)?; + } else { + Parser::fill_scaling_list_flat( + &mut sps.scaling_lists_4x4, + &mut sps.scaling_lists_8x8, + ); + } + } else { + sps.chroma_format_idc = 1; + Parser::fill_scaling_list_flat(&mut sps.scaling_lists_4x4, &mut sps.scaling_lists_8x8); + } + + sps.log2_max_frame_num_minus4 = r.read_ue_max(12)?; + + sps.pic_order_cnt_type = r.read_ue_max(2)?; + + if sps.pic_order_cnt_type == 0 { + sps.log2_max_pic_order_cnt_lsb_minus4 = r.read_ue_max(12)?; + sps.expected_delta_per_pic_order_cnt_cycle = 0; + } else if sps.pic_order_cnt_type == 1 { + sps.delta_pic_order_always_zero_flag = r.read_bit()?; + sps.offset_for_non_ref_pic = r.read_se()?; + sps.offset_for_top_to_bottom_field = r.read_se()?; + sps.num_ref_frames_in_pic_order_cnt_cycle = r.read_ue_max(254)?; + + let mut offset_acc = 0; + for i in 0..usize::from(sps.num_ref_frames_in_pic_order_cnt_cycle) { + sps.offset_for_ref_frame[i] = r.read_se()?; + + // (7-12) in the spec. + offset_acc += sps.offset_for_ref_frame[i]; + } + + sps.expected_delta_per_pic_order_cnt_cycle = offset_acc; + } + + sps.max_num_ref_frames = r.read_ue_max(DPB_MAX_SIZE as u32)?; + sps.gaps_in_frame_num_value_allowed_flag = r.read_bit()?; + sps.pic_width_in_mbs_minus1 = r.read_ue()?; + sps.pic_height_in_map_units_minus1 = r.read_ue()?; + sps.frame_mbs_only_flag = r.read_bit()?; + + if !sps.frame_mbs_only_flag { + sps.mb_adaptive_frame_field_flag = r.read_bit()?; + } + + sps.direct_8x8_inference_flag = r.read_bit()?; + sps.frame_cropping_flag = r.read_bit()?; + + if sps.frame_cropping_flag { + sps.frame_crop_left_offset = r.read_ue()?; + sps.frame_crop_right_offset = r.read_ue()?; + sps.frame_crop_top_offset = r.read_ue()?; + sps.frame_crop_bottom_offset = r.read_ue()?; + + // Validate that cropping info is valid. + let (crop_unit_x, crop_unit_y) = sps.crop_unit_x_y(); + + let _ = sps + .frame_crop_left_offset + .checked_add(sps.frame_crop_right_offset) + .and_then(|r| r.checked_mul(crop_unit_x)) + .and_then(|r| sps.width().checked_sub(r)) + .ok_or::("Invalid frame crop width".into())?; + + let _ = sps + .frame_crop_top_offset + .checked_add(sps.frame_crop_bottom_offset) + .and_then(|r| r.checked_mul(crop_unit_y)) + .and_then(|r| sps.height().checked_sub(r)) + .ok_or::("invalid frame crop height".into())?; + } + + sps.vui_parameters_present_flag = r.read_bit()?; + if sps.vui_parameters_present_flag { + Parser::parse_vui(&mut r, &mut sps)?; + } + + let key = sps.seq_parameter_set_id; + + if self.active_spses.keys().len() >= MAX_SPS_COUNT as usize { + return Err("Broken data: Number of active SPSs > MAX_SPS_COUNT".into()); + } + + let sps = Rc::new(sps); + self.active_spses.remove(&key); + Ok(self.active_spses.entry(key).or_insert(sps)) + } + + pub fn parse_pps(&mut self, nalu: &Nalu) -> Result<&Pps, String> { + if !matches!(nalu.header.type_, NaluType::Pps) { + return Err(format!( + "Invalid NALU type, expected {:?}, got {:?}", + NaluType::Pps, + nalu.header.type_ + )); + } + + let data = nalu.as_ref(); + // Skip the header + let mut r = BitReader::new(&data[nalu.header.len()..], true); + let pic_parameter_set_id = r.read_ue_max(MAX_PPS_COUNT as u32 - 1)?; + let seq_parameter_set_id = r.read_ue_max(MAX_SPS_COUNT as u32 - 1)?; + let sps = self.get_sps(seq_parameter_set_id).ok_or::(format!( + "Could not get SPS for seq_parameter_set_id {}", + seq_parameter_set_id + ))?; + let mut pps = Pps { + pic_parameter_set_id, + seq_parameter_set_id, + sps: Rc::clone(sps), + scaling_lists_4x4: [[0; 16]; 6], + scaling_lists_8x8: [[0; 64]; 6], + entropy_coding_mode_flag: Default::default(), + bottom_field_pic_order_in_frame_present_flag: Default::default(), + num_slice_groups_minus1: Default::default(), + num_ref_idx_l0_default_active_minus1: Default::default(), + num_ref_idx_l1_default_active_minus1: Default::default(), + weighted_pred_flag: Default::default(), + weighted_bipred_idc: Default::default(), + pic_init_qp_minus26: Default::default(), + pic_init_qs_minus26: Default::default(), + chroma_qp_index_offset: Default::default(), + deblocking_filter_control_present_flag: Default::default(), + constrained_intra_pred_flag: Default::default(), + redundant_pic_cnt_present_flag: Default::default(), + transform_8x8_mode_flag: Default::default(), + second_chroma_qp_index_offset: Default::default(), + pic_scaling_matrix_present_flag: Default::default(), + }; + + pps.entropy_coding_mode_flag = r.read_bit()?; + pps.bottom_field_pic_order_in_frame_present_flag = r.read_bit()?; + pps.num_slice_groups_minus1 = r.read_ue_max(7)?; + + if pps.num_slice_groups_minus1 > 0 { + return Err("Stream contain unsupported/unimplemented NALs".into()); + } + + pps.num_ref_idx_l0_default_active_minus1 = r.read_ue_max(31)?; + pps.num_ref_idx_l1_default_active_minus1 = r.read_ue_max(31)?; + + pps.weighted_pred_flag = r.read_bit()?; + pps.weighted_bipred_idc = r.read_bits(2)?; + + let qp_bd_offset_y = i32::from(6 * (sps.bit_depth_luma_minus8)); + pps.pic_init_qp_minus26 = r.read_se_bounded(-(26 + qp_bd_offset_y), 25)?; + pps.pic_init_qs_minus26 = r.read_se_bounded(-26, 25)?; + + pps.chroma_qp_index_offset = r.read_se_bounded(-12, 12)?; + + // When second_chroma_qp_index_offset is not present, it shall be + // inferred to be equal to chroma_qp_index_offset. + pps.second_chroma_qp_index_offset = pps.chroma_qp_index_offset; + + pps.deblocking_filter_control_present_flag = r.read_bit()?; + pps.constrained_intra_pred_flag = r.read_bit()?; + pps.redundant_pic_cnt_present_flag = r.read_bit()?; + + if r.has_more_rsbp_data() { + pps.transform_8x8_mode_flag = r.read_bit()?; + pps.pic_scaling_matrix_present_flag = r.read_bit()?; + + if pps.pic_scaling_matrix_present_flag { + Parser::parse_pps_scaling_lists(&mut r, &mut pps, sps)?; + } + + pps.second_chroma_qp_index_offset = r.read_se()?; + } + + if !pps.pic_scaling_matrix_present_flag { + // If not set, specifies that the scaling lists used for the picture + // shall be inferred to be equal to those specified by the sequence + // parameter set. When pic_scaling_matrix_present_flag is not + // present, it shall be inferred to be not set. + pps.scaling_lists_4x4 = sps.scaling_lists_4x4; + pps.scaling_lists_8x8 = sps.scaling_lists_8x8; + } + + let key = pps.pic_parameter_set_id; + + if self.active_ppses.keys().len() >= MAX_PPS_COUNT as usize { + return Err("Broken Data: number of active PPSs > MAX_PPS_COUNT".into()); + } + + let pps = Rc::new(pps); + self.active_ppses.remove(&key); + Ok(self.active_ppses.entry(key).or_insert(pps)) + } + + fn parse_ref_pic_list_modification( + r: &mut BitReader, + num_ref_idx_active_minus1: u8, + ref_list_mods: &mut Vec, + ) -> Result<(), String> { + if num_ref_idx_active_minus1 >= 32 { + return Err("Broken Data: num_ref_idx_active_minus1 >= 32".into()); + } + + loop { + let mut pic_num_mod = RefPicListModification { + modification_of_pic_nums_idc: r.read_ue_max(3)?, + ..Default::default() + }; + + match pic_num_mod.modification_of_pic_nums_idc { + 0 | 1 => { + pic_num_mod.abs_diff_pic_num_minus1 = r.read_ue()?; + } + + 2 => { + pic_num_mod.long_term_pic_num = r.read_ue()?; + } + + 3 => { + ref_list_mods.push(pic_num_mod); + break; + } + + _ => return Err("Broken Data: modification_of_pic_nums_idc > 3".into()), + } + + ref_list_mods.push(pic_num_mod); + } + + Ok(()) + } + + fn parse_ref_pic_list_modifications( + r: &mut BitReader, + header: &mut SliceHeader, + ) -> Result<(), String> { + if !header.slice_type.is_i() && !header.slice_type.is_si() { + header.ref_pic_list_modification_flag_l0 = r.read_bit()?; + if header.ref_pic_list_modification_flag_l0 { + Parser::parse_ref_pic_list_modification( + r, + header.num_ref_idx_l0_active_minus1, + &mut header.ref_pic_list_modification_l0, + )?; + } + } + + if header.slice_type.is_b() { + header.ref_pic_list_modification_flag_l1 = r.read_bit()?; + if header.ref_pic_list_modification_flag_l1 { + Parser::parse_ref_pic_list_modification( + r, + header.num_ref_idx_l1_active_minus1, + &mut header.ref_pic_list_modification_l1, + )?; + } + } + + Ok(()) + } + + fn parse_pred_weight_table( + r: &mut BitReader, + sps: &Sps, + header: &mut SliceHeader, + ) -> Result<(), String> { + let pt = &mut header.pred_weight_table; + pt.luma_log2_weight_denom = r.read_ue_max(7)?; + + // When luma_weight_l0_flag is equal to 0, luma_weight_l0[i] shall be + // inferred to be equal to 2 ^ luma_log2_weight_denom for + // RefPicList0[i]. + let default_luma_weight = 1 << pt.luma_log2_weight_denom; + for i in 0..=header.num_ref_idx_l0_active_minus1 { + pt.luma_weight_l0[usize::from(i)] = default_luma_weight; + } + + // When luma_weight_l1_flag is equal to 1, luma_weight_l1[i] shall be + // inferred to be equal to 2 ^ luma_log2_weight_denom for + // RefPicList1[i]. + if header.slice_type.is_b() { + for i in 0..=header.num_ref_idx_l1_active_minus1 { + pt.luma_weight_l1[usize::from(i)] = default_luma_weight; + } + } + + if sps.chroma_array_type() != 0 { + pt.chroma_log2_weight_denom = r.read_ue_max(7)?; + let default_chroma_weight = 1 << pt.chroma_log2_weight_denom; + + // When chroma_weight_l0_flag is equal to 0, chroma_weight_l0[i] + // shall be inferred to be equal to 2 ^ chroma_log2_weight_denom for + // RefPicList0[i]. + for i in 0..=header.num_ref_idx_l0_active_minus1 { + pt.chroma_weight_l0[usize::from(i)][0] = default_chroma_weight; + pt.chroma_weight_l0[usize::from(i)][1] = default_chroma_weight; + } + + // When chroma_weight_l1_flag is equal to 0, chroma_weight_l1[i] + // shall be inferred to be equal to 2 ^ chroma_log2_weight_denom for + // RefPicList1[i]. + for i in 0..=header.num_ref_idx_l1_active_minus1 { + pt.chroma_weight_l1[usize::from(i)][0] = default_chroma_weight; + pt.chroma_weight_l1[usize::from(i)][1] = default_chroma_weight; + } + } + + for i in 0..=header.num_ref_idx_l0_active_minus1 { + let luma_weight_l0_flag = r.read_bit()?; + + if luma_weight_l0_flag { + pt.luma_weight_l0[usize::from(i)] = r.read_se_bounded(-128, 127)?; + pt.luma_offset_l0[usize::from(i)] = r.read_se_bounded(-128, 127)?; + } + + if sps.chroma_array_type() != 0 { + let chroma_weight_l0_flag = r.read_bit()?; + if chroma_weight_l0_flag { + for j in 0..2 { + pt.chroma_weight_l0[usize::from(i)][j] = r.read_se_bounded(-128, 127)?; + pt.chroma_offset_l0[usize::from(i)][j] = r.read_se_bounded(-128, 127)?; + } + } + } + } + + if header.slice_type.is_b() { + for i in 0..=header.num_ref_idx_l1_active_minus1 { + let luma_weight_l1_flag = r.read_bit()?; + + if luma_weight_l1_flag { + pt.luma_weight_l1[usize::from(i)] = r.read_se_bounded(-128, 127)?; + pt.luma_offset_l1[usize::from(i)] = r.read_se_bounded(-128, 127)?; + } + + if sps.chroma_array_type() != 0 { + let chroma_weight_l1_flag = r.read_bit()?; + if chroma_weight_l1_flag { + for j in 0..2 { + pt.chroma_weight_l1[usize::from(i)][j] = + r.read_se_bounded(-128, 127)?; + pt.chroma_offset_l1[usize::from(i)][j] = + r.read_se_bounded(-128, 127)?; + } + } + } + } + } + + Ok(()) + } + + fn parse_dec_ref_pic_marking( + r: &mut BitReader, + nalu: &Nalu, + header: &mut SliceHeader, + ) -> Result<(), String> { + let rpm = &mut header.dec_ref_pic_marking; + + let num_bits_left = r.num_bits_left(); + if nalu.header.idr_pic_flag { + rpm.no_output_of_prior_pics_flag = r.read_bit()?; + rpm.long_term_reference_flag = r.read_bit()?; + } else { + rpm.adaptive_ref_pic_marking_mode_flag = r.read_bit()?; + + if rpm.adaptive_ref_pic_marking_mode_flag { + loop { + let mut marking = RefPicMarkingInner::default(); + + let mem_mgmt_ctrl_op = r.read_ue_max::(6)?; + marking.memory_management_control_operation = mem_mgmt_ctrl_op; + + if mem_mgmt_ctrl_op == 0 { + break; + } + + if mem_mgmt_ctrl_op == 1 || mem_mgmt_ctrl_op == 3 { + marking.difference_of_pic_nums_minus1 = r.read_ue()?; + } + + if mem_mgmt_ctrl_op == 2 { + marking.long_term_pic_num = r.read_ue()?; + } + + if mem_mgmt_ctrl_op == 3 || mem_mgmt_ctrl_op == 6 { + marking.long_term_frame_idx = r.read_ue()?; + } + + if mem_mgmt_ctrl_op == 4 { + marking.max_long_term_frame_idx = + MaxLongTermFrameIdx::from_value_plus1(r.read_ue()?); + } + + rpm.inner.push(marking); + } + } + } + header.dec_ref_pic_marking_bit_size = num_bits_left - r.num_bits_left(); + + Ok(()) + } + + pub fn parse_slice_header<'a>(&self, nalu: Nalu<'a>) -> Result, String> { + if !matches!( + nalu.header.type_, + NaluType::Slice + | NaluType::SliceDpa + | NaluType::SliceDpb + | NaluType::SliceDpc + | NaluType::SliceIdr + | NaluType::SliceExt + ) { + return Err(format!( + "Invalid NALU type: {:?} is not a slice NALU", + nalu.header.type_ + )); + } + + let data = nalu.as_ref(); + // Skip the header + let mut r = BitReader::new(&data[nalu.header.len()..], true); + + let mut header = SliceHeader { + first_mb_in_slice: r.read_ue()?, + ..Default::default() + }; + + let slice_type = r.read_ue_max::(9)? % 5; + header.slice_type = SliceType::try_from(slice_type)?; + + header.pic_parameter_set_id = r.read_ue()?; + + let pps = self + .get_pps(header.pic_parameter_set_id) + .ok_or::(format!( + "Could not get PPS for pic_parameter_set_id {}", + header.pic_parameter_set_id + ))?; + + let sps = &pps.sps; + + if sps.separate_colour_plane_flag { + header.colour_plane_id = r.read_bits(2)?; + } + + header.frame_num = r.read_bits(usize::from(sps.log2_max_frame_num_minus4) + 4)?; + + if !sps.frame_mbs_only_flag { + header.field_pic_flag = r.read_bit()?; + if header.field_pic_flag { + header.bottom_field_flag = r.read_bit()?; + } + } + + if header.field_pic_flag { + header.max_pic_num = 2 * sps.max_frame_num(); + } else { + header.max_pic_num = sps.max_frame_num(); + } + + if nalu.header.idr_pic_flag { + header.idr_pic_id = r.read_ue_max(0xffff)?; + } + + let num_bits_left = r.num_bits_left(); + if sps.pic_order_cnt_type == 0 { + header.pic_order_cnt_lsb = + r.read_bits(usize::from(sps.log2_max_pic_order_cnt_lsb_minus4) + 4)?; + + if pps.bottom_field_pic_order_in_frame_present_flag && !header.field_pic_flag { + header.delta_pic_order_cnt_bottom = r.read_se()?; + } + } + + if sps.pic_order_cnt_type == 1 && !sps.delta_pic_order_always_zero_flag { + header.delta_pic_order_cnt[0] = r.read_se()?; + if pps.bottom_field_pic_order_in_frame_present_flag && !header.field_pic_flag { + header.delta_pic_order_cnt[1] = r.read_se()?; + } + } + header.pic_order_cnt_bit_size = num_bits_left - r.num_bits_left(); + + if pps.redundant_pic_cnt_present_flag { + header.redundant_pic_cnt = r.read_ue_max(127)?; + } + + if header.slice_type.is_b() { + header.direct_spatial_mv_pred_flag = r.read_bit()?; + } + + if header.slice_type.is_p() || header.slice_type.is_sp() || header.slice_type.is_b() { + header.num_ref_idx_active_override_flag = r.read_bit()?; + if header.num_ref_idx_active_override_flag { + header.num_ref_idx_l0_active_minus1 = r.read_ue()?; + if header.slice_type.is_b() { + header.num_ref_idx_l1_active_minus1 = r.read_ue()?; + } + } else { + header.num_ref_idx_l0_active_minus1 = pps.num_ref_idx_l0_default_active_minus1; + if header.slice_type.is_b() { + header.num_ref_idx_l1_active_minus1 = pps.num_ref_idx_l1_default_active_minus1; + } + } + } + + if header.field_pic_flag { + if header.num_ref_idx_l0_active_minus1 > 31 || header.num_ref_idx_l1_active_minus1 > 31 + { + return Err("Broken Data".into()); + } + } else if header.num_ref_idx_l0_active_minus1 > 15 + || header.num_ref_idx_l1_active_minus1 > 15 + { + return Err("Broken Data".into()); + } + + if let NaluType::SliceExt = nalu.header.type_ { + return Err("Stream contain unsupported/unimplemented NALs".into()); + } + + Parser::parse_ref_pic_list_modifications(&mut r, &mut header)?; + + if (pps.weighted_pred_flag && (header.slice_type.is_p() || header.slice_type.is_sp())) + || (pps.weighted_bipred_idc == 1 && header.slice_type.is_b()) + { + Parser::parse_pred_weight_table(&mut r, sps, &mut header)?; + } + + if nalu.header.ref_idc != 0 { + Parser::parse_dec_ref_pic_marking(&mut r, &nalu, &mut header)?; + } + + if pps.entropy_coding_mode_flag && !header.slice_type.is_i() && !header.slice_type.is_si() { + header.cabac_init_idc = r.read_ue_max(2)?; + } + + header.slice_qp_delta = r.read_se_bounded(-87, 77)?; + + if header.slice_type.is_sp() || header.slice_type.is_si() { + if header.slice_type.is_sp() { + header.sp_for_switch_flag = r.read_bit()?; + } + + header.slice_qs_delta = r.read_se_bounded(-51, 51)?; + } + + if pps.deblocking_filter_control_present_flag { + header.disable_deblocking_filter_idc = r.read_ue_max(2)?; + + if header.disable_deblocking_filter_idc != 1 { + header.slice_alpha_c0_offset_div2 = r.read_se_bounded(-6, 6)?; + header.slice_beta_offset_div2 = r.read_se_bounded(-6, 6)?; + } + } + + if pps.num_slice_groups_minus1 > 0 { + return Err("Stream contain unsupported/unimplemented NALs".into()); + } + + let epb = r.num_epb(); + header.header_bit_size = (nalu.size - epb) * 8 - r.num_bits_left(); + + header.n_emulation_prevention_bytes = epb; + + Ok(Slice { header, nalu }) + } + + pub fn get_sps(&self, sps_id: u8) -> Option<&Rc> { + self.active_spses.get(&sps_id) + } + + pub fn get_pps(&self, pps_id: u8) -> Option<&Rc> { + self.active_ppses.get(&pps_id) + } +} + +#[derive(Debug)] +pub struct NaluHeader { + pub ref_idc: u8, + pub type_: NaluType, + pub idr_pic_flag: bool, +} + +impl Header for NaluHeader { + fn parse>(cursor: &mut Cursor) -> Result { + let mut byte_buf = [0u8; 1]; + cursor + .read_exact(&mut byte_buf) + .map_err(|_| String::from("Broken Data"))?; + let byte = byte_buf[0]; + let _ = cursor.seek(SeekFrom::Current(-1 * byte_buf.len() as i64)); + + let type_ = NaluType::try_from(byte & 0x1f)?; + + if let NaluType::SliceExt = type_ { + return Err("Stream contain unsupported/unimplemented NALs".into()); + } + + let ref_idc = (byte & 0x60) >> 5; + let idr_pic_flag = matches!(type_, NaluType::SliceIdr); + + Ok(NaluHeader { + ref_idc, + type_, + idr_pic_flag, + }) + } + + fn is_end(&self) -> bool { + matches!(self.type_, NaluType::SeqEnd | NaluType::StreamEnd) + } + + fn len(&self) -> usize { + 1 + } +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use crate::codec::h264::parser::Level; + use crate::codec::h264::parser::MaxLongTermFrameIdx; + use crate::codec::h264::parser::Nalu; + use crate::codec::h264::parser::NaluType; + use crate::codec::h264::parser::Parser; + + const STREAM_TEST_25_FPS: &[u8] = include_bytes!("test_data/test-25fps.h264"); + const STREAM_TEST_25_FPS_NUM_NALUS: usize = 759; + + const STREAM_TEST_25_FPS_SLICE_0: &[u8] = + include_bytes!("test_data/test-25fps-h264-slice-data-0.bin"); + const STREAM_TEST_25_FPS_SLICE_2: &[u8] = + include_bytes!("test_data/test-25fps-h264-slice-data-2.bin"); + const STREAM_TEST_25_FPS_SLICE_4: &[u8] = + include_bytes!("test_data/test-25fps-h264-slice-data-4.bin"); + + /// This test is adapted from chromium, available at media/video/h264_parser_unittest.cc + #[test] + fn parse_nalus_from_stream_file() { + let mut cursor = Cursor::new(STREAM_TEST_25_FPS); + let mut num_nalus = 0; + while Nalu::next(&mut cursor).is_ok() { + num_nalus += 1; + } + + assert_eq!(num_nalus, STREAM_TEST_25_FPS_NUM_NALUS) + } + + /// The results were manually extracted from the GStreamer parser using GDB + /// (gsth264parser.c) in order to compare both implementations using the + /// following pipeline: + /// gst-launch-1.0 filesrc location=test-25fps.h264 ! h264parse ! 'video/x-h264,stream-format=byte-stream' ! vah264dec ! fakevideosink + #[test] + fn parse_test25fps() { + let mut cursor = Cursor::new(STREAM_TEST_25_FPS); + let mut sps_ids = Vec::new(); + let mut pps_ids = Vec::new(); + let mut slices = Vec::new(); + + let mut parser = Parser::default(); + + while let Ok(nalu) = Nalu::next(&mut cursor) { + match nalu.header.type_ { + NaluType::Slice + | NaluType::SliceDpa + | NaluType::SliceDpb + | NaluType::SliceDpc + | NaluType::SliceIdr + | NaluType::SliceExt => { + let slice = parser.parse_slice_header(nalu).unwrap(); + slices.push(slice); + } + NaluType::Sps => { + let sps = parser.parse_sps(&nalu).unwrap(); + sps_ids.push(sps.seq_parameter_set_id); + } + NaluType::Pps => { + let pps = parser.parse_pps(&nalu).unwrap(); + pps_ids.push(pps.pic_parameter_set_id); + } + _ => { + continue; + } + } + } + + for sps_id in &sps_ids { + // four identical SPSes in this stream + let sps = parser.get_sps(*sps_id).unwrap(); + + assert_eq!(sps.seq_parameter_set_id, 0); + assert_eq!(sps.profile_idc, 77); + assert!(!sps.constraint_set0_flag); + assert!(sps.constraint_set1_flag); + assert!(!sps.constraint_set2_flag); + assert!(!sps.constraint_set3_flag); + assert!(!sps.constraint_set4_flag); + assert!(!sps.constraint_set5_flag); + assert_eq!(sps.level_idc, Level::L1_3); + assert_eq!(sps.chroma_format_idc, 1); + assert!(!sps.separate_colour_plane_flag); + assert_eq!(sps.bit_depth_luma_minus8, 0); + assert_eq!(sps.bit_depth_chroma_minus8, 0); + assert!(!sps.qpprime_y_zero_transform_bypass_flag); + assert!(!sps.seq_scaling_matrix_present_flag); + + for outer in &sps.scaling_lists_4x4 { + for inner in outer { + assert_eq!(*inner, 16); + } + } + + for outer in &sps.scaling_lists_8x8 { + for inner in outer { + assert_eq!(*inner, 16); + } + } + + assert_eq!(sps.log2_max_frame_num_minus4, 1); + assert_eq!(sps.pic_order_cnt_type, 0); + assert_eq!(sps.log2_max_pic_order_cnt_lsb_minus4, 3); + assert!(!sps.delta_pic_order_always_zero_flag); + assert_eq!(sps.offset_for_non_ref_pic, 0); + assert_eq!(sps.offset_for_top_to_bottom_field, 0); + assert_eq!(sps.num_ref_frames_in_pic_order_cnt_cycle, 0); + + for offset in sps.offset_for_ref_frame { + assert_eq!(offset, 0); + } + + assert_eq!(sps.max_num_ref_frames, 2); + assert!(!sps.gaps_in_frame_num_value_allowed_flag); + assert_eq!(sps.pic_width_in_mbs_minus1, 19); + assert_eq!(sps.pic_height_in_map_units_minus1, 14); + assert!(sps.frame_mbs_only_flag); + assert!(!sps.mb_adaptive_frame_field_flag); + assert!(!sps.direct_8x8_inference_flag); + assert!(!sps.frame_cropping_flag); + assert_eq!(sps.frame_crop_left_offset, 0); + assert_eq!(sps.frame_crop_right_offset, 0); + assert_eq!(sps.frame_crop_top_offset, 0); + assert_eq!(sps.frame_crop_bottom_offset, 0); + assert_eq!(sps.chroma_array_type(), 1); + assert_eq!(sps.max_frame_num(), 32); + assert_eq!(sps.width(), 320); + assert_eq!(sps.height(), 240); + } + + for pps_id in &pps_ids { + // four identical SPSes in this stream + let pps = parser.get_pps(*pps_id).unwrap(); + assert_eq!(pps.pic_parameter_set_id, 0); + assert_eq!(pps.seq_parameter_set_id, 0); + assert!(pps.bottom_field_pic_order_in_frame_present_flag); + assert_eq!(pps.num_slice_groups_minus1, 0); + assert_eq!(pps.num_ref_idx_l0_default_active_minus1, 0); + assert_eq!(pps.num_ref_idx_l1_default_active_minus1, 0); + assert!(!pps.weighted_pred_flag); + assert_eq!(pps.weighted_bipred_idc, 0); + assert_eq!(pps.pic_init_qp_minus26, 2); + assert_eq!(pps.pic_init_qs_minus26, 0); + assert_eq!(pps.chroma_qp_index_offset, 0); + assert!(!pps.deblocking_filter_control_present_flag); + assert!(!pps.constrained_intra_pred_flag); + assert!(!pps.redundant_pic_cnt_present_flag); + assert!(!pps.transform_8x8_mode_flag); + + for outer in &pps.scaling_lists_4x4 { + for inner in outer { + assert_eq!(*inner, 16); + } + } + + for outer in &pps.scaling_lists_8x8 { + for inner in outer { + assert_eq!(*inner, 16); + } + } + + assert_eq!(pps.second_chroma_qp_index_offset, 0); + assert!(!pps.pic_scaling_matrix_present_flag); + } + + // test an I slice + let hdr = &slices[0].header; + let nalu = &slices[0].nalu; + assert_eq!(nalu.as_ref(), STREAM_TEST_25_FPS_SLICE_0); + + assert_eq!(hdr.first_mb_in_slice, 0); + assert!(hdr.slice_type.is_i()); + assert_eq!(hdr.colour_plane_id, 0); + assert_eq!(hdr.frame_num, 0); + assert!(!hdr.field_pic_flag); + assert!(!hdr.bottom_field_flag); + assert_eq!(hdr.idr_pic_id, 0); + assert_eq!(hdr.pic_order_cnt_lsb, 0); + assert_eq!(hdr.delta_pic_order_cnt_bottom, 0); + assert_eq!(hdr.delta_pic_order_cnt[0], 0); + assert_eq!(hdr.delta_pic_order_cnt[1], 0); + assert_eq!(hdr.redundant_pic_cnt, 0); + assert!(!hdr.direct_spatial_mv_pred_flag); + assert_eq!(hdr.num_ref_idx_l0_active_minus1, 0); + assert_eq!(hdr.num_ref_idx_l1_active_minus1, 0); + assert!(!hdr.ref_pic_list_modification_flag_l0); + + assert_eq!(hdr.ref_pic_list_modification_l0.len(), 0); + + for rplm in &hdr.ref_pic_list_modification_l0 { + assert_eq!(rplm.modification_of_pic_nums_idc, 0); + assert_eq!(rplm.abs_diff_pic_num_minus1, 0); + assert_eq!(rplm.long_term_pic_num, 0); + assert_eq!(rplm.abs_diff_view_idx_minus1, 0); + } + + assert!(!hdr.ref_pic_list_modification_flag_l1); + assert_eq!(hdr.ref_pic_list_modification_l1.len(), 0); + + for rplm in &hdr.ref_pic_list_modification_l1 { + assert_eq!(rplm.modification_of_pic_nums_idc, 0); + assert_eq!(rplm.abs_diff_pic_num_minus1, 0); + assert_eq!(rplm.long_term_pic_num, 0); + assert_eq!(rplm.abs_diff_view_idx_minus1, 0); + } + + // Safe because this type does not have any references + assert_eq!(hdr.pred_weight_table, Default::default()); + + assert_eq!(hdr.dec_ref_pic_marking, Default::default()); + + assert_eq!(hdr.cabac_init_idc, 0); + assert_eq!(hdr.slice_qp_delta, 12); + assert_eq!(hdr.slice_qs_delta, 0); + assert_eq!(hdr.disable_deblocking_filter_idc, 0); + assert_eq!(hdr.slice_alpha_c0_offset_div2, 0); + assert_eq!(hdr.slice_beta_offset_div2, 0); + assert_eq!(hdr.max_pic_num, 32); + assert_eq!(hdr.header_bit_size, 38); + assert!(!hdr.num_ref_idx_active_override_flag); + + // test a P slice + let hdr = &slices[2].header; + let nalu = &slices[2].nalu; + assert_eq!(nalu.as_ref(), STREAM_TEST_25_FPS_SLICE_2); + + assert_eq!(hdr.first_mb_in_slice, 0); + assert!(hdr.slice_type.is_p()); + assert_eq!(hdr.colour_plane_id, 0); + assert_eq!(hdr.frame_num, 1); + assert!(!hdr.field_pic_flag); + assert!(!hdr.bottom_field_flag); + assert_eq!(hdr.idr_pic_id, 0); + assert_eq!(hdr.pic_order_cnt_lsb, 4); + assert_eq!(hdr.delta_pic_order_cnt_bottom, 0); + assert_eq!(hdr.delta_pic_order_cnt[0], 0); + assert_eq!(hdr.delta_pic_order_cnt[1], 0); + assert_eq!(hdr.redundant_pic_cnt, 0); + assert!(!hdr.direct_spatial_mv_pred_flag); + assert_eq!(hdr.num_ref_idx_l0_active_minus1, 0); + assert_eq!(hdr.num_ref_idx_l1_active_minus1, 0); + assert!(!hdr.ref_pic_list_modification_flag_l0); + + assert_eq!(hdr.ref_pic_list_modification_l0.len(), 0); + + for rplm in &hdr.ref_pic_list_modification_l0 { + assert_eq!(rplm.modification_of_pic_nums_idc, 0); + assert_eq!(rplm.abs_diff_pic_num_minus1, 0); + assert_eq!(rplm.long_term_pic_num, 0); + assert_eq!(rplm.abs_diff_view_idx_minus1, 0); + } + + assert!(!hdr.ref_pic_list_modification_flag_l1); + assert_eq!(hdr.ref_pic_list_modification_l1.len(), 0); + + for rplm in &hdr.ref_pic_list_modification_l1 { + assert_eq!(rplm.modification_of_pic_nums_idc, 0); + assert_eq!(rplm.abs_diff_pic_num_minus1, 0); + assert_eq!(rplm.long_term_pic_num, 0); + assert_eq!(rplm.abs_diff_view_idx_minus1, 0); + } + + // Safe because this type does not have any references + assert_eq!(hdr.pred_weight_table, Default::default()); + + assert_eq!(hdr.dec_ref_pic_marking, Default::default()); + + assert_eq!(hdr.cabac_init_idc, 0); + assert_eq!(hdr.slice_qp_delta, 0); + assert_eq!(hdr.slice_qs_delta, 0); + assert_eq!(hdr.disable_deblocking_filter_idc, 0); + assert_eq!(hdr.slice_alpha_c0_offset_div2, 0); + assert_eq!(hdr.slice_beta_offset_div2, 0); + assert_eq!(hdr.max_pic_num, 32); + assert_eq!(hdr.header_bit_size, 28); + assert!(!hdr.num_ref_idx_active_override_flag); + + // test a B slice + let hdr = &slices[4].header; + let nalu = &slices[4].nalu; + assert_eq!(nalu.as_ref(), STREAM_TEST_25_FPS_SLICE_4); + + assert_eq!(hdr.first_mb_in_slice, 0); + assert!(hdr.slice_type.is_b()); + assert_eq!(hdr.colour_plane_id, 0); + assert_eq!(hdr.frame_num, 2); + assert!(!hdr.field_pic_flag); + assert!(!hdr.bottom_field_flag); + assert_eq!(hdr.idr_pic_id, 0); + assert_eq!(hdr.pic_order_cnt_lsb, 2); + assert_eq!(hdr.delta_pic_order_cnt_bottom, 0); + assert_eq!(hdr.delta_pic_order_cnt[0], 0); + assert_eq!(hdr.delta_pic_order_cnt[1], 0); + assert_eq!(hdr.redundant_pic_cnt, 0); + assert!(hdr.direct_spatial_mv_pred_flag); + assert_eq!(hdr.num_ref_idx_l0_active_minus1, 0); + assert_eq!(hdr.num_ref_idx_l1_active_minus1, 0); + assert!(!hdr.ref_pic_list_modification_flag_l0); + + assert_eq!(hdr.ref_pic_list_modification_l0.len(), 0); + + for rplm in &hdr.ref_pic_list_modification_l0 { + assert_eq!(rplm.modification_of_pic_nums_idc, 0); + assert_eq!(rplm.abs_diff_pic_num_minus1, 0); + assert_eq!(rplm.long_term_pic_num, 0); + assert_eq!(rplm.abs_diff_view_idx_minus1, 0); + } + + assert!(!hdr.ref_pic_list_modification_flag_l1); + assert_eq!(hdr.ref_pic_list_modification_l1.len(), 0); + + for rplm in &hdr.ref_pic_list_modification_l1 { + assert_eq!(rplm.modification_of_pic_nums_idc, 0); + assert_eq!(rplm.abs_diff_pic_num_minus1, 0); + assert_eq!(rplm.long_term_pic_num, 0); + assert_eq!(rplm.abs_diff_view_idx_minus1, 0); + } + + // Safe because this type does not have any references + assert_eq!(hdr.pred_weight_table, Default::default()); + + assert_eq!(hdr.dec_ref_pic_marking, Default::default()); + + assert_eq!(hdr.cabac_init_idc, 0); + assert_eq!(hdr.slice_qp_delta, 16); + assert_eq!(hdr.slice_qs_delta, 0); + assert_eq!(hdr.disable_deblocking_filter_idc, 0); + assert_eq!(hdr.slice_alpha_c0_offset_div2, 0); + assert_eq!(hdr.slice_beta_offset_div2, 0); + assert_eq!(hdr.max_pic_num, 32); + assert_eq!(hdr.header_bit_size, 41); + assert!(!hdr.num_ref_idx_active_override_flag); + } + + #[test] + fn invalid_sps_crop_width() { + // This SPS contains invalid frame_crop_*_offset settings. This led to + // unconditional panic in the parser in the past. This test make sure a + // panic is avoided. + let invalid_sps = vec![ + 0x00, 0x00, 0x01, 0x07, 0x00, 0x00, 0x0a, 0xfb, 0xb0, 0x32, 0xc0, 0xca, 0x80, + ]; + + let mut cursor = Cursor::new(invalid_sps.as_ref()); + let mut parser = Parser::default(); + + while let Ok(nalu) = Nalu::next(&mut cursor) { + assert_eq!(nalu.header.type_, NaluType::Sps); + parser.parse_sps(&nalu).unwrap_err(); + } + } + + #[test] + fn max_long_term_frame_idx() { + assert_eq!( + MaxLongTermFrameIdx::from_value_plus1(0), + MaxLongTermFrameIdx::NoLongTermFrameIndices + ); + assert_eq!( + MaxLongTermFrameIdx::NoLongTermFrameIndices.to_value_plus1(), + 0 + ); + + assert_eq!( + MaxLongTermFrameIdx::from_value_plus1(1), + MaxLongTermFrameIdx::Idx(0) + ); + assert_eq!(MaxLongTermFrameIdx::Idx(0).to_value_plus1(), 1); + + assert_eq!( + MaxLongTermFrameIdx::from_value_plus1(25), + MaxLongTermFrameIdx::Idx(24) + ); + assert_eq!(MaxLongTermFrameIdx::Idx(24).to_value_plus1(), 25); + + // Check PartialOrd implementation. + assert!(MaxLongTermFrameIdx::NoLongTermFrameIndices < 0); + assert_ne!(MaxLongTermFrameIdx::NoLongTermFrameIndices, 0); + assert_eq!(MaxLongTermFrameIdx::Idx(0), 0); + assert!(MaxLongTermFrameIdx::Idx(0) < 1); + assert_eq!(MaxLongTermFrameIdx::Idx(24), 24); + assert!(MaxLongTermFrameIdx::Idx(24) < 25); + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/picture.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/picture.rs new file mode 100644 index 00000000..c8819d58 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/picture.rs @@ -0,0 +1,443 @@ +// Copyright 2022 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::cell::RefCell; +use std::ops::Deref; +use std::rc::Rc; +use std::rc::Weak; + +use log::debug; + +use crate::codec::h264::parser::MaxLongTermFrameIdx; +use crate::codec::h264::parser::RefPicMarking; +use crate::codec::h264::parser::Slice; +use crate::codec::h264::parser::SliceType; +use crate::codec::h264::parser::Sps; +use crate::Resolution; + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum Field { + #[default] + Frame, + Top, + Bottom, +} + +impl Field { + /// Returns the field of opposite parity. + pub fn opposite(&self) -> Self { + match *self { + Field::Frame => Field::Frame, + Field::Top => Field::Bottom, + Field::Bottom => Field::Top, + } + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum Reference { + #[default] + None, + ShortTerm, + LongTerm, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum IsIdr { + #[default] + No, + Yes { + idr_pic_id: u16, + }, +} + +/// The rank of a field, i.e. whether it is the first or second one to be parsed from the stream. +/// This is unrelated to the `Field` type, as the first field can be either `Top` or `Bottom`. +#[derive(Default, Debug)] +pub enum FieldRank { + /// Frame has a single field. + #[default] + Single, + /// Frame is interlaced, and this is the first field (with a reference to the second one). + First(Weak>), + /// Frame is interlaced, and this is the second field (with a reference to the first one). + Second(Rc>), +} + +#[derive(Default)] +pub struct PictureData { + pub pic_order_cnt_type: u8, + pub top_field_order_cnt: i32, + pub bottom_field_order_cnt: i32, + pub pic_order_cnt: i32, + pub pic_order_cnt_msb: i32, + pub pic_order_cnt_lsb: i32, + pub delta_pic_order_cnt_bottom: i32, + pub delta_pic_order_cnt0: i32, + pub delta_pic_order_cnt1: i32, + + pub pic_num: i32, + pub long_term_pic_num: u32, + pub frame_num: u32, + pub frame_num_offset: u32, + pub frame_num_wrap: i32, + pub long_term_frame_idx: u32, + + pub coded_resolution: Resolution, + pub display_resolution: Resolution, + + pub type_: SliceType, + pub nal_ref_idc: u8, + pub is_idr: IsIdr, + reference: Reference, + pub ref_pic_list_modification_flag_l0: i32, + pub abs_diff_pic_num_minus1: i32, + + // Does memory management op 5 needs to be executed after this + // picture has finished decoding? + pub has_mmco_5: bool, + + // Created by the decoding process for gaps in frame_num. + // Not for decode or output. + pub nonexisting: bool, + + pub field: Field, + + // Values from slice_hdr to be used during reference marking and + // memory management after finishing this picture. + pub ref_pic_marking: RefPicMarking, + + field_rank: FieldRank, + + pub timestamp: u64, +} + +/// A `PictureData` within a `Rc` which field rank is guaranteed to be correct. +/// +/// The field rank of `PictureData` is only final after both fields have been constructed - namely, +/// the first field can only point to the second one after the latter is available as a Rc. Methods +/// [`PictureData::into_rc`] and [`PictureData::split_frame`] take care of this, and is this only +/// producer of this type, ensuring all instances are correct. +#[derive(Default, Debug, Clone)] +pub struct RcPictureData { + pic: Rc>, +} + +impl Deref for RcPictureData { + type Target = Rc>; + + fn deref(&self) -> &Self::Target { + &self.pic + } +} + +impl PictureData { + pub fn new_non_existing(frame_num: u32, timestamp: u64) -> Self { + PictureData { + frame_num, + nonexisting: true, + nal_ref_idc: 1, + field: Field::Frame, + pic_num: frame_num as i32, + reference: Reference::ShortTerm, + timestamp, + ..Default::default() + } + } + + /// Create a new picture from a `slice`, `sps`, and `timestamp`. + /// + /// `first_field` is set if this picture is the second field of a frame. + pub fn new_from_slice( + slice: &Slice, + sps: &Sps, + timestamp: u64, + first_field: Option<&RcPictureData>, + ) -> Self { + let hdr = &slice.header; + let nalu_hdr = &slice.nalu.header; + + let is_idr = if nalu_hdr.idr_pic_flag { + IsIdr::Yes { + idr_pic_id: hdr.idr_pic_id, + } + } else { + IsIdr::No + }; + + let field = if hdr.field_pic_flag { + if hdr.bottom_field_flag { + Field::Bottom + } else { + Field::Top + } + } else { + Field::Frame + }; + + let reference = if nalu_hdr.ref_idc != 0 { + Reference::ShortTerm + } else { + Reference::None + }; + + let pic_num = if !hdr.field_pic_flag { + hdr.frame_num + } else { + 2 * hdr.frame_num + 1 + }; + + let ( + pic_order_cnt_lsb, + delta_pic_order_cnt_bottom, + delta_pic_order_cnt0, + delta_pic_order_cnt1, + ) = match sps.pic_order_cnt_type { + 0 => ( + hdr.pic_order_cnt_lsb, + hdr.delta_pic_order_cnt_bottom, + Default::default(), + Default::default(), + ), + 1 => ( + Default::default(), + Default::default(), + hdr.delta_pic_order_cnt[0], + hdr.delta_pic_order_cnt[1], + ), + _ => ( + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ), + }; + + let coded_resolution = Resolution::from((sps.width(), sps.height())); + + let visible_rect = sps.visible_rectangle(); + + // punktfunk deviation (PROVENANCE.md #6): `Sps::visible_rectangle()` returns + // the crop offset in `min` and the visible SIZE in `max` (not a corner); + // upstream's `max - min` double-counts the left/top crop and panics on a u32 + // underflow for large-but-parser-valid left/top offsets. + let display_resolution = Resolution { + width: visible_rect.max.x, + height: visible_rect.max.y, + }; + + let mut pic = PictureData { + pic_order_cnt_type: sps.pic_order_cnt_type, + pic_order_cnt_lsb: i32::from(pic_order_cnt_lsb), + delta_pic_order_cnt_bottom, + delta_pic_order_cnt0, + delta_pic_order_cnt1, + pic_num: i32::from(pic_num), + frame_num: u32::from(hdr.frame_num), + nal_ref_idc: nalu_hdr.ref_idc, + is_idr, + reference, + field, + ref_pic_marking: hdr.dec_ref_pic_marking.clone(), + coded_resolution, + display_resolution, + timestamp, + ..Default::default() + }; + + if let Some(first_field) = first_field { + pic.set_first_field_to(first_field); + } + + pic + } + + /// Whether the current picture is a reference, either ShortTerm or LongTerm. + pub fn is_ref(&self) -> bool { + !matches!(self.reference, Reference::None) + } + + /// Whether this picture is a second field. + pub fn is_second_field(&self) -> bool { + matches!(self.field_rank, FieldRank::Second(..)) + } + + /// Returns the field rank of this picture, including a reference to its other field. + pub fn field_rank(&self) -> &FieldRank { + &self.field_rank + } + + /// Returns a reference to the picture's Reference + pub fn reference(&self) -> &Reference { + &self.reference + } + + /// Mark the picture as a reference picture. + pub fn set_reference(&mut self, reference: Reference, apply_to_other_field: bool) { + log::debug!("Set reference of {:#?} to {:?}", self, reference); + self.reference = reference; + + if apply_to_other_field { + if let Some(other_field) = self.other_field() { + log::debug!( + "other_field: Set reference of {:#?} to {:?}", + &other_field.borrow(), + reference + ); + other_field.borrow_mut().reference = reference; + } + } + } + + /// Get a reference to the picture's other field, if there is any + /// and its reference is still valid. + pub fn other_field(&self) -> Option>> { + match &self.field_rank { + FieldRank::Single => None, + FieldRank::First(other_field) => other_field.upgrade(), + FieldRank::Second(other_field) => Some(other_field.clone()), + } + } + + /// Set this picture's second field. + fn set_second_field_to(&mut self, other_field: &Rc>) { + self.field_rank = FieldRank::First(Rc::downgrade(other_field)); + } + + /// Whether the current picture is the second field of a complementary ref pair. + pub fn is_second_field_of_complementary_ref_pair(&self) -> bool { + self.is_ref() + && matches!(self.field_rank(), FieldRank::Second(first_field) if first_field.borrow().is_ref()) + } + + /// Set this picture's first field. + fn set_first_field_to(&mut self, other_field: &Rc>) { + self.field_rank = FieldRank::Second(other_field.clone()); + } + + pub fn pic_num_f(&self, max_pic_num: i32) -> i32 { + if !matches!(self.reference(), Reference::LongTerm) { + self.pic_num + } else { + max_pic_num + } + } + + pub fn long_term_pic_num_f(&self, max_long_term_frame_idx: MaxLongTermFrameIdx) -> u32 { + if matches!(self.reference(), Reference::LongTerm) { + self.long_term_pic_num + } else { + 2 * max_long_term_frame_idx.to_value_plus1() + } + } + + /// Consume this picture and return a Rc'd version. + /// + /// If the picture was a second field, adjust the field of the first field to point to this + /// one. + pub fn into_rc(self) -> RcPictureData { + let self_rc = Rc::new(RefCell::new(self)); + + if let FieldRank::Second(first_field) = self_rc.borrow().field_rank() { + first_field.borrow_mut().set_second_field_to(&self_rc); + } + + RcPictureData { pic: self_rc } + } + + /// Split a frame into two complementary fields that reference one another. + pub fn split_frame(mut self) -> (RcPictureData, RcPictureData) { + assert!(matches!(self.field, Field::Frame)); + assert!(matches!(self.field_rank, FieldRank::Single)); + + debug!( + "Splitting picture (frame_num, POC) ({:?}, {:?})", + self.frame_num, self.pic_order_cnt + ); + + let second_pic_order_cnt = if self.top_field_order_cnt < self.bottom_field_order_cnt { + self.field = Field::Top; + self.pic_order_cnt = self.top_field_order_cnt; + + self.bottom_field_order_cnt + } else { + self.field = Field::Bottom; + self.pic_order_cnt = self.bottom_field_order_cnt; + + self.top_field_order_cnt + }; + + let second_field = PictureData { + top_field_order_cnt: self.top_field_order_cnt, + bottom_field_order_cnt: self.bottom_field_order_cnt, + frame_num: self.frame_num, + reference: self.reference, + nonexisting: self.nonexisting, + pic_order_cnt: second_pic_order_cnt, + field: self.field.opposite(), + ..Default::default() + }; + + debug!( + "Split into picture (frame_num, POC) ({:?}, {:?}), field: {:?}", + self.frame_num, self.pic_order_cnt, self.field + ); + debug!( + "Split into picture (frame_num, POC) ({:?}, {:?}), field {:?}", + second_field.frame_num, second_field.pic_order_cnt, second_field.field + ); + + let first_field = Rc::new(RefCell::new(self)); + let second_field = Rc::new(RefCell::new(second_field)); + + first_field.borrow_mut().set_second_field_to(&second_field); + second_field.borrow_mut().set_first_field_to(&first_field); + + ( + RcPictureData { pic: first_field }, + RcPictureData { pic: second_field }, + ) + } +} + +impl std::fmt::Debug for PictureData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PictureData") + .field("pic_order_cnt_type", &self.pic_order_cnt_type) + .field("top_field_order_cnt", &self.top_field_order_cnt) + .field("bottom_field_order_cnt", &self.bottom_field_order_cnt) + .field("pic_order_cnt", &self.pic_order_cnt) + .field("pic_order_cnt_msb", &self.pic_order_cnt_msb) + .field("pic_order_cnt_lsb", &self.pic_order_cnt_lsb) + .field( + "delta_pic_order_cnt_bottom", + &self.delta_pic_order_cnt_bottom, + ) + .field("delta_pic_order_cnt0", &self.delta_pic_order_cnt0) + .field("delta_pic_order_cnt1", &self.delta_pic_order_cnt1) + .field("pic_num", &self.pic_num) + .field("long_term_pic_num", &self.long_term_pic_num) + .field("frame_num", &self.frame_num) + .field("frame_num_offset", &self.frame_num_offset) + .field("frame_num_wrap", &self.frame_num_wrap) + .field("long_term_frame_idx", &self.long_term_frame_idx) + .field("coded_resolution", &self.coded_resolution) + .field("display_resolution", &self.display_resolution) + .field("type_", &self.type_) + .field("nal_ref_idc", &self.nal_ref_idc) + .field("is_idr", &self.is_idr) + .field("reference", &self.reference) + .field( + "ref_pic_list_modification_flag_l0", + &self.ref_pic_list_modification_flag_l0, + ) + .field("abs_diff_pic_num_minus1", &self.abs_diff_pic_num_minus1) + .field("has_mmco_5", &self.has_mmco_5) + .field("nonexisting", &self.nonexisting) + .field("field", &self.field) + .field("ref_pic_marking", &self.ref_pic_marking) + .field("field_rank", &self.field_rank) + .finish() + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/synthesizer.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/synthesizer.rs new file mode 100644 index 00000000..f983bb9e --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/synthesizer.rs @@ -0,0 +1,593 @@ +// Copyright 2024 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +use std::fmt; +use std::io::Write; + +use crate::codec::h264::nalu_writer::NaluWriter; +use crate::codec::h264::nalu_writer::NaluWriterError; +use crate::codec::h264::parser::HrdParams; +use crate::codec::h264::parser::NaluType; +use crate::codec::h264::parser::Pps; +use crate::codec::h264::parser::Sps; +use crate::codec::h264::parser::DEFAULT_4X4_INTER; +use crate::codec::h264::parser::DEFAULT_4X4_INTRA; +use crate::codec::h264::parser::DEFAULT_8X8_INTER; +use crate::codec::h264::parser::DEFAULT_8X8_INTRA; + +mod private { + pub trait NaluStruct {} +} + +impl private::NaluStruct for Sps {} + +impl private::NaluStruct for Pps {} + +#[derive(Debug)] +pub enum SynthesizerError { + Unsupported, + NaluWriter(NaluWriterError), +} + +impl fmt::Display for SynthesizerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SynthesizerError::Unsupported => write!(f, "tried to synthesize unsupported settings"), + SynthesizerError::NaluWriter(x) => write!(f, "{}", x.to_string()), + } + } +} + +impl From for SynthesizerError { + fn from(err: NaluWriterError) -> Self { + SynthesizerError::NaluWriter(err) + } +} + +pub type SynthesizerResult = Result; + +/// A helper to output typed NALUs to [`std::io::Write`] using [`NaluWriter`]. +pub struct Synthesizer<'n, N: private::NaluStruct, W: Write> { + writer: NaluWriter, + nalu: &'n N, +} + +/// Extended Sample Aspect Ratio - H.264 Table E-1 +const EXTENDED_SAR: u8 = 255; + +impl Synthesizer<'_, N, W> { + fn u>(&mut self, bits: usize, value: T) -> SynthesizerResult<()> { + self.writer.write_u(bits, value)?; + Ok(()) + } + + fn f>(&mut self, bits: usize, value: T) -> SynthesizerResult<()> { + self.writer.write_f(bits, value)?; + Ok(()) + } + + fn ue>(&mut self, value: T) -> SynthesizerResult<()> { + self.writer.write_ue(value)?; + Ok(()) + } + + fn se>(&mut self, value: T) -> SynthesizerResult<()> { + self.writer.write_se(value)?; + Ok(()) + } + + fn scaling_list(&mut self, list: &[u8], default: &[u8]) -> SynthesizerResult<()> { + // H.264 7.3.2.1.1.1 + if list == default { + self.se(-8)?; + return Ok(()); + } + + // The number of list values we want to encode. + let mut run = list.len(); + + // Check how many values at the end of the matrix are the same, + // so we can save on encoding those. + for j in (1..list.len()).rev() { + if list[j - 1] != list[j] { + break; + } + run -= 1; + } + + // Encode deltas. + let mut last_scale = 8; + for scale in &list[0..run] { + let delta_scale = *scale as i32 - last_scale; + self.se(delta_scale)?; + last_scale = *scale as i32; + } + + // Didn't encode all values, encode -|last_scale| to set decoder's + // |next_scale| (H.264 7.3.2.1.1.1) to zero, i.e. decoder should repeat + // last values in matrix. + if run < list.len() { + self.se(-last_scale)?; + } + + Ok(()) + } + + fn default_scaling_list(i: usize) -> &'static [u8] { + // H.264 Table 7-2 + match i { + 0 => &DEFAULT_4X4_INTRA[..], + 1 => &DEFAULT_4X4_INTRA[..], + 2 => &DEFAULT_4X4_INTRA[..], + 3 => &DEFAULT_4X4_INTER[..], + 4 => &DEFAULT_4X4_INTER[..], + 5 => &DEFAULT_4X4_INTER[..], + 6 => &DEFAULT_8X8_INTRA[..], + 7 => &DEFAULT_8X8_INTER[..], + 8 => &DEFAULT_8X8_INTRA[..], + 9 => &DEFAULT_8X8_INTER[..], + 10 => &DEFAULT_8X8_INTRA[..], + 11 => &DEFAULT_8X8_INTER[..], + _ => unreachable!(), + } + } + + fn rbsp_trailing_bits(&mut self) -> SynthesizerResult<()> { + self.f(1, 1u32)?; + + while !self.writer.aligned() { + self.f(1, 0u32)?; + } + + Ok(()) + } +} + +impl<'n, W: Write> Synthesizer<'n, Sps, W> { + pub fn synthesize( + ref_idc: u8, + sps: &'n Sps, + writer: W, + ep_enabled: bool, + ) -> SynthesizerResult<()> { + let mut s = Self { + writer: NaluWriter::::new(writer, ep_enabled), + nalu: sps, + }; + + s.writer.write_header(ref_idc, NaluType::Sps as u8)?; + s.seq_parameter_set_data()?; + s.rbsp_trailing_bits() + } + + fn hrd_parameters(&mut self, hrd_params: &HrdParams) -> SynthesizerResult<()> { + self.ue(hrd_params.cpb_cnt_minus1)?; + self.u(4, hrd_params.bit_rate_scale)?; + self.u(4, hrd_params.cpb_size_scale)?; + + for i in 0..=(hrd_params.cpb_cnt_minus1 as usize) { + self.ue(hrd_params.bit_rate_value_minus1[i])?; + self.ue(hrd_params.cpb_size_value_minus1[i])?; + self.u(1, hrd_params.cbr_flag[i])?; + } + + self.u(5, hrd_params.initial_cpb_removal_delay_length_minus1)?; + self.u(5, hrd_params.cpb_removal_delay_length_minus1)?; + self.u(5, hrd_params.dpb_output_delay_length_minus1)?; + self.u(5, hrd_params.time_offset_length)?; + + Ok(()) + } + + fn vui_parameters(&mut self) -> SynthesizerResult<()> { + // H.264 E.1.1 + let vui_params = &self.nalu.vui_parameters; + + self.u(1, vui_params.aspect_ratio_info_present_flag)?; + if vui_params.aspect_ratio_info_present_flag { + self.u(8, vui_params.aspect_ratio_idc)?; + if vui_params.aspect_ratio_idc == EXTENDED_SAR { + self.u(16, vui_params.sar_width)?; + self.u(16, vui_params.sar_height)?; + } + } + + self.u(1, vui_params.overscan_info_present_flag)?; + if vui_params.overscan_info_present_flag { + self.u(1, vui_params.overscan_appropriate_flag)?; + } + + self.u(1, vui_params.video_signal_type_present_flag)?; + if vui_params.video_signal_type_present_flag { + self.u(3, vui_params.video_format)?; + self.u(1, vui_params.video_full_range_flag)?; + + self.u(1, vui_params.colour_description_present_flag)?; + if vui_params.colour_description_present_flag { + self.u(8, vui_params.colour_primaries)?; + self.u(8, vui_params.transfer_characteristics)?; + self.u(8, vui_params.matrix_coefficients)?; + } + } + + self.u(1, vui_params.chroma_loc_info_present_flag)?; + if vui_params.chroma_loc_info_present_flag { + self.ue(vui_params.chroma_sample_loc_type_top_field)?; + self.ue(self.nalu.vui_parameters.chroma_sample_loc_type_bottom_field)?; + } + + self.u(1, vui_params.timing_info_present_flag)?; + if vui_params.timing_info_present_flag { + self.u(32, vui_params.num_units_in_tick)?; + self.u(32, vui_params.time_scale)?; + self.u(1, vui_params.fixed_frame_rate_flag)?; + } + + self.u(1, vui_params.nal_hrd_parameters_present_flag)?; + if vui_params.nal_hrd_parameters_present_flag { + self.hrd_parameters(&vui_params.nal_hrd_parameters)?; + } + self.u(1, vui_params.vcl_hrd_parameters_present_flag)?; + if vui_params.vcl_hrd_parameters_present_flag { + self.hrd_parameters(&vui_params.vcl_hrd_parameters)?; + } + + if vui_params.nal_hrd_parameters_present_flag || vui_params.vcl_hrd_parameters_present_flag + { + self.u(1, vui_params.low_delay_hrd_flag)?; + } + + self.u(1, vui_params.pic_struct_present_flag)?; + + self.u(1, vui_params.bitstream_restriction_flag)?; + if vui_params.bitstream_restriction_flag { + self.u(1, vui_params.motion_vectors_over_pic_boundaries_flag)?; + self.ue(vui_params.max_bytes_per_pic_denom)?; + self.ue(vui_params.max_bits_per_mb_denom)?; + self.ue(vui_params.log2_max_mv_length_horizontal)?; + self.ue(vui_params.log2_max_mv_length_vertical)?; + self.ue(vui_params.max_num_reorder_frames)?; + self.ue(vui_params.max_dec_frame_buffering)?; + } + + Ok(()) + } + + fn seq_parameter_set_data(&mut self) -> SynthesizerResult<()> { + // H.264 7.3.2.1.1 + self.u(8, self.nalu.profile_idc)?; + self.u(1, self.nalu.constraint_set0_flag)?; + self.u(1, self.nalu.constraint_set1_flag)?; + self.u(1, self.nalu.constraint_set2_flag)?; + self.u(1, self.nalu.constraint_set3_flag)?; + self.u(1, self.nalu.constraint_set4_flag)?; + self.u(1, self.nalu.constraint_set5_flag)?; + self.u(2, /* reserved_zero_2bits */ 0u32)?; + self.u(8, self.nalu.level_idc as u32)?; + self.ue(self.nalu.seq_parameter_set_id)?; + + if self.nalu.profile_idc == 100 + || self.nalu.profile_idc == 110 + || self.nalu.profile_idc == 122 + || self.nalu.profile_idc == 244 + || self.nalu.profile_idc == 44 + || self.nalu.profile_idc == 83 + || self.nalu.profile_idc == 86 + || self.nalu.profile_idc == 118 + || self.nalu.profile_idc == 128 + || self.nalu.profile_idc == 138 + || self.nalu.profile_idc == 139 + || self.nalu.profile_idc == 134 + || self.nalu.profile_idc == 135 + { + self.ue(self.nalu.chroma_format_idc)?; + + if self.nalu.chroma_format_idc == 3 { + self.u(1, self.nalu.separate_colour_plane_flag)?; + } + + self.ue(self.nalu.bit_depth_luma_minus8)?; + self.ue(self.nalu.bit_depth_chroma_minus8)?; + self.u(1, self.nalu.qpprime_y_zero_transform_bypass_flag)?; + self.u(1, self.nalu.seq_scaling_matrix_present_flag)?; + + if self.nalu.seq_scaling_matrix_present_flag { + let scaling_list_count = if self.nalu.chroma_format_idc != 3 { + 8 + } else { + 12 + }; + + for i in 0..scaling_list_count { + // Assume if scaling lists are zeroed that they are not present. + if i < 6 { + if self.nalu.scaling_lists_4x4[i] == [0; 16] { + self.u(1, /* seq_scaling_list_present_flag */ false)?; + } else { + self.u(1, /* seq_scaling_list_present_flag */ true)?; + self.scaling_list( + &self.nalu.scaling_lists_4x4[i], + Self::default_scaling_list(i), + )?; + } + } else if self.nalu.scaling_lists_8x8[i - 6] == [0; 64] { + self.u(1, /* seq_scaling_list_present_flag */ false)?; + } else { + self.u(1, /* seq_scaling_list_present_flag */ true)?; + self.scaling_list( + &self.nalu.scaling_lists_8x8[i - 6], + Self::default_scaling_list(i), + )?; + } + } + } + } + + self.ue(self.nalu.log2_max_frame_num_minus4)?; + self.ue(self.nalu.pic_order_cnt_type)?; + + if self.nalu.pic_order_cnt_type == 0 { + self.ue(self.nalu.log2_max_pic_order_cnt_lsb_minus4)?; + } else if self.nalu.pic_order_cnt_type == 1 { + self.u(1, self.nalu.delta_pic_order_always_zero_flag)?; + self.se(self.nalu.offset_for_non_ref_pic)?; + self.se(self.nalu.offset_for_top_to_bottom_field)?; + self.ue(self.nalu.num_ref_frames_in_pic_order_cnt_cycle)?; + + for offset_for_ref_frame in &self.nalu.offset_for_ref_frame { + self.se(*offset_for_ref_frame)?; + } + } + + self.ue(self.nalu.max_num_ref_frames)?; + self.u(1, self.nalu.gaps_in_frame_num_value_allowed_flag)?; + self.ue(self.nalu.pic_width_in_mbs_minus1)?; + self.ue(self.nalu.pic_height_in_map_units_minus1)?; + self.u(1, self.nalu.frame_mbs_only_flag)?; + if !self.nalu.frame_mbs_only_flag { + self.u(1, self.nalu.mb_adaptive_frame_field_flag)?; + } + self.u(1, self.nalu.direct_8x8_inference_flag)?; + + self.u(1, self.nalu.frame_cropping_flag)?; + if self.nalu.frame_cropping_flag { + self.ue(self.nalu.frame_crop_left_offset)?; + self.ue(self.nalu.frame_crop_right_offset)?; + self.ue(self.nalu.frame_crop_top_offset)?; + self.ue(self.nalu.frame_crop_bottom_offset)?; + } + + self.u(1, self.nalu.vui_parameters_present_flag)?; + if self.nalu.vui_parameters_present_flag { + self.vui_parameters()?; + } + + Ok(()) + } +} + +impl<'n, W: Write> Synthesizer<'n, Pps, W> { + pub fn synthesize( + ref_idc: u8, + pps: &'n Pps, + writer: W, + ep_enabled: bool, + ) -> SynthesizerResult<()> { + let mut s = Self { + writer: NaluWriter::::new(writer, ep_enabled), + nalu: pps, + }; + + s.writer.write_header(ref_idc, NaluType::Pps as u8)?; + s.pic_parameter_set_rbsp()?; + s.rbsp_trailing_bits() + } + + fn pic_parameter_set_rbsp(&mut self) -> SynthesizerResult<()> { + self.ue(self.nalu.pic_parameter_set_id)?; + self.ue(self.nalu.seq_parameter_set_id)?; + self.u(1, self.nalu.entropy_coding_mode_flag)?; + self.u(1, self.nalu.bottom_field_pic_order_in_frame_present_flag)?; + + self.ue(self.nalu.num_slice_groups_minus1)?; + if self.nalu.num_slice_groups_minus1 > 0 { + return Err(SynthesizerError::Unsupported); + } + + self.ue(self.nalu.num_ref_idx_l0_default_active_minus1)?; + self.ue(self.nalu.num_ref_idx_l1_default_active_minus1)?; + self.u(1, self.nalu.weighted_pred_flag)?; + self.u(2, self.nalu.weighted_bipred_idc)?; + self.se(self.nalu.pic_init_qp_minus26)?; + self.se(self.nalu.pic_init_qs_minus26)?; + self.se(self.nalu.chroma_qp_index_offset)?; + self.u(1, self.nalu.deblocking_filter_control_present_flag)?; + self.u(1, self.nalu.constrained_intra_pred_flag)?; + self.u(1, self.nalu.redundant_pic_cnt_present_flag)?; + + if !(self.nalu.transform_8x8_mode_flag + || self.nalu.pic_scaling_matrix_present_flag + || self.nalu.second_chroma_qp_index_offset != 0) + { + return Ok(()); + } + + self.u(1, self.nalu.transform_8x8_mode_flag)?; + self.u(1, self.nalu.pic_scaling_matrix_present_flag)?; + + if self.nalu.pic_scaling_matrix_present_flag { + let mut scaling_list_count = 6; + if self.nalu.transform_8x8_mode_flag { + if self.nalu.sps.chroma_format_idc != 3 { + scaling_list_count += 2; + } else { + scaling_list_count += 6; + } + } + + for i in 0..scaling_list_count { + // Assume if scaling lists are zeroed that they are not present. + if i < 6 { + if self.nalu.scaling_lists_4x4[i] == [0; 16] { + self.u(1, /* seq_scaling_list_present_flag */ false)?; + } else { + self.u(1, /* seq_scaling_list_present_flag */ true)?; + self.scaling_list( + &self.nalu.scaling_lists_4x4[i], + Self::default_scaling_list(i), + )?; + } + } else if self.nalu.scaling_lists_8x8[i - 6] == [0; 64] { + self.u(1, /* seq_scaling_list_present_flag */ false)?; + } else { + self.u(1, /* seq_scaling_list_present_flag */ true)?; + self.scaling_list( + &self.nalu.scaling_lists_8x8[i - 6], + Self::default_scaling_list(i), + )?; + } + } + } + + self.se(self.nalu.second_chroma_qp_index_offset)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use super::*; + use crate::codec::h264::parser::Nalu; + use crate::codec::h264::parser::NaluType; + use crate::codec::h264::parser::Parser; + use crate::codec::h264::parser::Profile; + + #[test] + fn synthesize_sps() { + let raw_sps_buf = [0x00, 0x00, 0x00, 0x01, 0x07, 0x00, 0x00, 0x0a, 0xfb, 0x88]; + let mut raw_sps = Cursor::new(&raw_sps_buf[..]); + + let nalu = Nalu::next(&mut raw_sps).unwrap(); + assert_eq!(nalu.header.type_, NaluType::Sps); + + let mut parser = Parser::default(); + let sps = parser.parse_sps(&nalu).unwrap(); + + let mut buf = Vec::::new(); + Synthesizer::<'_, Sps, _>::synthesize(0, sps, &mut buf, false).unwrap(); + + assert_eq!(buf, raw_sps_buf); + + let write_to_file = std::option_env!("CROS_CODECS_TEST_WRITE_TO_FILE") == Some("true"); + if write_to_file { + let mut out = std::fs::File::create("sps.h264").unwrap(); + out.write_all(&buf).unwrap(); + out.flush().unwrap(); + } + + let mut cursor = Cursor::new(&buf[..]); + let nalu = Nalu::next(&mut cursor).unwrap(); + + let mut parser = Parser::default(); + + let sps2 = parser.parse_sps(&nalu).unwrap(); + + assert_eq!(sps, sps2); + } + + #[test] + fn synthesize_sps_scaling_lists() { + let sps = Sps { + profile_idc: Profile::High as u8, + seq_scaling_matrix_present_flag: true, + scaling_lists_4x4: [[ + 11, 20, 10, 20, 10, 22, 10, 20, 10, 20, 13, 20, 10, 20, 10, 24, + ]; 6], + scaling_lists_8x8: [ + [ + 33, 20, 10, 21, 33, 20, 12, 20, 33, 23, 10, 20, 33, 20, 10, 20, 33, 24, 10, 20, + 33, 20, 15, 20, 33, 20, 10, 26, 33, 20, 17, 20, 33, 28, 10, 20, 33, 20, 10, 20, + 33, 29, 10, 20, 33, 20, 11, 20, 33, 20, 10, 20, 33, 20, 10, 20, 33, 20, 10, 20, + 33, 20, 10, 20, + ], + [ + 10, 77, 11, 20, 10, 77, 12, 20, 10, 77, 13, 20, 10, 77, 14, 20, 10, 77, 15, 20, + 10, 77, 16, 20, 10, 77, 17, 20, 10, 77, 18, 20, 10, 77, 19, 20, 10, 77, 10, 20, + 10, 77, 10, 21, 10, 77, 10, 22, 10, 77, 10, 23, 10, 77, 10, 24, 10, 77, 10, 26, + 10, 77, 10, 28, + ], + [0; 64], + [0; 64], + [0; 64], + [0; 64], + ], + frame_mbs_only_flag: true, + ..Default::default() + }; + + let mut buf = Vec::::new(); + Synthesizer::<'_, Sps, _>::synthesize(0, &sps, &mut buf, false).unwrap(); + + let write_to_file = std::option_env!("CROS_CODECS_TEST_WRITE_TO_FILE") == Some("true"); + if write_to_file { + let mut out = std::fs::File::create("sps.h264").unwrap(); + out.write_all(&buf).unwrap(); + out.flush().unwrap(); + } + + let mut cursor = Cursor::new(&buf[..]); + let nalu = Nalu::next(&mut cursor).unwrap(); + + let mut parser = Parser::default(); + + let sps2 = parser.parse_sps(&nalu).unwrap(); + + assert_eq!(sps.scaling_lists_4x4, sps2.scaling_lists_4x4); + assert_eq!(sps.scaling_lists_8x8, sps2.scaling_lists_8x8); + } + + #[test] + fn synthesize_pps() { + let raw_sps_pps = [ + 0x00, 0x00, 0x00, 0x01, 0x07, 0x4d, 0x40, 0x0d, 0xa9, 0x18, 0x28, 0x3e, 0x60, 0x0d, + 0x41, 0x80, 0x41, 0xad, 0xb0, 0xad, 0x7b, 0xdf, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, + 0xde, 0x09, 0x88, + ]; + + let mut buf = Vec::::new(); + let mut out = Cursor::new(&mut buf); + + let mut cursor = Cursor::new(&raw_sps_pps[..]); + let mut parser: Parser = Default::default(); + + while let Ok(nalu) = Nalu::next(&mut cursor) { + match nalu.header.type_ { + NaluType::Sps => { + let sps = parser.parse_sps(&nalu).unwrap(); + Synthesizer::<'_, Sps, _>::synthesize(0, sps, &mut out, false).unwrap(); + } + NaluType::Pps => { + let pps = parser.parse_pps(&nalu).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(0, pps, &mut out, false).unwrap(); + } + _ => panic!(), + } + } + + let write_to_file = std::option_env!("CROS_CODECS_TEST_WRITE_TO_FILE") == Some("true"); + if write_to_file { + let mut out = std::fs::File::create("sps_pps.h264").unwrap(); + out.write_all(&buf).unwrap(); + out.flush().unwrap(); + + let mut out = std::fs::File::create("sps_pps_ref.h264").unwrap(); + out.write_all(&raw_sps_pps).unwrap(); + out.flush().unwrap(); + } + + assert_eq!(buf, raw_sps_pps); + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P-high.h264 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P-high.h264 new file mode 100644 index 00000000..359c3732 Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P-high.h264 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P-high.h264.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P-high.h264.crc new file mode 100644 index 00000000..f53b1576 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P-high.h264.crc @@ -0,0 +1,3 @@ +43656b2f +c9dd1361 +62d34555 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P-high.h264.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P-high.h264.md5 new file mode 100644 index 00000000..70f822bf --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P-high.h264.md5 @@ -0,0 +1,3 @@ +45ba0c1a27d0ff82fd7a969631adffe6 +cee875ded4998c9810a14f9497a11f72 +d1e0b9347134ba7a07cbcf249066f022 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P.h264 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P.h264 new file mode 100644 index 00000000..31328608 Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P.h264 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P.h264.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P.h264.crc new file mode 100644 index 00000000..b6ffab18 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P.h264.crc @@ -0,0 +1,3 @@ +ee936370 +0e5e577c +bfe430af diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P.h264.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P.h264.md5 new file mode 100644 index 00000000..9e0796db --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P-B-P.h264.md5 @@ -0,0 +1,3 @@ +fe6701d54768bc37c76e630435e8fe02 +3298d47365ac1f9dc0942ece9e104246 +7cb9209603f53ba995f7eee20b2bbf4b diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P.h264 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P.h264 new file mode 100644 index 00000000..c1fc49ce Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P.h264 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P.h264.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P.h264.crc new file mode 100644 index 00000000..e8b11af9 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P.h264.crc @@ -0,0 +1,2 @@ +9fc67012 +7f0b441e diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P.h264.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P.h264.md5 new file mode 100644 index 00000000..a741ae55 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I-P.h264.md5 @@ -0,0 +1,2 @@ +115a8e6899b71c04e32a0254ba62b30a +1fa3f5a930e08943e6d6bfbd5d43004e diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I.h264 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I.h264 new file mode 100644 index 00000000..afb61ac3 Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I.h264 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I.h264.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I.h264.crc new file mode 100644 index 00000000..83389050 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I.h264.crc @@ -0,0 +1 @@ +7dd66ef1 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I.h264.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I.h264.md5 new file mode 100644 index 00000000..180e18a5 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/64x64-I.h264.md5 @@ -0,0 +1 @@ +d2304abbf0349ec63324741bf723960d diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/README.md b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/README.md new file mode 100644 index 00000000..62f33e69 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/README.md @@ -0,0 +1,70 @@ +# H.264 Test Data + +This document lists the test data used by the H.264 decoder. + +Unless otherwise noted, the CRCs were computed using GStreamer's VA-API decoder in +`gst-plugins-bad`. + +## 16x16-I.h264 + +A 16x16 progressive byte-stream encoded I-frame to make it easier to spot errors on the libva trace. +Encoded with the following GStreamer pipeline: + +``` +gst-launch-1.0 videotestsrc num-buffers=1 ! video/x-raw,format=I420,width=16,height=16 ! \ +x264enc ! video/x-h264,profile=constrained-baseline,stream-format=byte-stream ! \ +filesink location="/tmp/16x16-I.h264" +``` + +## 16x16-I-P.h264 + +A 16x16 progressive byte-stream encoded I-frame and P-frame to make it easier to spot errors on the +libva trace. Encoded with the following GStreamer pipeline: + +``` +gst-launch-1.0 videotestsrc num-buffers=2 ! video/x-raw,format=I420,width=16,height=16 ! \ +x264enc b-adapt=false ! video/x-h264,profile=constrained-baseline,stream-format=byte-stream ! \ +filesink location="/tmp/16x16-I-P.h264" +``` + +## 16x16-I-P-B-P.h264 + +A 16x16 progressive byte-stream encoded I-P-B-P sequence to make it easier to it easier to spot +errors on the libva trace. Encoded with the following GStreamer pipeline: + +``` +gst-launch-1.0 videotestsrc num-buffers=3 ! video/x-raw,format=I420,width=16,height=16 ! \ +x264enc b-adapt=false bframes=1 ! video/x-h264,profile=constrained-baseline,stream-format=byte-stream ! \ +filesink location="/tmp/16x16-I-B-P.h264" +``` + +## 16x16-I-P-B-P-high.h264 + +A 16x16 progressive byte-stream encoded I-P-B-P sequence to make it easier to it easier to spot +errors on the libva trace. Also tests whether the decoder supports the high profile. Encoded with +the following GStreamer pipeline: + +``` +gst-launch-1.0 videotestsrc num-buffers=3 ! video/x-raw,format=I420,width=16,height=16 ! \ +x264enc b-adapt=false bframes=1 ! video/x-h264,profile=high,stream-format=byte-stream ! \ +filesink location="/tmp/16x16-I-B-P-high.h264" +``` + +## test-25fps.h264 + +Same as Chromium's `test-25fps.h264`. The slice data in `test-25fps-h264-slice-data-*.bin` was +manually extracted from GStreamer using GDB. + +## test-25fps-interlaced.h264 + +Adapted from Chromium's `test-25fps.h264`. Same file as above, but encoded as interlaced instead +using the following ffmpeg command: + +``` +ffmpeg -i \ +src/third_party/blink/web_tests/media/content/test-25fps.mp4 \ +-flags +ilme+ildct -vbsf h264_mp4toannexb -an test-25fps.h264 +``` + +This test makes sure that the interlaced logic in the decoder actually works, specially that "frame +splitting" works, as the fields here were encoded as frames. diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/gen_crcs.sh b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/gen_crcs.sh new file mode 100755 index 00000000..c0eb7b21 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/gen_crcs.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Generates the CRCs for all .h264 files in the current directory using ffmpeg. + +for f in `ls *.h264`; do + ffmpeg -i $f -pix_fmt nv12 -f framehash -hash crc32 - |grep -v '^#' |awk '{print $6}' >$f.crc + ffmpeg -i $f -pix_fmt nv12 -f framehash -hash md5 - |grep -v '^#' |awk '{print $6}' >$f.md5 +done diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-h264-slice-data-0.bin b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-h264-slice-data-0.bin new file mode 100644 index 00000000..08b46e22 Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-h264-slice-data-0.bin differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-h264-slice-data-2.bin b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-h264-slice-data-2.bin new file mode 100644 index 00000000..65f72dec Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-h264-slice-data-2.bin differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-h264-slice-data-4.bin b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-h264-slice-data-4.bin new file mode 100644 index 00000000..0327e5c4 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-h264-slice-data-4.bin @@ -0,0 +1 @@ +`__`}%\jafw[bJA1?' 4_3I?`Wqa 6FzzZ+0I`ݖ2˪}-~ _C 88l50k \ No newline at end of file diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-interlaced.h264 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-interlaced.h264 new file mode 100644 index 00000000..76990939 Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-interlaced.h264 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-interlaced.h264.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-interlaced.h264.crc new file mode 100644 index 00000000..6ba15054 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-interlaced.h264.crc @@ -0,0 +1,250 @@ +8e0a948b +0d25f469 +612a0a5c +0a452520 +6c83e97b +d1598a6e +26892325 +e7eb5f61 +e527446a +f85a9d52 +16bfd3c5 +e837620a +38484895 +050d070f +b30cfcd9 +bec9e023 +9b272332 +f6a542ba +bf978015 +1767b90a +f3755595 +5ae99ff9 +5b8b86a6 +44cd3b51 +d99e1fab +17761096 +271806c4 +b0d17f38 +01f033d7 +6ae775c1 +42933e3b +11f61f13 +4ecab452 +8e50b509 +dfde6c00 +06703f5c +92a02866 +c6f3a13b +4371bed0 +5dd9e344 +cfd668a3 +abfc6c45 +ffbc45ac +be1cbdf9 +a6dd2c68 +fc838af2 +d8f88c05 +e377a83d +8acea967 +cfcae361 +47ec0343 +c5af87e3 +a4c1d94e +080ca745 +74f48838 +00480284 +f2da9c1d +8bb000aa +32857438 +e7fcb9ae +9bbb834f +3889cf50 +a3ec330c +6963e8b3 +8de3e2f3 +8e1d1dc1 +242fd47d +e11cc789 +9e558667 +2ff16820 +d75fad55 +5f4a5907 +83182bc7 +450343a5 +dad31a6d +bb365f64 +cdd2d57b +99c0e687 +52eecf8a +951cd566 +f29f0fef +d1165eae +0df626f8 +6fbdfc15 +7f5180af +c1f6a321 +7d9b9418 +a0f25570 +c5af5562 +acc18caa +0d3d93a1 +cce8f670 +7804b84a +ed7999c0 +38dbf871 +09143151 +866c7c00 +c782f291 +65605fc3 +8f37e317 +aaf653bf +2894b605 +7eeba8f6 +cad6e5a6 +bce25e79 +cffbc84e +c29dc1b4 +99ac23e8 +7805efed +78623121 +88767543 +a5e2df6f +555355b0 +4e646cd0 +1fa3064f +dc4f65f4 +77c52338 +41287dca +0ea69260 +2fb243a8 +3de20c11 +ad7fd2c0 +79f73884 +cf89a598 +301e0a62 +3e26fed7 +8e8dacbe +628cfc2b +5c7fe45f +06892492 +fb7d8b50 +ddf98de9 +5fb4c485 +501ccf38 +e5f40baf +07ab7574 +8596934d +f7066878 +27e166e6 +10a37320 +39dc9664 +ca9e1642 +5d23f171 +dafd915b +ddcbc72c +56b4b83d +3bbde005 +41ed4060 +68caa834 +957b84b9 +5f6bbbfc +98e64dae +ebd05871 +c851d8e9 +f73fbbe4 +7451ac0b +cbfd7a78 +901fe589 +9ed86bd9 +1e9fef87 +faca6981 +98b7bd7a +e1d4bdf1 +b6a95dee +3955fb57 +90d28016 +64472746 +5f2b76df +478197b2 +c59fda8c +169c2bfe +74210735 +94662d44 +f16e4ef6 +f2134ee3 +f881db32 +0d927db1 +ab77556a +12a65d29 +632965b4 +807d10f7 +339e5f7e +be2c9336 +bc593f8b +c9dfe52d +fc738a40 +17699e28 +e8be4231 +b4264279 +895a00b1 +41eb9726 +e804873a +c6a4b014 +bdc91323 +14b6934d +dd31a422 +0d379528 +4e4b45cd +9f6773f8 +326f3f46 +6180b23d +61329916 +01a1ecf3 +8b77abcf +4508213e +dfc43ab1 +0937305a +a9d22bba +541a1ba4 +5eb3490a +4702020a +190299ca +202ef749 +911daf20 +b7b63628 +3bbb965c +25971699 +3cf16f5f +1e95278a +273193c1 +48794404 +a5a25d98 +7c85b782 +d17a5be3 +c218d70a +8ce5be9b +eed76688 +7fdd906e +136a9e51 +87674a67 +0c1f44d8 +6e09307d +f4f2d7f0 +855c52e4 +e63633cc +8baecd74 +08371d8b +18949a47 +c253b241 +c94aa045 +e272c69c +f2b73158 +6b6d3a84 +76e9bf4d +4c05b45e +1a774816 +47e007d1 +37e715a7 +8d8bd4f6 +733862ca diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-interlaced.h264.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-interlaced.h264.md5 new file mode 100644 index 00000000..6e92fb47 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps-interlaced.h264.md5 @@ -0,0 +1,250 @@ +b318f483b819b982a9756e53d7e15648 +f0257b3a3e42c43334b5114f7af458c2 +7e53dcfd409361c9747f2e15498bffec +d149b038ca22572e33a1f395e5ecee08 +eb27dd6ebe6c63da3383569c0775a831 +2cbbf7c8d7519b6f5e8ac14363b4bfa5 +ca1a19e2110b2129f11e8b49127bdd85 +286936def7ad4df160b360d65ac9b46e +b33f9984878150925a7967072e99d49b +075757bf9b1875431fff1fef1f8d757b +68d3706a9b387f97e6ddbfd4abc57ad0 +bdde980b77c72c36b4f090c9b5da5344 +2574458dd66165304420f6ea813d63e5 +35fcaf6785eb36fc903eb5279b6c29aa +ec4f4e19d7e54e26d3dfcc30e46f8a57 +bffdfe52242097b034c1716a4f3c2c28 +5fb0282d92050c9a5c7b3c12dd5ced0f +6167bc48f2e77dad220a3a6aa67b05bc +d3cffb696153090eb014e6a88802bd4e +9d175a5550156b1e8bca6099b4c09f61 +7fbd2aaeedd6f3d6cb9603910c7cf2c2 +56e21b1c18fff15a62e9933e442fe8ff +546312d22a42fc9b67b08e18d6b82852 +2509014b171245b4a3b692527c2781f3 +fb31a14a29920145563197157f60b0cf +64985fa4c405b139aa60202819bee68c +7886fd15be015e09715906507fd7ee6b +f86709eedd0ea8413c30e5417dab459a +0d3b8df30ef8188a889dc7cdeb02b21f +255db4b53038918896afe70d26f7a52c +c35945881b96b536573f7eb5a35d9bd3 +809a7dcc7966d196b61e0645025e959b +fe2130868d0026602d29492cfb77365b +1f341d6cb526c6cb2b18a4919d13dd3f +14a9658c25c52651b75b009cf505c268 +f1b76a3abadb11253729430c7f333fa8 +cd204999e6f7912ba9caa4f625314a7d +3701e818d60637dfc56407d88b79feda +819907f586f4c7515106b4083ddbad47 +c43789f2fcd506cd7ac5116c9ef4d15d +19ae171488b106301ee54078b6f5b5df +21ca7fe9e1ab4f13ef5a7f8a041d471d +6d23d3b81f1c4c787e0ee9f62f5a9849 +3c9ea82d2bd157595b69138c327133a6 +506a237ea2ec916734d9511fbe79be10 +5001323d2e1b5410def30db2f59e4261 +f4e315814b0812511fbe50d5c061e324 +a687ca61e888fe0e36db694a870a62f5 +9f6f17e40f587c27ba7a24227f279285 +1e09cb2dcafb86be51648f2a4e14dc24 +db5fff370bd822000ca204fd64bac350 +0ad486dd80931116e0eedc3c2938a201 +c2a0ee00978debffcbd633efe9299d97 +21fe48e7b1666fedfeb8a6e7b67ed7e9 +bd2f4438022e4007207af7db7e488674 +f4c6654ae5325c06d7248b423d2dfff0 +f24ccbfb2bb7856c3a6a03570b01d1df +253e90c855cb01f47c98b86c02ab5038 +9b1dc8a79e1d93d8756ed0cfcbfe5137 +aea93cf9423bc3dfc384faaca064397c +fb030c4f06693f3610802851ceb11232 +9307c5df0bef220e2dac16807f7f237e +1934c1a1a72b0646d0e9e71c15dc4987 +5ddc040a414fc5a141ff949e4b127d4a +a1d5f1d432006062b5d6b9443c717ad2 +0589d0ebf3c8d2364237e80d41a2b475 +ddcebf0cefb4f874243424fac2e3b6de +cdf1dfcfc39fa58d1f9092bf38ba7e7f +167ce118cb7d087ea441c56fe4d35fb1 +20dc2343a99d4f973832173523c2f5b1 +3c54348b7b75234d86ee3dcf298b00dc +187f13356155b3966596693b3ba7f6fc +e64ec5b7d6554e8976f9d55620ae8411 +8e4d7d9d357d5146b824655e99284939 +221da170942678c1bb96f106bdacca65 +10397b2182e00d3c2f3521adb1125b1e +820a2cbffd802eb8444d7d321903855d +fb6543d46d8733d2fe8505f058b14e4b +088bb42d269720bc1d023537f7b57213 +5d34e835cf4fe44f26e6ea7df3699323 +7525e57a778a4b298ab8a1f529dea231 +f6611d41cae1236372a61794515f3286 +fa807ac173b70892aa9fbbfe5cf097bf +b30a12a5c07b96c5d2a7ef716a40e214 +b699c6d2b59396338c025cf72e28235e +567c19149bb2089eae35fb58e6d3ca9f +d3d47ec2f02bc7d564192a98fad66e34 +bd8293548cf345333c892503bc02762e +84c3e877420d17c1536836f1b4b0cf42 +55c15bb8eb4cd81545314e0ab6535c73 +e47f3fd5c096033b0d1d3e9de8e05dfe +63e69377f86e999221ce62dd101fb530 +8ca70fbe70b840ffc159f2ba5890b472 +e5da1d908351429ba3875de6e541a294 +0cd2b4b3d9dc052d4f3d82235a469809 +8c47d7a5e67175912811cf24ce1be9df +b3a7c2128d325519e6e788ffa19b867b +bf803115b52d1452e2f46355deba01a4 +5db69b7973fd9e6da6aaf7836219d495 +4466c1bf9d8cb075799e7259b06c3dfd +0df28c89649a7ada81ed6b73995dacd3 +60adb4f96fdf22479e7d1edc583d532f +af2802fee9230f40f7662f6eb6318518 +f1a5beadc8a5ba88b74d31e211b3da2b +407d4028cf27cfe3cb265d932dd01ad8 +837ef722923ce9df10ead5332e2ad1b4 +d468d3075b95985c692d1182cf45c1f0 +cf750f31df41c4fd0f64797d7e776243 +bd0881cfb36014d96dbbd8d1872b449a +b3fdd5a0235590e0606eb8223425c206 +c2130fcb9907554280a2f1024bf5b58e +0b4cc296d9d7354718341a0b241d5417 +09be4cf122c20e92a8460571af96236d +2c8763ee3dffb8a2ab123a0b98909531 +bc07e96aa9fd80daee6b6747f3eb10d9 +5191f574d1aed73c2fea26ba696ecd73 +cd863713a5588c5df97f1272bbbc8537 +1ed61be740e1c0dbca9e803537e913fe +6ff899d76a0599bf67b4898078d10c8d +ee1f3a668c150ed6dfae060e97dc46cc +d73c55aa581d36c4f7f534f1e03c9418 +d9689cd24ae9b08375530f7ec41a574a +08846be9ad2afa27ed851634d9cab4e5 +1668585e3d72b2bc736fd5b582b17ee8 +5d2d6df2d6c40044086e8faf5e20fa76 +53e50b205ac2de14ac79d2a658ef4327 +e73d59cfc81ec50e7aa5ca9087aaeae4 +2ca1fe1fc342c4c3dcb28089e82604a3 +b1cce1a87a2486705b59517c895e817f +86143d602317e1302c8d1825bfb201d2 +7c962811ce295e6ab9c4650cae19319e +be87fbaa40ac8989649700e9a82d0ce9 +d3fd645e47efd43950972463ffd74e3d +6ed763a90aca846393467bd37bf6ecc2 +f888abbf25d47985669b2ecbbda04063 +1c64db945e018359d80762d0f1e82f85 +a5c8ff3c974ebbb722e9d6ef92de4c3e +7611866a4cb7d346752bc4700621779e +075ddd7b4bf5bcc6d308065b94c9e4e6 +0a59ab9ec9d8228b1b3d554bcbdafa26 +af6014ace2c40bada1bac0da4ca1855b +86955b7f88078d167b381366229f8a42 +f8c66e921bf190453f6c64ba9dbeb3d5 +c353624e039f8f5a6ea83196d7559886 +bb418dee4a5aab8c9c1e5f4d32a4bc8a +b56240f383319fc8ceddf6864f715a5b +544ab0aebf0e6b5bac10f5c27329ef52 +dde30afce82bbb95752b39662d79648e +ffe0f8f122b0b575c17a745d98af418a +ba476f1704cc103f303dc88d6aa5fd31 +ddfc7326b1e137d4338da33585c94fc4 +32a313e1553677d00ac43cb8610186bc +4fed1c0a1959a9dca55c1d126c704ce7 +cc073c0fcb57c2a0ac7785d3e36289cb +7e91a82a927a14d0be7a4ccdce702222 +85a1e2124c2acb27c2eae947e371332d +b8dad5132989583791ea3180ede734e3 +b749f5cfa51035c2c901c8f51fdae26f +63c8d810da89f6d93895744cf6bc6092 +c4c32e4c185cf04da9e6f6cb4b228c52 +6d48d28f00b6cbd78b4ddfa056c4b24f +a56ec3b1d282706257e9539d97708745 +3487a2b855eef272eca5c04ee2b826dd +5d35907998c38c521a0bb08dd114900b +f01a320fef7ed3cfa74fd5e7b7214491 +3655861d44a93225eab5a6f6aa4dbd9f +adc67f4388f9e4aa42232f19db02690c +096cebe6b60bc924def53b2d9413febb +8200dc7f73ae39a7fbeaf6b7fdf11e2e +ccc47ba3d01aaeb9859cd998d3472efe +727e467f3789d4b3805318dd600e8d41 +ab74f71926d8e1b5d0fb89904c4f0e56 +2ef13831a4100e211309658d5c02a177 +796c34d7ccc6790fccabc9be3e42db9c +b1fb2e39582d47ab8172702288b2fcbc +ac5d037d7457af598c9b7fca0eadb384 +7a82d0ec070abcbb2d1ef16bc8af47e7 +ed68a5b6911d53f073b778294192e44f +55b14258fc94575ccea51bc4bcf6cb61 +2502f7cd3fd06ad7994acd5ac8995508 +afbfe5035e3cdede39a1250b1d91eff4 +c7bd0e18288966e3fc902e401a1cf92a +a3243a3f3ed1f32bf4e2d5ee06cf69ab +3538beb96dff67a1d7b81a317950d038 +ff24aa585a23d93671add56e45ffdca3 +912bcec63bdabb5a4b472df4b4f557e1 +d59ad48f9b4378ce89b4bfffec9b916e +ea4e4951dc51b56e07bf01c0d4171549 +7197584855f20ff4903c247939967dca +9f7482cc0d499ab79f2eb5480807ec6e +eaadda1f5ff577d617e6c4c479b5f563 +a4c9e70aebeaf8b4f986fbf8bd8aa902 +10f3080e8374ba0397f1e296fbb60350 +b021caa467751d617842f66bb4e62b47 +444ce58ff856af0ee2ba0e99ae37950b +b61d88b82f582f32723ee5dfdb1d34de +55f921778031bd848ebddb7dc0d63db7 +bb9ad6dd7d13d4e412bdfec31783992d +a4d4f8ce07d3496017024897233b09dc +ffd49b1bb1ebb2afa89ac73305762656 +69d843e05f072e3616e9fbeefe1feb9a +4758c49357c312d6187a3cc7322d40a9 +0200eae7e77a5f7eb0ab6a52b5934c43 +6f6de279c5b1bf22064d6173e0f4c659 +dd1f5fc9e67d0f2375e72bfaefa72590 +bffd8a460d7ee4746ced0abd2e108562 +2d638249a3d755b2c778f6b384b862e8 +1e5322f8df0406fdbd2c99d5eeed0a68 +41e1a633a6adfaa70198f5872272013c +9900e6ccab80f965f0f42f5f466e51f6 +0da72ec72eb2eae9f410267cee90d1b7 +c808dd41b542e64db7ee2b6770eb99b3 +2d8b514f3e2257521b2ab872ea7d4ea4 +be6a7befa9d91157e6a70bee2cdb9516 +ccc15622babeb66a984b81d80a713966 +caa22e1262b249b7e0dbac4bdd59e391 +d759cf8e65cb2b805a7101b32bfe64d2 +43da07daa63a0ad6c39e5d3b468b7df9 +2cf45923c9b52de4b23a34ae07616bfa +1c5dd7b336a9cfcfa7772e4df2f62075 +8261e8791fc1d8a5097af6ed2c1487b8 +e97b262b8f2056851ba19b6b1b23a378 +567442abf4ee97d491572bd868d50daa +cf25196079d433cdd802e87d4211ad3e +e7295385f1842ede9e7e5e48602fac00 +6e44cb843be327f938e1558fd22d9aee +e8efa873da70fb0c55e95ccb3b98dd0c +1f520e477d527dfee845187c8bba3433 +da789f8b71e7618b6b614e7f38c6eb06 +4e9f640a8f317b0da5d3f24e87708866 +922134326b2d0223cbd3a679adc908e4 +272aa55d0137df113bee04f8c06e4d46 +6e5f5d3f6d2c88fdc80f26c3af4baf39 +2fec6a2b1154841837b77a456627de14 +71c562a6801157465fd5df54b87504b9 +eec76ebe71275d86b69a1ca42a8da947 +42e10d7a5b99affc5d9ed41006e701f2 +ae0aa8ce889e3ff30b7840b7a2b74737 +06bebb86bff1e08d071adf6284bbe3b1 +0fbe06a9de9f8195b03364748b6455e3 +46f2d55d237687a1b9d3b326c56da6c3 +3ffbbb180f1c3bf8303967a0840297af +19cd3d6d0641fe34da63c78cfc94cb72 +953d1514d529804060f71d1430fdcf7e +961279642b7ca4f0ddc329c2c0d7a819 +cc4ffd1fe4dab49b3a5ad06948fd3875 +32f0f8ff923031ac246925020c11868c +ab56616053efde4cc28e379d689d380c +253c3fa01be26ad95522f95bb3927132 +0b137261e168b892d7b6b2255198d545 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264 new file mode 100644 index 00000000..b1c36d9d Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264.crc new file mode 100644 index 00000000..71110e4f --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264.crc @@ -0,0 +1,250 @@ +3eae0fa4 +671432fb +36f2633d +0d4c5ed0 +5401b3b1 +132ea2eb +638474a8 +ddaac73d +55d506dd +a895940a +552aa819 +3df1c8cb +4570f1a7 +e355e911 +29220d98 +e1d6c064 +44b2001e +264b8f82 +d13e3c00 +36b2feb1 +dcee531a +f8a44834 +a913018c +90f486e7 +9a1101d0 +15332185 +842e0474 +bff7bbf9 +dc8cd701 +835bd3cd +2a3ea506 +67e8e53b +ef5b8038 +97c89f86 +ef8a9d7c +792b083e +388e20b0 +c9b83d6c +868e8252 +d77091cc +33a41580 +67912531 +d3a7e0cc +fd5ac6f7 +ed96bee7 +8b4a495c +1219c05f +b8050ff4 +c9c27101 +465b4659 +01fb0b1e +7a01e4d9 +1a6333b7 +bd4e9d8e +fe812caa +4bb09d4d +64b15b3d +cba3b242 +720daf5f +44efaa8c +14ae7ec5 +6c6c8c3d +3bbaa125 +7b4872f6 +a59dd7f2 +83925660 +b0679228 +f52ef9fb +2850560d +01dfd505 +94a48963 +7fcc0cca +47528c61 +032dc2f3 +bb8e0fb1 +63914082 +b9b4c36c +c69f899d +49a9c94e +77af09d5 +07cdcc3d +eabc68d5 +62421f8d +2811bd16 +e3d5a2e5 +17d95adb +55bcd9cb +d785f280 +45e97423 +c7457a9c +3e2b380f +57b67280 +d02339e4 +065e3598 +d5977df8 +8ae372e3 +54e17f4c +ba6e24c1 +a8a11da8 +ed23d1fd +c1f3ed84 +4a46e699 +fea03186 +8bf1c40d +7ee9d985 +fd3fafcc +f2b62184 +aceda460 +97d31d6c +54aa313e +d95b8ea8 +70c3792a +677ee6ad +ca53aba4 +1351a9f5 +99a1bb43 +c01cff4c +b1cb013c +ef543a58 +f5c2ae90 +ca5387ad +7b5ca533 +78d125a3 +5e04f1b8 +8f620bd6 +24ffe5c1 +5fda8ca9 +ae38f186 +2d86f080 +2b94cc5d +697a09f7 +a6187634 +dae9cce8 +74bb20a4 +18a41fd8 +a4d92f33 +e902181c +18b9f932 +e5ee4cda +1ccb55bb +6ae0fe2e +05129a95 +66d634d7 +0dceffae +00814ab6 +6370890a +c6d35660 +8eb21a54 +9ccd4c7e +b3a01706 +79d5382b +d56f2dda +b6bd04c6 +becb8b5d +0860f480 +1febbb26 +c45e567d +9b12f235 +916e5b2b +7a0c2458 +cdfd327b +666d700e +d5743e0e +a8ead497 +2de13dfe +1ef94a75 +8a05fd88 +e8498198 +b3f388bf +3ad2a8ed +9a871a37 +1e994a52 +3c9c0f61 +9e67b705 +b37baee1 +c57c176a +8cd83bd6 +ed5d5bef +d933b481 +75882924 +662a5466 +956e24ed +f55aa48e +16273797 +1dbffc93 +dadd656e +7bec0c75 +aabdd998 +136aa991 +72abfe71 +b2be52f9 +df04bac8 +fc14d7f9 +f34ae6f1 +1ea238aa +d77bd8f7 +821bb618 +4c543487 +81bc3b15 +afd3f64e +843d503a +54bf670f +f447048a +51221d02 +f20efc3b +e492c81f +a2cff836 +41ffd6d6 +8e1877af +a866b3ba +c7751eaf +6d80911d +a8701556 +0185616d +ce91cb8b +a8b341b7 +49c4f47d +d3001a6b +6410d282 +ee71f778 +190d2d8f +05c09706 +e25514d3 +7a8e74df +543ac711 +4477777e +40803303 +3cfae743 +bf691cc7 +c94b4dc2 +7127479d +2aaaa442 +a36b21a2 +f8fcf82a +e024d413 +7e080f9d +937d6382 +3e0a998f +f2af5462 +64df59a0 +9a9d533f +9e2bd916 +58084d1b +d638f9c7 +04d186be +1b3b2b54 +48a8fa7d +d20ab14c +c4b87b7e +0ec6efbf diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264.json b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264.json new file mode 100644 index 00000000..14e8ef24 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264.json @@ -0,0 +1,260 @@ +{ + "profile": "H264PROFILE_MAIN", + "width": 320, + "height": 240, + "frame_rate": 25, + "num_frames": 250, + "num_fragments": 258, + "md5_checksums": [ + "a5dad6170eb13fc5cbc6fe3511d44053", + "e056362baaf13dd0f888e67a681ab381", + "ee0c33d2b92e0443ca5770bd0c56911f", + "0c8f0226fd484358b69e9fce6294a888", + "a0809d811b6273bb63ecfdb74097e0df", + "0cada76917c6dbca2093352b3beaa2e9", + "63a5dae178cfa9e10fc6fdae7957f38d", + "7e473898faa27f0372c30ccf7c1702e3", + "6d3e896e1748f259207b2be30d24a0ad", + "7a8a94778f723b6a4da79be367577dd0", + "abbcff2c3d72fe2ccf8205c0e47142cb", + "4790caa46b6c5998d627d53cf8a45ed0", + "6d183cdb8d57e3c5cb5e5340c71214e0", + "2594ab487dcd844860fe120e92e9513b", + "1b365becbe416007e1fb269dfe2bf0d5", + "ae6953f149e85d1170a27e22cf8fea89", + "b6e8a55010fdd0a2a2f9b5aa26f24994", + "0ea165fcde3b1a71bbb2e16c615c9e5a", + "0dd539e2735c216bce229ada7e0e7722", + "9d6b6e80b820a773a16060cd73d0b047", + "f9375adb2bebacaed22305ba32af4583", + "9185b111100dd0431b23ed799ca02b70", + "bdbc5de6197178cc0ef70cdc07b5c34a", + "505517fa76a772b8af27670302b5bb80", + "df7a95b296257c8ec2c9fed399376631", + "8d8d754f34f3118fb0d9ca69da2bf6b0", + "f5d0ef57ef80fb4efffc2d4530f0d60e", + "262eee5c2f052fcff8d330c5aa62353a", + "90ff465117bfc3cc8b6363b204326a0d", + "dc2445266fec9498194b185f4dd41fb2", + "878f7dcb0df9ddeaa8470f73bbf0d1e6", + "848d8910f7c95fbe27bf00d26ce3fc97", + "e01547b08bd83922ccd677733ea21472", + "fdc049f772de5fb88493d8ec58bf808e", + "9f74486171798cbd0d4da6c6bf0ff00f", + "557ff971fa63589cd4ae687abb778e15", + "0fec7b663d136fcdab088178c7c240a2", + "65b1f1ae4a015b9da106ba97baa929f1", + "5e495f76502f2010c30c08d0ea7b6371", + "96067b4e79207c0430f68ab0a4be612b", + "5a5ff39ae498ba16009071e2b19a1047", + "cdf81c7f4c94d199cb252cb0f880ba0f", + "3e482b016234911fda0acb5a2b31b1e8", + "0eba802cbbab73ab3ea6c1713bf38249", + "4e3c939c00b2f439b9bb0e4f748cd693", + "7da28a3926f11c1ad21ada44919326b7", + "bb6ddd70a7c21190a9896deb89f0393f", + "3bbc5a5d4855fdef5cff6a72e080db59", + "8809185828397c4f188225a3a261319e", + "0e6763c0673aad271cd13fc5c6c5b05f", + "7dd4aa0e1a6ac5d308ca8437d3b33420", + "4b38d5173b9e9f03df4c7d02ae380f7c", + "a252d94720017908735d566322be9e25", + "a547bf19701974e914d556b55e5cc876", + "d9f65e78870600cd4b53cb19ecbde3e0", + "22d740be86b5ae270b34a6a05f306169", + "c7d9428660fdffccc6cff0a038a65c8f", + "50dcb76ce04decbc97e17897a03120f5", + "b39d19ae0f6a8dba61eb15f3a27512cf", + "bd1dbca640bef318da29f3d6de5bafc4", + "5eafb79ab0af0235020931cb2b411e97", + "050393b1311b31b5d711cb3a84fa0398", + "f8827e35815f52022a340129bc005860", + "5c6f844ab5b0fab77e98ea3e5a2c6d30", + "f29317ec99a14eeea363fa70d55dfe7a", + "9fe58ebd66d22be9a7a443baa2733f83", + "f7cdb42ccf66afa9cfe598fdd6243869", + "ec68961bf9e81cedb36269a0bf94c851", + "16b64cdafde9e955bf5930c4dbeb2f8e", + "0c412adf668372e1676957e58e876518", + "fb0179693e77dfa0e4873552f81415e0", + "9d31a15e3fe050695d0a7a77387dc7b6", + "737fa9ddc16c371672ab7c43645b69e5", + "3c0723cf264055cdade0834c1fd0c503", + "53b4f854484bab3f7fa8043729e14bf0", + "6fa2c2fd8930dfc46e37ea8a2a153ea5", + "a2d85585fa27d9f000aeed5da5d1722a", + "7da150a3fbfad35c7c7e233147f97927", + "f33db156c0c29a6125904477c7a52e0c", + "8a4b651ac53d128e8bd530f947ecc393", + "0ac0408a68e2fee3f359941c3c7664c4", + "0af391a381b14b44c61e67baf5716869", + "626d9be039035d65f3e6e40f3a1849b7", + "388532467f13b64b84280fc2df75157c", + "79ac74b267d1e31a9760e4797f3bd9af", + "3cb436415cc056b2d2e0b56c5ec99b14", + "66b2a9581b65a9f8381dab9b4e3cb107", + "13f8261ab7bcc21ab46848b861cab446", + "9fd2eb5e88ce9137ba774b8e5743f842", + "531d9b50624f55446efe0ce2eb168cbc", + "be08a1e2b214db9d3b8a7995bdf8c401", + "fbb6f7ff4a4fec07e4609e6292846873", + "70924eb573abb590615100d48f54ef92", + "7dc5a37f2365231da2145dfbcbcf4a1b", + "ca1ef1e6859945533b4afc4bf1a12ab3", + "289ee446a3b431865d539ca2d3a50d59", + "bc1d07902b4572ea615b6653c01f9b52", + "fae9de5e08b65986e9e43df1e5e474e3", + "ea4bb6045444a9c3085dcf546eb71016", + "162392f165140997cf35e436c03366dd", + "1a39e8d92602106ed428487c1e543541", + "383cb9b29e98c471ff4de80a3abfe0cb", + "cc70ac50536aa7b063375423dad34096", + "e2e11c7ade1b414be1e51f626b9939a6", + "2eb1b386c45d627ec119abbc84ea8dd9", + "e788b25c134e1a0f258af9a88b1e1e2f", + "10fb71dee1a7653cb6cc2d19e58b04cb", + "8d20c4bbf93fe920a470f4dfb7f2d130", + "9ff22a54b4589962f1d475a4d308f96f", + "b5f27d12234ab57a97fd8d8595738aac", + "6673fddc74d578fdf5e716218211b7ad", + "e1cc1721e9c048e480214a8cb1369f08", + "cc0d3ce9313fbc02005f4eacf08246f0", + "7e275f2e832e8b4518e2aec5925323e6", + "747af41fea71a7c443b58632ee06b6b7", + "fe8a0cc71b908e241692e36284c9e2a6", + "cdba33dbf114170b84ce01177b1eaa7d", + "0eb286cac90b1e17f4e02c8625df85b9", + "620252c40523dee3f0d3ed75d2d2bfb2", + "ab8165d3282c27f4166c44a3f947154f", + "25cb402b0681d921c819e00e5b8f77ad", + "8ff4db1bb234ca8f2f6f8f4e45dc5088", + "f30db0bbdee5bb63bdf04f5d944352a2", + "985a0d075da06266437d3257e85e6d0f", + "c53c0d7258484d806b908d2c69376688", + "9390be2306908ee719ed6135ae3e0661", + "3f74122552fa39da806b039fdcafe885", + "2c7650ec0a56db5dce916508157d282f", + "44f7fa9c7a71d3968830b2dc97005ed2", + "0e5a240b5b64b1f86db4f603248d2d48", + "dcc1268f74789b61f0120445d86dc3a7", + "1df397a99a63248fe2a283d9398e3bf1", + "72da4e7b2d0b9327bfc198ea86f259ff", + "7152dd7b90eaf1699d5d03a29f41fa96", + "ba8f38171542fd2de0e63fc4dd5ca4d1", + "452e1a61ecef97d95eedbda97f27626d", + "b5163a55f4d3da0cc563f2d8308aa8ea", + "0594c34726be28517ff0180cc826031a", + "0da52fffcf3ab88f9f1e9a0c5b6c277a", + "787b744b305354ac0127af6f7ab6e5b4", + "682d202d5b4b154c1e1d87fea635bfe9", + "b2dbacace18349a8c4bc4af6d0109caf", + "25fbe415ae8031b5f4260222520618f3", + "bab2de7762b7e0db9c8b1583b2ca2fe6", + "1d8b3418ff6170f7492f8b8098dfbe3c", + "89cf144aac4e2932d2b30683ceb8a5df", + "b952fe86a6275745f461e2f0243a8f2f", + "1c5e35bb8d3f117ad22b80c3208ab36b", + "8cba5bec1dbe1e9045cfc458fc3be807", + "e3eb152d091b689b142fb52fde68acfe", + "a90e523e20ef52a9708c27dd32a32e51", + "276c2cb2e4dc2dee97014b913d20ceef", + "bb1c76cafaed84030fbdf88cbd38bd10", + "dd7155ad9cdb7914394f376585716ce4", + "fd088933a0743c3f014c5cde815f8c0d", + "cb41f67f3730d052129aa534e4cae33b", + "349d01c4e208ae68fa5fbfa5c0b259c2", + "44571e3c02368efe154dc942a86db41d", + "9d50ad1788986f0e71aaeed49bfde214", + "9e102fe1eb1cbfd67370cfe2ec676c42", + "16eeab863b12b958d9da04ec16b76049", + "697873e7878031ba7e5fb57fcf8bb3ba", + "7d1c52765bd7dc87b01b96963085eca5", + "dbc235422231280b8b314ac38bf03943", + "bad7ce4deaf3ba172e30ee67770d5495", + "6c7b0cf1bcb5da8ac7805d722d4c8e52", + "92a923806b59470f83a1121c0a2b1282", + "790f8f46429044540879beec078c2b9e", + "fee37fef18b74dfc5fac61dd343297a4", + "adeb2bf5bddd3ee4c3977570f7957eb2", + "42da9b4d6561a9faa4646c1938665d8d", + "96ef3aa375d97217ac52921f11f53393", + "b4b9c2e6dccb82477037b237bc29beb7", + "78f5063f7255796d6b954ffa63f171bc", + "c48f05d7bd5865ba64bb670ff950528c", + "737413a29e706d1ce3cb47476881d72b", + "69da9015e20f16edccf22676850577b8", + "9b76caf25e9566b9a507d2bc9d381d54", + "c7820c8373e48a8219421ce9f01ab923", + "1482abead594e3f5dcc4bc73e5cb3ccd", + "ae8504362f1612eaf12fd4c99fb09849", + "de613970f131a752cceef714c8cee331", + "22a358e5278844f125e72d277397fc91", + "a57f9588335e6afe9b346a674b8c4b02", + "85e88cea5a43e5c7984df07a0b5bed61", + "28ef730a1eade5e972beba4e46273451", + "238dc0244e981538ef2c57c4667164a5", + "7dbead8203f5037fd4ccaf9e662995dc", + "a15b6b70fd457142da0841378b31b9e0", + "e0e51fa5868886349846a60b105721f0", + "afe444099e5c600d3ccaa8711c9099c0", + "67eca422c9f2301e0f9f00674392ff31", + "d21bc2ee616b28d5501334a56570b229", + "93d9d78079d426c690b6c3f707fcc7ab", + "50cc4faf4fa623f78f47865cfd638093", + "89457f854433da6e28fe3e7d921d632a", + "028a95319145051732f732422ad75473", + "ef7124eb0ad577d2fb7d4fbd67146242", + "d5f82be488a5bb797cbfe4449c96b13e", + "06e79dfe8b046868ef7b80eeca6f213c", + "52ecabe5b54e0bcdd9e61855b07403d9", + "ad0f3e305e4c98d31a7aa7b38bd23feb", + "fc8177e47267379c6d86cb66c70a9ecb", + "78f8589a7a57c08808c16bf976e97eb7", + "f32ef46b3a6364450a4c84dc59a3fdb1", + "6dbda2170360e7f601c76ab2d8054ef1", + "d7447d04d50b84be9fb685de827fb799", + "bd678e5ecd23870c85f0d3ac3b304548", + "caa25dceb0de2d4918b62bf290641b36", + "5e231beb0c3c7f29770db53fa311efae", + "dd68639cb5083b06a1ea1c850028cd6f", + "fa1c9525569aba0ef23804b5f586e78b", + "d75607ea7fe696715bf898aa537dc1b2", + "bb0d9294e588699ce1cf9a3e6a477340", + "ef2787371909de8fa59c9f45c41eca96", + "7835e9094edcddfb16b147525cd8792e", + "42dbe4b5aadf06ffca6f24b465b5ba3c", + "36f6aa385cd30d73424113c1a6ff0aad", + "cf2ac7f34a03a9e086858debebafb447", + "9816f6620aafc8cad3a17e089c0bb3e8", + "72db245f772dfcbd40489b96c8f2dd81", + "555b6cb91b77a820359dc17e3a85c7c9", + "88044cb9b0719ef254908df5694daf16", + "82d5c83ae707acc6a24c5e6ea8369a53", + "d6ab871f03fb9c3ebd2c16f7b1cd7778", + "7eb6daaedc6863c8ca86605862cf55fc", + "473e3dc83b70a0639735c211c6fa0fc1", + "3b249d9a5747aaae5d21b54cea85f9f4", + "1e270e073d634b47675716d555a285fc", + "7b2123f7dba790ee1fa3c2f9de87fc19", + "5e331a4678035837e9ed06957d13e05d", + "fc79c2f6c81d68ea24b5f66373ba1b6f", + "a29698a51f90cdeebe78133e9cbb741a", + "e4b88aad03803aa0e4432545b2f4d851", + "c214f459aa112b1d78bc37a599208891", + "b429e6cc68732d804d94929f89c31158", + "bb31ded86e464207d3b0b28b544806ad", + "92bdd6f9949b8ce7e610345551fcfeec", + "a0eccd9dd12fa381f36d0d877c9efa69", + "f205c3e0255e4ff75bd86d3fd4ff0fbb", + "950096d65b623715914809acf4c0a557", + "b87ef8d2938e34894ed1bbf609fb99a2", + "0a303937d3043c39e8904cc0ad181555", + "05458b392f6555d1343949122653d6af", + "d3dd90431ded20aae2f5ff0b65802fa9", + "34df02751bd5cc9efe159cb85d0e22a2", + "b32744532a99fc2657cdfbc85fd87be7", + "160a29e8413328fde6623493a4da522f", + "a2029dbf4304d50965dc2aad44b930c4", + "1c082bc656c7752f21118f10b8677a6c" + ] +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264.md5 new file mode 100644 index 00000000..fc0ca72a --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264.md5 @@ -0,0 +1,250 @@ +776f98580836e9d1f6de6cd5eaa26541 +b99c6281ace0eeaf0e5718808a568c2b +133d598e8d3bc1ad6dad666799ef3b8c +a0a05e3c48e4351dcf08934e55a53147 +79ecb0b46cf15db887390ee78403c27d +b8ad982874240fee89b026eab547af09 +fb7de18f21faa1480d0f1c50b643b8a5 +a750adaf7e7a955744c377f3ba9bd99e +3b5effc41ded56870353e19aeca14793 +adacf75ba5386d3442d30c8092fb4384 +0fb60fb5707a4b34c4a54f156c532423 +bcafabeb810e85570de61e4bd1b83466 +096823e6e69b2240cfc5f1c08a23d145 +d182e6428b339c9ec59081c4c196c4b5 +0c7edf230a9ca3112cf4895f2af7a835 +f6aca8639f2d838dba473658547a2928 +8eb680b4878e0c9e0bcf089b760b2267 +478b442172760bf7c3c7fe003ad73f21 +10e0f6e66e9739c36caae0cc2eaa97d3 +046a852108fc096d1d553a6583590d4a +45d7284eafcbc5cccc618c12bc6af4c1 +8b5426beaac95a80df290220654c1dbf +226d2eeae31131078f6d257e5ee89285 +2e1774cd1b8776a772b6dcb69773c5eb +f31ec82bece107ec42c68a53b808e949 +8a9ba366d610b45d268848d3176ea179 +17a5556be0efb4a31949857a46cee27e +f2077c849aebec971fa1cbc99d1ee8ee +a067dd84f656e0367c4b3bad1862c533 +8be7cef3ee5b1edca2902e0afb28a116 +5adae2c59438976603e9b4d7da49e210 +44390208c61cda26906a26c85264ed6e +222501ba177acc3aeda64927b0646d4e +0d5fdc758b4632fd7792ce22bcfe717c +144f01c1f347bb9f4dc27df962d37b4f +c88762634ecfa06f1d12a4ce8a025af5 +2f5304392f987f4522e1ed7519475321 +1cbef99d860f038da6b8b2769d6c8e22 +8b5209b0fd0c84b96b9707c77778fe4c +d78469cd4fba6d81b440d1fcb6ed1a71 +11d869672accf917e3f4f706f8f4a9d4 +ef985cbc882bac5bf8639f8d82680e8f +96e113dc4f4d7526b49fb5ea0f8e901d +ce418516be51f76d27509e2fd550f986 +7bd8928c90578e0d2fd8c9c1989946f9 +5a09fc3b51daba42beef52fce8e15881 +ab0430d72c7fd2288830fb99aae787e3 +7a5b68603fd986c7f6027f82fce47cdf +305ec7419408437f8daeff01e3aad6cc +0038fd9154185e9f0a5fded4e8dca4dd +375019f32ed04e8484864f9fc51afd21 +6d8fb78c18042b55884d0a9e8c7ed7ea +f83931a19f8de74dfb93082c228a73bb +04a585694299a82dc862b108f1691dbf +fbc073e705710480225f17d16ccd6527 +1d9e07daeb879c11029397f80513aec9 +3a1ba3f2da5307b17c68803bea449df1 +8d2f9d9a9d1188dbb90b987b321b9cb6 +57c52b47a13cc8aeab3ab005cceb45fd +c52eee2498f6d567bd48e5a7a4046b51 +a401da6fb4349055cf343cd44a974497 +2def25c6377a6c1b069003f838f6ec31 +67331ed57c8a7f189727f84e9a5469ec +8d279d4327fe50f2ff21ee53eb6cf015 +9e5e8223bdc1abbfba3cb65700452b70 +eb211e3d53c1fe49e89aa6ad729fc13c +fc5a3e258868dfb2aedde494f8f475cd +1fe91a15c43a4ad8f7007234de8ac222 +9d43fdbd942765778da18fa223f43cc6 +733b6b775a4a0421fc091216705ef652 +9ab696e0d1dac6105f84ea8ca9465756 +d5686e35b935d8990e5dee6d1bcf4723 +706aba57f814480fa8a17e078b6352f8 +0e5f8befc07ff3317ca174fd587e25bc +f95f242b2e2eafea3f5a20cdf0c34d89 +0f1a8078ce533787c9cb30c9429dd450 +a226c15cc7e0c0b7e1c4de8c620c07b7 +36edee26f6f02c1576019034648cd4c8 +c79dea598375f06c3a24d75f5b77e8cb +eb6fd06a0916ba6d60be7f4ad8561c0b +d022fdb8aa0e7cdc1fd83680b9b8a0fd +b5bb9c5a4cb36aa9c6b4b58076268d3d +38416e5de6f0092bbf61efe8ed7f76cb +a2ce2bf815fffc1fca8d99a4c97afb50 +6beb9dbfdc0edb74498a49c2d81106b3 +7b13ce77fc66031f036a8667076790f2 +569dcc3536bb44110d6b3a54bf0dee88 +6e08f74630a07154df6461ae25a955c9 +af8235bf965e972834a181f9210b9e09 +f9e4b7b0ffbbb24431e2ec6b6b337436 +d87cfba6630357b213316c6608583be5 +ec3382387784e715ea321fc05ce95372 +bc0fef2061b2e64ab5a40e0174a49ea7 +91d25f646606b2f9992688ef4edf4abb +008e89f1070b0d8e2477d7e9fb9a86c9 +69bf6be50b0c1f9305f2522a1b01deef +11db65d54b6c6b6c39062a3beaf82427 +efe6a4e563d48c7c86341a0f30e8fefe +8df7408615732649f592c7830cb0f00e +2fe4ec387ce34336dd8b968dac027b62 +0ae9182a07b24f27298e8eff4a74b541 +c0215167505dc21651dbef4565d0ae9c +4453511aab349d4e9444949ce7b5c138 +29921ddff3b0ba42de2b36249f06a70a +9e8fb2d42c7c90f64c346023186c1d46 +76e252269587a07138b016f57d09d5d4 +a3d3e21942d46be6f98cac225bac9fa1 +47ebd96106eb758c2ab1d7f6325a46ae +fea2c0b6ff517b07aec90c975d3e929f +a58220f49b95e399365f5a9c083923d6 +0ba65b271d8d95d5b1c4ba96a6281e48 +980589673748a3e2cfd37920e330d1e2 +5c800089bdd1626fb2d3b9b51520b2a1 +5aea26dea4436520f9643ed92409daae +48499bbd0c06d1873249ddd2a5dc76a0 +6905e3dc0ee6930b3678d56898bfc5ef +76c17247950a478a67b6adc5e4baa195 +18c123fbb6833207b27e7a400529a607 +9a41b6d8f3c52ebd3a1517fb822d09e1 +10cb9cde61fe48ed38bbe9d50755eaaf +1899fe52ca9524948a641acba633c087 +03b254591b13d993575b960b8a5e5bfe +2cbcffec5b410eb6ecdd05c48e7f0526 +69fd3b0f432bb70404a2a8deddd33856 +f0a8ad93606ae12b229aff1ec0e2c3ee +580ae926ff57ac9ae54f35b000012b97 +87e3e0f6204e2ad502eb5153c65b2632 +d129b9408e42287088d7613d3bfbcd12 +dbc5c280fb9001496ed8b45602b4de1b +f9c1efcff8549db819571d9a6cd55480 +9d0a444c988a2027cbc41c2163be63db +1070020a0a6afb6c597d61b8524381ba +a21ab68bc243e08536feb2624647f7db +2e718c27e0d3266618907e40cc4a28a6 +4669c75778da3184aba250b6310b6d1b +4e841515c3647e65a00b63dee84e67eb +f867fae5ebf1ed714c5a70f3fe10adbc +90cacccf6932ba8660e08b586f7597f7 +cb0774848fc16d1536418ab7e5ada88a +c3e74fbbd564f993828e8cbeb876f31e +90fe0f8363b9a3fb33b9b2bb4db254a4 +d38e11dcb17af7dd7e700355aa92e003 +9bb3f94f84033c4937adf84586e2be95 +4b4aedbc5dfa015b897b765e566feb96 +acd1ceb766b0a4e15f25870f0244f549 +32119d41be592c1f669ffd0b3a7888eb +7e7775b06fa9b72f771ec6b8d481a6d0 +4415957d5a0d1526db1c19bc17b5d395 +334f9ae780437dca2a5fc3377d46f6e5 +35c3b43d748aad62f7410306ea3c3f99 +1ab4c3340132c3f5fa6ef55ba7b08127 +a61818d54edda2029762e87b9b431c34 +1af968ff49189f25e28b9d8afcd46a92 +80a8b78662dcd2aa4b1615b1325757a2 +10ad96ed6e46f5b054603f66ed6bf29b +362457c91a605e04d3334b03b349ec78 +7704dc866db3238023317a0eac5b8249 +4231732950cb318403a45f38a46ad46b +a8482c9229879f410b309fc77983e3d1 +45063084d5e5dfab462adaf5fcb43795 +5028d5d62a02bf9c6843602c9843f54d +bc0d1e086e96d1347cb800a1d1aae611 +343d41fca235543df80e8ecee45f2248 +e68ed7bc5e4f4d5b92202011cdcda940 +d8735dcc4a5b6f28f8724616e256b759 +c261e7a13a9b507af279ed948acbe386 +4d5af215399a10792b02e6a372aadb02 +381abc836088178c39cc7da9b588b5a3 +406e3372afbd23924539b43f6fb9dc63 +032c29160e0529defc053d863b543926 +eb4b9c324770d1f900191833ccc4c603 +7540374c617d048db93719f2f74869e0 +8d851c4ee3ed3b42a8f8abc37dc037d9 +0cc60cb97509cd32f8de9a33248ec2a5 +bcb5a48c5816ecf1c701c1887fd72531 +2d0681c5f0c5daa293ac07c83ef0c852 +936265a67adf4d3bc271d0ef0513c4bf +58e976105d9eb6e0715927143ddfad37 +b6a82350ee6b0f00fb5323b32e8b8dd4 +5cbc1b4da91aed315f7988ee9b4f6ae7 +446945e9f46db22cc480474572957f43 +8cdbbe21312c86754be28c1af186c85f +c696ac596e16de134f9026c2fc89fcb9 +ac368b1a10748d0e18452527fdb67cd9 +f1eb8157957c7a2a84b5310c2825c5fd +7fd5969b27e06110ee5eb52c0b7f895f +93a4c8093abe3a4e37cb317584adec57 +3495c733a5c07428d7014799147f903b +5fb650661b0891f52cc9d908e3425033 +399e846bf54ea08b854adc57b33bb940 +5377988dae4eb18ac1d50e0add7938a1 +f6f14c9520459a1bde9378e1faa59ca0 +93cce870e1a92b6ab841ccf2a27b2770 +7f79cd2127ac7c289414e993005828a6 +ed4d0faa60421d65cbefcc8ea9a83f00 +8c6b723120855441c69ef4ddd7e5194b +9d675f33810dcac032f38e224e4bb65d +a99ad6ea3c77cfb3dd94f8b39a909a34 +9d2f4e964ba17ef2c7d0496231b1c47b +a6f1e6419f610d1f33888bcc7b16c6d1 +f7e92cd6ee5c8aa99f7be89fd4b9f79f +9f95bf8beae3f90eeaa45f8a6003f0aa +361311a47b6cefc08b8e8af37fd68038 +afc33b5877411da4349e013560a15e17 +3811debaef6b08f2e9338e9d04f6f02c +7fd97c64677f845e98a4a0dc97a7711c +7a7392b1f2525842393dba40e92077cb +24ec025a0d2e7c2a57ff336f0a9baba1 +72f4ad6e07a788cd9356f01c92d94c8a +3a801f8e16b8fc40b03be96b9db43e08 +c89e78f74a7dd4c649413129534b9367 +026609682e9ef750ea8e3f196c017c56 +2cc76327407153e015d1ae69f14f643f +9aa38da2abb78d2b237f89ccf3fd052d +e6f44e4485cef73ffa5a649aaeb92293 +ed8a7d1abce5a3f45d771ded4b8d0ddf +b477e09272a812756950266e6d9d21a3 +7f7822c20694075ba7f74a1cc58ed326 +da4bd23e36c0cd2683ad3a08ccb92381 +a9e2a88720bfa413e5e3b2fd4c1306bc +3e91bbfe96a7462186151f6e76e36233 +8fb00375e1d7595fa4544359f349eaed +106d0cc1aad3fa1014b8a34ebe7a4a3c +1692448922564e97d70d04cc12d55e81 +3537b9b91dcae2df5d3a09805a7ffc4d +111ddd889dc595e18f97a93e36f7d3f2 +8e8875c00cfc5394e731e8c2d4846f07 +3f728c9ea9954a093405040047271fb4 +f93ef278c1c3dff430da16ee3596a83f +51c0bd2ac650dd07df5b3630aee3705e +f33a19dd85ef67cdf5a0d92f3f1fbecf +0a6a7457190fa0fa9989dc71fee2152b +f4b157a6ef24d5d2a1027120fc419a91 +dd69ac1443293ea2f753eda6e94101bc +0f431525cb6ac56aa05484cf2a067303 +4aa02c74b3054a4ae3b13a5fbdeccd1f +4f7c553a7830426c493b59685b371223 +738fc54e143b460de42f6279a9ef1672 +a437cbbd9abf89d9e8b1e69b4ef48b86 +afe6cb281c9ce601a1f4ab4d673f734f +4756e9d1ebb100cdf77ff9e6f9d182f6 +a65853ac0ade5103a5840542e6e16c32 +f5abe519e7bf7078d4c581ed477b80bc +d75c1f88e47baeef857fc43e8ad25ba0 +7200f686415f52d0d3ac0684eca69fb2 +31674be1dabca7c3fabef1c7481d6538 +4aa521f1811346604b61a29c658ab020 +fd049c4f80a58b6e2f4aea4d02b941d7 +ef6b331ba064b6637de7e821d5d6d935 +f23fa47c8cc237fa2f878b0bfc508986 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265.rs new file mode 100644 index 00000000..44f3f8a8 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265.rs @@ -0,0 +1,7 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +pub mod dpb; +pub mod parser; +pub mod picture; diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/dpb.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/dpb.rs new file mode 100644 index 00000000..63f906d4 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/dpb.rs @@ -0,0 +1,297 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::cell::Ref; +use std::cell::RefCell; +use std::cell::RefMut; +use std::rc::Rc; + +use crate::codec::h265::parser::Sps; +use crate::codec::h265::picture::PictureData; +use crate::codec::h265::picture::Reference; + +// Shortcut to refer to a DPB entry. +// +// The first member of the tuple is the `PictureData` for the frame. +// +// The second member is the backend handle of the frame. +#[derive(Clone, Debug)] +pub struct DpbEntry(pub Rc>, pub T); + +pub struct Dpb { + /// List of `PictureData` and backend handles to decoded pictures. + entries: Vec>, + /// The maximum number of pictures that can be stored. + max_num_pics: usize, +} + +impl Dpb { + /// Returns an iterator over the underlying H265 pictures stored in the + /// DPB. + pub fn pictures(&self) -> impl Iterator> { + self.entries.iter().map(|h| h.0.borrow()) + } + + /// Returns a mutable iterator over the underlying H265 pictures stored in + /// the DPB. + pub fn pictures_mut(&mut self) -> impl Iterator> { + self.entries.iter().map(|h| h.0.borrow_mut()) + } + + /// Returns the length of the DPB. + pub fn len(&self) -> usize { + self.entries.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Get a reference to the whole DPB entries. + pub fn entries(&self) -> &Vec> { + &self.entries + } + + /// Set the dpb's max num pics. + pub fn set_max_num_pics(&mut self, max_num_pics: usize) { + self.max_num_pics = max_num_pics; + } + + /// Get a reference to the dpb's max num pics. + pub fn max_num_pics(&self) -> usize { + self.max_num_pics + } + + /// Mark all pictures in the DPB as unused for reference. + pub fn mark_all_as_unused_for_ref(&mut self) { + for mut picture in self.pictures_mut() { + picture.set_reference(Reference::None); + } + } + + /// Gets the position of `needle` in the DPB, if any. + fn get_position(&self, needle: &Rc>) -> Option { + self.entries + .iter() + .position(|handle| Rc::ptr_eq(&handle.0, needle)) + } + + /// Finds a reference picture in the DPB using `poc`. + pub fn find_ref_by_poc(&self, poc: i32) -> Option> { + let position = self + .pictures() + .position(|p| p.is_ref() && p.pic_order_cnt_val == poc); + + log::debug!("find_ref_by_poc: {}, found position {:?}", poc, position); + Some(self.entries[position?].clone()) + } + + /// Finds a reference picture in the DPB using `poc` and `mask`. + pub fn find_ref_by_poc_masked(&self, poc: i32, mask: i32) -> Option> { + let position = self + .pictures() + .position(|p| p.is_ref() && p.pic_order_cnt_val & mask == poc); + + log::debug!("find_ref_by_poc: {}, found position {:?}", poc, position); + Some(self.entries[position?].clone()) + } + + /// Finds a short term reference picture in the DPB using `poc`. + pub fn find_short_term_ref_by_poc(&self, poc: i32) -> Option> { + let position = self.pictures().position(|p| { + matches!(p.reference(), Reference::ShortTerm) && p.pic_order_cnt_val == poc + }); + + log::debug!( + "find_short_term_ref_by_poc: {}, found position {:?}", + poc, + position + ); + Some(self.entries[position?].clone()) + } + + /// Drains the DPB by continuously invoking the bumping process. + pub fn drain(&mut self) -> Vec> { + log::debug!("Draining the DPB."); + + let mut pics = vec![]; + while let Some(pic) = self.bump(true) { + pics.push(pic); + } + + pics + } + + /// Whether the DPB needs bumping. See C.5.2.2. + pub fn needs_bumping(&mut self, sps: &Sps) -> bool { + let num_needed_for_output = self.pictures().filter(|pic| pic.needed_for_output).count(); + + let highest_tid = sps.max_sub_layers_minus1; + let max_num_reorder_pics = sps.max_num_reorder_pics[usize::from(highest_tid)]; + let max_latency_increase_plus1 = sps.max_latency_increase_plus1[usize::from(highest_tid)]; + let pic_over_max_latency = self.pictures().find(|pic| { + pic.needed_for_output && pic.pic_latency_cnt >= i32::from(max_latency_increase_plus1) + }); + let max_dec_pic_buffering = + usize::from(sps.max_dec_pic_buffering_minus1[usize::from(highest_tid)]) + 1; + + num_needed_for_output > max_num_reorder_pics.into() + || (max_latency_increase_plus1 != 0 && pic_over_max_latency.is_some()) + || self.entries().len() >= max_dec_pic_buffering + } + + /// Find the lowest POC in the DPB that can be bumped. + fn find_lowest_poc_for_bumping(&self) -> Option> { + let lowest = self + .pictures() + .filter(|pic| pic.needed_for_output) + .min_by_key(|pic| pic.pic_order_cnt_val)?; + + let position = self + .entries + .iter() + .position(|handle| handle.0.borrow().pic_order_cnt_val == lowest.pic_order_cnt_val) + .unwrap(); + + Some(self.entries[position].clone()) + } + + /// See C.5.2.4 "Bumping process". + pub fn bump(&mut self, flush: bool) -> Option> { + let handle = self.find_lowest_poc_for_bumping()?; + let mut pic = handle.0.borrow_mut(); + + pic.needed_for_output = false; + log::debug!("Bumping POC {} from the dpb", pic.pic_order_cnt_val); + log::trace!("{:#?}", pic); + + if !pic.is_ref() || flush { + let index = self.get_position(&handle.0).unwrap(); + + log::debug!( + "Removed POC {} from the dpb: reference: {}, flush: {}", + pic.pic_order_cnt_val, + pic.is_ref(), + flush + ); + log::trace!("{:#?}", pic); + + self.entries.remove(index); + } + + Some(handle.clone()) + } + + /// See C.5.2.3. Happens when we are done decoding the picture. + pub fn needs_additional_bumping(&mut self, sps: &Sps) -> bool { + let num_needed_for_output = self.pictures().filter(|pic| pic.needed_for_output).count(); + let highest_tid = sps.max_sub_layers_minus1; + + let max_num_reorder_pics = sps.max_num_reorder_pics[usize::from(highest_tid)]; + let max_latency_increase_plus1 = sps.max_latency_increase_plus1[usize::from(highest_tid)]; + + let pic_over_max_latency = self.pictures().find(|pic| { + pic.needed_for_output && pic.pic_latency_cnt >= i32::from(max_latency_increase_plus1) + }); + + num_needed_for_output > max_num_reorder_pics.into() + || (max_latency_increase_plus1 != 0 && pic_over_max_latency.is_some()) + } + + /// Clears the DPB, dropping all the pictures. + pub fn clear(&mut self) { + log::debug!("Clearing the DPB"); + + let max_num_pics = self.max_num_pics; + + *self = Default::default(); + self.max_num_pics = max_num_pics; + } + + /// Removes all pictures which are marked as "not needed for output" and + /// "unused for reference". See C.5.2.2 + pub fn remove_unused(&mut self) { + log::debug!("Removing unused pictures from DPB."); + self.entries.retain(|e| { + let pic = e.0.borrow(); + let retain = pic.needed_for_output || pic.is_ref(); + log::debug!("Retaining pic POC: {}: {}", pic.pic_order_cnt_val, retain); + retain + }) + } + + /// Store a picture and its backend handle in the DPB. + pub fn store_picture( + &mut self, + picture: Rc>, + handle: T, + ) -> Result<(), String> { + if self.entries.len() >= self.max_num_pics { + return Err("Can't add a picture to the DPB: DPB is full.".into()); + } + + let mut pic = picture.borrow_mut(); + log::debug!( + "Stored picture POC {:?}, the DPB length is {:?}", + pic.pic_order_cnt_val, + self.entries.len() + ); + + if pic.pic_output_flag { + pic.needed_for_output = true; + pic.pic_latency_cnt = 0; + } else { + pic.needed_for_output = false; + } + + // C.3.4. + // After all the slices of the current picture have been decoded, this + // picture is marked as "used for short-term reference". + pic.set_reference(Reference::ShortTerm); + drop(pic); + + for mut pic in self.pictures_mut() { + pic.pic_latency_cnt += 1; + } + + self.entries.push(DpbEntry(picture, handle)); + + Ok(()) + } + + /// Returns all the references in the DPB. + pub fn get_all_references(&self) -> Vec> { + self.entries + .iter() + .filter(|e| e.0.borrow().is_ref()) + .cloned() + .collect() + } +} + +impl Default for Dpb { + fn default() -> Self { + // See https://github.com/rust-lang/rust/issues/26925 on why this can't + // be derived. + Self { + entries: Default::default(), + max_num_pics: Default::default(), + } + } +} + +impl std::fmt::Debug for Dpb { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let pics = self + .entries + .iter() + .map(|h| &h.0) + .enumerate() + .collect::>(); + f.debug_struct("Dpb") + .field("pictures", &pics) + .field("max_num_pics", &self.max_num_pics) + .finish() + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/parser.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/parser.rs new file mode 100644 index 00000000..4b6cddf1 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/parser.rs @@ -0,0 +1,4861 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +//! An Annex B h.265 parser. +//! +//! Parses VPSs, SPSs, PPSs and Slices from NALUs. + +use std::collections::BTreeMap; +use std::io::Read; +use std::io::Seek; +use std::io::SeekFrom; +use std::rc::Rc; + +use crate::bitstream_utils::BitReader; +use crate::codec::h264::nalu; +use crate::codec::h264::nalu::Header; +use crate::codec::h264::parser::Point; +use crate::codec::h264::parser::Rect; + +// Given the max VPS id. +const MAX_VPS_COUNT: usize = 16; +// Given the max SPS id. +const MAX_SPS_COUNT: usize = 16; +// Given the max PPS id. +const MAX_PPS_COUNT: usize = 64; +// 7.4.7.1 +const MAX_REF_IDX_ACTIVE: u32 = 15; + +// 7.4.3.2.1: +// num_short_term_ref_pic_sets specifies the number of st_ref_pic_set( ) syntax +// structures included in the SPS. The value of num_short_term_ref_pic_sets +// shall be in the range of 0 to 64, inclusive. +// NOTE 5 – A decoder should allocate memory for a total number of +// num_short_term_ref_pic_sets + 1 st_ref_pic_set( ) syntax structures since +// there may be a st_ref_pic_set( ) syntax structure directly signalled in the +// slice headers of a current picture. A st_ref_pic_set( ) syntax structure +// directly signalled in the slice headers of a current picture has an index +// equal to num_short_term_ref_pic_sets. +const MAX_SHORT_TERM_REF_PIC_SETS: usize = 65; + +// 7.4.3.2.1: +const MAX_LONG_TERM_REF_PIC_SETS: usize = 32; + +// From table 7-5. +const DEFAULT_SCALING_LIST_0: [u8; 16] = [16; 16]; + +// From Table 7-6. +const DEFAULT_SCALING_LIST_1: [u8; 64] = [ + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 16, 17, 16, 17, 18, 17, 18, 18, 17, 18, 21, 19, 20, + 21, 20, 19, 21, 24, 22, 22, 24, 24, 22, 22, 24, 25, 25, 27, 30, 27, 25, 25, 29, 31, 35, 35, 31, + 29, 36, 41, 44, 41, 36, 47, 54, 54, 47, 65, 70, 65, 88, 88, 115, +]; + +// From Table 7-6. +const DEFAULT_SCALING_LIST_2: [u8; 64] = [ + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 20, 20, 20, + 20, 20, 20, 20, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 28, 28, 28, 28, 28, + 28, 33, 33, 33, 33, 33, 41, 41, 41, 41, 54, 54, 54, 71, 71, 91, +]; + +/// Table 7-1 – NAL unit type codes and NAL unit type classes +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum NaluType { + #[default] + TrailN = 0, + TrailR = 1, + TsaN = 2, + TsaR = 3, + StsaN = 4, + StsaR = 5, + RadlN = 6, + RadlR = 7, + RaslN = 8, + RaslR = 9, + RsvVclN10 = 10, + RsvVclR11 = 11, + RsvVclN12 = 12, + RsvVclR13 = 13, + RsvVclN14 = 14, + RsvVclR15 = 15, + BlaWLp = 16, + BlaWRadl = 17, + BlaNLp = 18, + IdrWRadl = 19, + IdrNLp = 20, + CraNut = 21, + RsvIrapVcl22 = 22, + RsvIrapVcl23 = 23, + RsvVcl24 = 24, + RsvVcl25 = 25, + RsvVcl26 = 26, + RsvVcl27 = 27, + RsvVcl28 = 28, + RsvVcl29 = 29, + RsvVcl30 = 30, + RsvVcl31 = 31, + VpsNut = 32, + SpsNut = 33, + PpsNut = 34, + AudNut = 35, + EosNut = 36, + EobNut = 37, + FdNut = 38, + PrefixSeiNut = 39, + SuffixSeiNut = 40, + RsvNvcl41 = 41, + RsvNvcl42 = 42, + RsvNvcl43 = 43, + RsvNvcl44 = 44, + RsvNvcl45 = 45, + RsvNvcl46 = 46, + RsvNvcl47 = 47, +} + +impl TryFrom for NaluType { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(NaluType::TrailN), + 1 => Ok(NaluType::TrailR), + 2 => Ok(NaluType::TsaN), + 3 => Ok(NaluType::TsaR), + 4 => Ok(NaluType::StsaN), + 5 => Ok(NaluType::StsaR), + 6 => Ok(NaluType::RadlN), + 7 => Ok(NaluType::RadlR), + 8 => Ok(NaluType::RaslN), + 9 => Ok(NaluType::RaslR), + 10 => Ok(NaluType::RsvVclN10), + 11 => Ok(NaluType::RsvVclR11), + 12 => Ok(NaluType::RsvVclN12), + 13 => Ok(NaluType::RsvVclR13), + 14 => Ok(NaluType::RsvVclN14), + 15 => Ok(NaluType::RsvVclR15), + 16 => Ok(NaluType::BlaWLp), + 17 => Ok(NaluType::BlaWRadl), + 18 => Ok(NaluType::BlaNLp), + 19 => Ok(NaluType::IdrWRadl), + 20 => Ok(NaluType::IdrNLp), + 21 => Ok(NaluType::CraNut), + 22 => Ok(NaluType::RsvIrapVcl22), + 23 => Ok(NaluType::RsvIrapVcl23), + 24 => Ok(NaluType::RsvVcl24), + 25 => Ok(NaluType::RsvVcl25), + 26 => Ok(NaluType::RsvVcl26), + 27 => Ok(NaluType::RsvVcl27), + 28 => Ok(NaluType::RsvVcl28), + 29 => Ok(NaluType::RsvVcl29), + 30 => Ok(NaluType::RsvVcl30), + 31 => Ok(NaluType::RsvVcl31), + 32 => Ok(NaluType::VpsNut), + 33 => Ok(NaluType::SpsNut), + 34 => Ok(NaluType::PpsNut), + 35 => Ok(NaluType::AudNut), + 36 => Ok(NaluType::EosNut), + 37 => Ok(NaluType::EobNut), + 38 => Ok(NaluType::FdNut), + 39 => Ok(NaluType::PrefixSeiNut), + 40 => Ok(NaluType::SuffixSeiNut), + 41 => Ok(NaluType::RsvNvcl41), + 42 => Ok(NaluType::RsvNvcl42), + 43 => Ok(NaluType::RsvNvcl43), + 44 => Ok(NaluType::RsvNvcl44), + 45 => Ok(NaluType::RsvNvcl45), + 46 => Ok(NaluType::RsvNvcl46), + 47 => Ok(NaluType::RsvNvcl47), + _ => Err(format!("Invalid NaluType {}", value)), + } + } +} + +impl NaluType { + /// Whether this is an IDR NALU. + pub fn is_idr(&self) -> bool { + matches!(self, Self::IdrWRadl | Self::IdrNLp) + } + + /// Whether this is an IRAP NALU. + pub fn is_irap(&self) -> bool { + let type_ = *self as u32; + type_ >= Self::BlaWLp as u32 && type_ <= Self::RsvIrapVcl23 as u32 + } + + /// Whether this is a BLA NALU. + pub fn is_bla(&self) -> bool { + let type_ = *self as u32; + type_ >= Self::BlaWLp as u32 && type_ <= Self::BlaNLp as u32 + } + + /// Whether this is a CRA NALU. + pub fn is_cra(&self) -> bool { + matches!(self, Self::CraNut) + } + + /// Whether this is a RADL NALU. + pub fn is_radl(&self) -> bool { + matches!(self, Self::RadlN | Self::RadlR) + } + + /// Whether this is a RASL NALU. + pub fn is_rasl(&self) -> bool { + matches!(self, Self::RaslN | Self::RaslR) + } + + //// Whether this is a SLNR NALU. + pub fn is_slnr(&self) -> bool { + // From the specification: + // If a picture has nal_unit_type equal to TRAIL_N, TSA_N, STSA_N, + // RADL_N, RASL_N, RSV_VCL_N10, RSV_VCL_N12 or RSV_VCL_N14, the picture + // is an SLNR picture. Otherwise, the picture is a sub-layer reference + // picture. + matches!( + self, + Self::TrailN + | Self::TsaN + | Self::StsaN + | Self::RadlN + | Self::RaslN + | Self::RsvVclN10 + | Self::RsvVclN12 + | Self::RsvVclN14 + ) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct NaluHeader { + /// The NALU type. + pub type_: NaluType, + /// Specifies the identifier of the layer to which a VCL NAL unit belongs or + /// the identifier of a layer to which a non-VCL NAL unit applies. + pub nuh_layer_id: u8, + /// Minus 1 specifies a temporal identifier for the NAL unit. The value of + /// nuh_temporal_id_plus1 shall not be equal to 0. + pub nuh_temporal_id_plus1: u8, +} + +impl NaluHeader { + pub fn nuh_temporal_id(&self) -> u8 { + self.nuh_temporal_id_plus1.saturating_sub(1) + } +} + +impl Header for NaluHeader { + fn parse>(cursor: &mut std::io::Cursor) -> Result { + let mut data = [0u8; 2]; + cursor + .read_exact(&mut data) + .map_err(|_| String::from("Broken Data"))?; + let mut r = BitReader::new(&data, false); + let _ = cursor.seek(SeekFrom::Current(-1 * data.len() as i64)); + + // Skip forbidden_zero_bit + r.skip_bits(1)?; + + Ok(Self { + type_: NaluType::try_from(r.read_bits::(6)?)?, + nuh_layer_id: r.read_bits::(6)?, + nuh_temporal_id_plus1: r.read_bits::(3)?, + }) + } + + fn is_end(&self) -> bool { + matches!(self.type_, NaluType::EosNut | NaluType::EobNut) + } + + fn len(&self) -> usize { + // 7.3.1.2 + 2 + } +} + +pub type Nalu<'a> = nalu::Nalu<'a, NaluHeader>; + +/// H265 levels as defined by table A.8. +/// `general_level_idc` and `sub_layer_level_idc[ OpTid ]` shall be set equal to a +/// value of 30 times the level number specified in Table A.8 +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum Level { + #[default] + L1 = 30, + L2 = 60, + L2_1 = 63, + L3 = 90, + L3_1 = 93, + L4 = 120, + L4_1 = 123, + L5 = 150, + L5_1 = 153, + L5_2 = 156, + L6 = 180, + L6_1 = 183, + L6_2 = 186, +} + +impl TryFrom for Level { + type Error = String; + + fn try_from(value: u8) -> Result { + match value { + 30 => Ok(Level::L1), + 60 => Ok(Level::L2), + 63 => Ok(Level::L2_1), + 90 => Ok(Level::L3), + 93 => Ok(Level::L3_1), + 120 => Ok(Level::L4), + 123 => Ok(Level::L4_1), + 150 => Ok(Level::L5), + 153 => Ok(Level::L5_1), + 156 => Ok(Level::L5_2), + 180 => Ok(Level::L6), + 183 => Ok(Level::L6_1), + 186 => Ok(Level::L6_2), + _ => Err(format!("Invalid Level {}", value)), + } + } +} + +/// H265 profiles. See A.3. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum Profile { + #[default] + Main = 1, + Main10 = 2, + MainStill = 3, + RangeExtensions = 4, + HighThroughput = 5, + MultiviewMain = 6, + ScalableMain = 7, + ThreeDMain = 8, + ScreenContentCoding = 9, + ScalableRangeExtensions = 10, + HighThroughputScreenContentCoding = 11, +} + +impl TryFrom for Profile { + type Error = String; + + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(Profile::Main), + 2 => Ok(Profile::Main10), + 3 => Ok(Profile::MainStill), + 4 => Ok(Profile::RangeExtensions), + 5 => Ok(Profile::HighThroughput), + 6 => Ok(Profile::MultiviewMain), + 7 => Ok(Profile::ScalableMain), + 8 => Ok(Profile::ThreeDMain), + 9 => Ok(Profile::ScreenContentCoding), + 10 => Ok(Profile::ScalableRangeExtensions), + 11 => Ok(Profile::HighThroughputScreenContentCoding), + _ => Err(format!("Invalid Profile {}", value)), + } + } +} + +/// A H.265 Video Parameter Set. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Vps { + /// Identifies the VPS for reference by other syntax elements. + pub video_parameter_set_id: u8, + /// If vps_base_layer_internal_flag is equal to 1 and + /// vps_base_layer_available_flag is equal to 1, the base layer is present + /// in the bitstream. + pub base_layer_internal_flag: bool, + /// See `base_layer_internal_flag`. + pub base_layer_available_flag: bool, + /// Plus 1 specifies the maximum allowed number of layers in each CVS + /// referring to the VPS. + pub max_layers_minus1: u8, + /// Plus 1 specifies the maximum number of temporal sub-layers that may be + /// present in each CVS referring to the VPS. + pub max_sub_layers_minus1: u8, + /// When vps_max_sub_layers_minus1 is greater than 0, specifies whether + /// inter prediction is additionally restricted for CVSs referring to the + /// VPS. + pub temporal_id_nesting_flag: bool, + /// ProfileTierLevel() data. + pub profile_tier_level: ProfileTierLevel, + /// When true, specifies that `vps_max_dec_pic_buffering_minus1[ i ]`, + /// `vps_max_num_reorder_pics[ i ]` and `vps_max_latency_increase_plus1[ i ]` + /// are present for vps_max_sub_layers_ minus1 + 1 sub-layers. + /// vps_sub_layer_ordering_info_present_flag equal to 0 specifies that the + /// values of `vps_max_dec_pic_buffering_minus1[ vps_max_sub_layers_minus1 ]`, + /// vps_max_num_reorder_pics[ vps_max_sub_ layers_minus1 ] and + /// `vps_max_latency_increase_plus1[ vps_max_sub_layers_minus1 ]` apply to all + /// sub-layers + pub sub_layer_ordering_info_present_flag: bool, + /// `max_dec_pic_buffering_minus1[i]` plus 1 specifies the maximum required + /// size of the decoded picture buffer for the CVS in units of picture + /// storage buffers when HighestTid is equal to i. + pub max_dec_pic_buffering_minus1: [u32; 7], + /// Indicates the maximum allowed number of pictures with PicOutputFlag + /// equal to 1 that can precede any picture with PicOutputFlag equal to 1 in + /// the CVS in decoding order and follow that picture with PicOutputFlag + /// equal to 1 in output order when HighestTid is equal to i. + pub max_num_reorder_pics: [u32; 7], + /// When true, `max_latency_increase_plus1[i]` is used to compute the value of + /// `VpsMaxLatencyPictures[ i ]`, which specifies the maximum number of + /// pictures with PicOutputFlag equal to 1 that can precede any picture with + /// PicOutputFlag equal to 1 in the CVS in output order and follow that + /// picture with PicOutputFlag equal to 1 in decoding order when HighestTid + /// is equal to i. + pub max_latency_increase_plus1: [u32; 7], + /// Specifies the maximum allowed value of nuh_layer_id of all NAL units in + /// each CVS referring to the VPS. + pub max_layer_id: u8, + /// num_layer_sets_minus1 plus 1 specifies the number of layer sets that are + /// specified by the VPS. + pub num_layer_sets_minus1: u32, + /// When true, specifies that num_units_in_tick, time_scale, + /// poc_proportional_to_timing_flag and num_hrd_parameters are present in + /// the VPS. + pub timing_info_present_flag: bool, + /// The number of time units of a clock operating at the frequency + /// vps_time_scale Hz that corresponds to one increment (called a clock + /// tick) of a clock tick counter. The value of vps_num_units_in_tick shall + /// be greater than 0. A clock tick, in units of seconds, is equal to the + /// quotient of vps_num_units_in_tick divided by vps_time_scale. For + /// example, when the picture rate of a video signal is 25 Hz, + /// vps_time_scale may be equal to 27 000 000 and vps_num_units_in_tick may + /// be equal to 1 080 000, and consequently a clock tick may be 0.04 + /// seconds. + pub num_units_in_tick: u32, + /// The number of time units that pass in one second. For example, a time + /// coordinate system that measures time using a 27 MHz clock has a + /// vps_time_scale of 27 000 000. + pub time_scale: u32, + /// When true, indicates that the picture order count value for each picture + /// in the CVS that is not the first picture in the CVS, in decoding order, + /// is proportional to the output time of the picture relative to the output + /// time of the first picture in the CVS. When false, indicates that the + /// picture order count value for each picture in the CVS that is not the + /// first picture in the CVS, in decoding order, may or may not be + /// proportional to the output time of the picture relative to the output + /// time of the first picture in the CVS. + pub poc_proportional_to_timing_flag: bool, + /// num_ticks_poc_diff_one_minus1 plus 1 specifies the number of clock ticks + /// corresponding to a difference of picture order count values equal to 1. + pub num_ticks_poc_diff_one_minus1: u32, + /// Specifies the number of hrd_parameters( ) syntax structures present in + /// the VPS RBSP before the vps_extension_flag syntax element. + pub num_hrd_parameters: u32, + /// `hrd_layer_set_idx[ i ]` specifies the index, into the list of layer sets + /// specified by the VPS, of the layer set to which the i-th hrd_parameters( + /// ) syntax structure in the VPS applies. + pub hrd_layer_set_idx: Vec, + /// `cprms_present_flag[ i ]` equal to true specifies that the HRD parameters + /// that are common for all sub-layers are present in the i-th + /// hrd_parameters( ) syntax structure in the VPS. `cprms_present_flag[ i ]` + /// equal to false specifies that the HRD parameters that are common for all + /// sub-layers are not present in the i-th hrd_parameters( ) syntax + /// structure in the VPS and are derived to be the same as the ( i − 1 )-th + /// hrd_parameters( ) syntax structure in the VPS. `cprms_present_flag[ 0 ]` + /// is inferred to be equal to true. + pub cprms_present_flag: Vec, + /// The hrd_parameters() data. + pub hrd_parameters: Vec, + /// When false, specifies that no vps_extension_data_flag syntax elements + /// are present in the VPS RBSP syntax structure. When true, specifies that + /// there are vps_extension_data_flag syntax elements present in the VPS + /// RBSP syntax structure. Decoders conforming to a profile specified in + /// Annex A but not supporting the INBLD capability specified in Annex F + /// shall ignore all data that follow the value 1 for vps_extension_flag in + /// a VPS NAL unit. + pub extension_flag: bool, +} + +impl Default for Vps { + fn default() -> Self { + Self { + video_parameter_set_id: Default::default(), + base_layer_internal_flag: Default::default(), + base_layer_available_flag: Default::default(), + max_layers_minus1: Default::default(), + max_sub_layers_minus1: Default::default(), + temporal_id_nesting_flag: Default::default(), + profile_tier_level: Default::default(), + sub_layer_ordering_info_present_flag: Default::default(), + max_dec_pic_buffering_minus1: Default::default(), + max_num_reorder_pics: Default::default(), + max_latency_increase_plus1: Default::default(), + max_layer_id: Default::default(), + num_layer_sets_minus1: Default::default(), + timing_info_present_flag: Default::default(), + num_units_in_tick: Default::default(), + time_scale: Default::default(), + poc_proportional_to_timing_flag: Default::default(), + num_ticks_poc_diff_one_minus1: Default::default(), + num_hrd_parameters: Default::default(), + hrd_layer_set_idx: Default::default(), + cprms_present_flag: vec![true], + hrd_parameters: Default::default(), + extension_flag: Default::default(), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ProfileTierLevel { + /// Specifies the context for the interpretation of general_profile_idc and + /// `general_profile_compatibility_flag[ j ]` for all values of j in the range + /// of 0 to 31, inclusive. + pub general_profile_space: u8, + /// Specifies the tier context for the interpretation of general_level_idc + /// as specified in Annex A. + pub general_tier_flag: bool, + /// When general_profile_space is equal to 0, indicates a profile to which + /// the CVS conforms as specified in Annex A. Bitstreams shall not contain + /// values of general_profile_idc other than those specified in Annex A. + /// Other values of general_profile_idc are reserved for future use by ITU-T + /// | ISO/IEC. + pub general_profile_idc: u8, + /// `general_profile_compatibility_flag[ j ]` equal to true, when + /// general_profile_space is false, indicates that the CVS conforms to the + /// profile indicated by general_profile_idc equal to j as specified in + /// Annex A. + pub general_profile_compatibility_flag: [bool; 32], + /// general_progressive_source_flag and general_interlaced_source_flag are + /// interpreted as follows: + /// + /// –If general_progressive_source_flag is true and + /// general_interlaced_source_flag is false, the source scan type of the + /// pictures in the CVS should be interpreted as progressive only. + /// + /// –Otherwise, if general_progressive_source_flag is false and + /// general_interlaced_source_flag is true, the source scan type of the + /// pictures in the CVS should be interpreted as interlaced only. + /// + /// –Otherwise, if general_progressive_source_flag is false and + /// general_interlaced_source_flag is false, the source scan type of the + /// pictures in the CVS should be interpreted as unknown or unspecified. + /// + /// –Otherwise (general_progressive_source_flag is true and + /// general_interlaced_source_flag is true), the source scan type of each + /// picture in the CVS is indicated at the picture level using the syntax + /// element source_scan_type in a picture timing SEI message. + pub general_progressive_source_flag: bool, + /// See `general_progressive_source_flag`. + pub general_interlaced_source_flag: bool, + /// If true, specifies that there are no frame packing arrangement SEI + /// messages, segmented rectangular frame packing arrangement SEI messages, + /// equirectangular projection SEI messages, or cubemap projection SEI + /// messages present in the CVS. If false, indicates that there may or may + /// not be one or more frame packing arrangement SEI messages, segmented + /// rectangular frame packing arrangement SEI messages, equirectangular + /// projection SEI messages, or cubemap projection SEI messages present in + /// the CVS. + pub general_non_packed_constraint_flag: bool, + /// When true, specifies that field_seq_flag is false. When false, indicates + /// that field_seq_flag may or may not be false. + pub general_frame_only_constraint_flag: bool, + /// See Annex A. + pub general_max_12bit_constraint_flag: bool, + /// See Annex A. + pub general_max_10bit_constraint_flag: bool, + /// See Annex A. + pub general_max_8bit_constraint_flag: bool, + /// See Annex A. + pub general_max_422chroma_constraint_flag: bool, + /// See Annex A. + pub general_max_420chroma_constraint_flag: bool, + /// See Annex A. + pub general_max_monochrome_constraint_flag: bool, + /// See Annex A. + pub general_intra_constraint_flag: bool, + /// See Annex A. + pub general_lower_bit_rate_constraint_flag: bool, + /// See Annex A. + pub general_max_14bit_constraint_flag: bool, + /// See Annex A. + pub general_one_picture_only_constraint_flag: bool, + /// When true, specifies that the INBLD capability as specified in Annex F + /// is required for decoding of the layer to which the profile_tier_level( ) + /// syntax structure applies. When false, specifies that the INBLD + /// capability as specified in Annex F is not required for decoding of the + /// layer to which the profile_tier_level( ) syntax structure applies. + pub general_inbld_flag: bool, + /// Indicates a level to which the CVS conforms as specified in Annex A. + pub general_level_idc: Level, + /// Sub-layer syntax element. + pub sub_layer_profile_present_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_level_present_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_profile_space: [u8; 6], + /// Sub-layer syntax element. + pub sub_layer_tier_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_profile_idc: [u8; 6], + /// Sub-layer syntax element. + pub sub_layer_profile_compatibility_flag: [[bool; 32]; 6], + /// Sub-layer syntax element. + pub sub_layer_progressive_source_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_interlaced_source_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_non_packed_constraint_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_frame_only_constraint_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_max_12bit_constraint_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_max_10bit_constraint_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_max_8bit_constraint_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_max_422chroma_constraint_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_max_420chroma_constraint_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_max_monochrome_constraint_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_intra_constraint_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_one_picture_only_constraint_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_lower_bit_rate_constraint_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_max_14bit_constraint_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_inbld_flag: [bool; 6], + /// Sub-layer syntax element. + pub sub_layer_level_idc: [Level; 6], +} + +impl ProfileTierLevel { + pub fn max_luma_ps(&self) -> u32 { + // See Table A.8. + match self.general_level_idc { + Level::L1 => 36864, + Level::L2 => 122880, + Level::L2_1 => 245760, + Level::L3 => 552960, + Level::L3_1 => 983040, + Level::L4 | Level::L4_1 => 2228224, + Level::L5 | Level::L5_1 | Level::L5_2 => 8912896, + _ => 35651584, + } + } + + pub fn max_dpb_pic_buf(&self) -> u32 { + if self.general_profile_idc >= 1 && self.general_profile_idc <= 5 { + 6 + } else { + 7 + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SpsRangeExtension { + pub transform_skip_rotation_enabled_flag: bool, + pub transform_skip_context_enabled_flag: bool, + pub implicit_rdpcm_enabled_flag: bool, + pub explicit_rdpcm_enabled_flag: bool, + pub extended_precision_processing_flag: bool, + pub intra_smoothing_disabled_flag: bool, + pub high_precision_offsets_enabled_flag: bool, + pub persistent_rice_adaptation_enabled_flag: bool, + pub cabac_bypass_alignment_enabled_flag: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SpsSccExtension { + /// When set, specifies that a picture in the CVS may be included in a + /// reference picture list of a slice of the picture itself. When not set, + /// specifies that a picture in the CVS is never included in a reference + /// picture list of a slice of the picture itself. + pub curr_pic_ref_enabled_flag: bool, + /// When set, specifies that the decoding process for palette mode may be + /// used for intra blocks. When not set, specifies that the decoding process + /// for palette mode is not applied. + pub palette_mode_enabled_flag: bool, + /// Specifies the maximum allowed palette size. + pub palette_max_size: u8, + /// Specifies the difference between the maximum allowed palette predictor + /// size and the maximum allowed palette size. + pub delta_palette_max_predictor_size: u8, + /// When set, specifies that the sequence palette predictors are initialized + /// using the sps_palette_predictor_initializers. When not set, specifies + /// that the entries in the sequence palette predictor are initialized to 0. + pub palette_predictor_initializers_present_flag: bool, + /// num_palette_predictor_initializers_minus1 plus 1 specifies the number of + /// entries in the sequence palette predictor initializer. + pub num_palette_predictor_initializer_minus1: u8, + /// `palette_predictor_initializer[ comp ][ i ]` specifies the value of the + /// comp-th component of the i-th palette entry in the SPS that is used to + /// initialize the array PredictorPaletteEntries. + pub palette_predictor_initializer: [[u32; 128]; 3], + /// Controls the presence and inference of the use_integer_mv_flag that + /// specifies the resolution of motion vectors for inter prediction. + pub motion_vector_resolution_control_idc: u8, + /// When set, specifies that the intra boundary filtering process is + /// unconditionally disabled for intra prediction. If not set, specifies + /// that the intra boundary filtering process may be used. + pub intra_boundary_filtering_disabled_flag: bool, +} + +impl Default for SpsSccExtension { + fn default() -> Self { + Self { + curr_pic_ref_enabled_flag: Default::default(), + palette_mode_enabled_flag: Default::default(), + palette_max_size: Default::default(), + delta_palette_max_predictor_size: Default::default(), + palette_predictor_initializers_present_flag: Default::default(), + num_palette_predictor_initializer_minus1: Default::default(), + palette_predictor_initializer: [[0; 128]; 3], + motion_vector_resolution_control_idc: Default::default(), + intra_boundary_filtering_disabled_flag: Default::default(), + } + } +} + +/// A H.265 Sequence Parameter Set. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Sps { + /// Specifies the value of the vps_video_parameter_set_id of the active VPS. + pub video_parameter_set_id: u8, + /// `max_sub_layers_minus1` plus 1 specifies the maximum number of temporal + /// sub-layers that may be present in each CVS referring to the SPS. + pub max_sub_layers_minus1: u8, + /// When sps_max_sub_layers_minus1 is greater than 0, specifies whether + /// inter prediction is additionally restricted for CVSs referring to the + /// SPS. + pub temporal_id_nesting_flag: bool, + /// profile_tier_level() data. + pub profile_tier_level: ProfileTierLevel, + /// Provides an identifier for the SPS for reference by other syntax + /// elements. + pub seq_parameter_set_id: u8, + /// Specifies the chroma sampling relative to the luma sampling as specified + /// in clause 6.2. + pub chroma_format_idc: u8, + /// When true, specifies that the three colour components of the 4:4:4 + /// chroma format are coded separately. When false, specifies that the + /// colour components are not coded separately. + pub separate_colour_plane_flag: bool, + /// Specifies the width of each decoded picture in units of luma samples. + pub pic_width_in_luma_samples: u16, + /// Specifies the height of each decoded picture in units of luma samples. + pub pic_height_in_luma_samples: u16, + /// When true, indicates that the conformance cropping window offset + /// parameters follow next in the SPS. When false, indicates that the + /// conformance cropping window offset parameters are not present. + pub conformance_window_flag: bool, + /* if conformance_window_flag */ + /// Specify the samples of the pictures in the CVS that are output from the + /// decoding process, in terms of a rectangular region specified in picture + /// coordinates for output. + pub conf_win_left_offset: u32, + pub conf_win_right_offset: u32, + pub conf_win_top_offset: u32, + pub conf_win_bottom_offset: u32, + + /// Specifies the bit depth of the samples of the luma array BitDepthY and + /// the value of the luma quantization parameter range offset QpBdOffsetY. + pub bit_depth_luma_minus8: u8, + /// Specifies the bit depth of the samples of the chroma arrays BitDepthC + /// and the value of the chroma quantization parameter range offset + /// QpBdOffsetC. + pub bit_depth_chroma_minus8: u8, + /// Specifies the value of the variable MaxPicOrderCntLsb that is used in + /// the decoding process for picture order count. + pub log2_max_pic_order_cnt_lsb_minus4: u8, + /// When true, specifies that `max_dec_pic_buffering_minus1[ i ]`, + /// `max_num_reorder_pics[ i ]` and `max_latency_increase_plus1[ i ]` are + /// present for max_sub_layers_minus1 + 1 sub- layers. When false, specifies + /// that the values of `max_dec_pic_ buffering_minus1[ max_sub_layers_minus1 + /// ]`, `max_num_reorder_pics[ max_sub_layers_minus1 ]` and max_ + /// `latency_increase_plus1[ max_sub_layers_minus1 ]` apply to all sub-layers. + pub sub_layer_ordering_info_present_flag: bool, + /// `max_dec_pic_buffering_minus1[ i ]` plus 1 specifies the maximum required + /// size of the decoded picture buffer for the CVS in units of picture + /// storage buffers when HighestTid is equal to i. + pub max_dec_pic_buffering_minus1: [u8; 7], + /// `max_num_reorder_pics[ i ]` indicates the maximum allowed number of + /// pictures with PicOutputFlag equal to 1 that can precede any picture with + /// PicOutputFlag equal to 1 in the CVS in decoding order and follow that + /// picture with PicOutputFlag equal to 1 in output order when HighestTid is + /// equal to i. + pub max_num_reorder_pics: [u8; 7], + /// `max_latency_increase_plus1[ i ]` not equal to 0 is used to compute the + /// value of `SpsMaxLatencyPictures[ i ]`, which specifies the maximum number + /// of pictures with PicOutputFlag equal to 1 that can precede any picture + /// with PicOutputFlag equal to 1 in the CVS in output order and follow that + /// picture with PicOutputFlag equal to 1 in decoding order when HighestTid + /// is equal to i. + pub max_latency_increase_plus1: [u8; 7], + /// min_luma_coding_block_size_minus3 plus 3 specifies the minimum luma + /// coding block size. + pub log2_min_luma_coding_block_size_minus3: u8, + /// Specifies the difference between the maximum and minimum luma coding + /// block size. + pub log2_diff_max_min_luma_coding_block_size: u8, + /// min_luma_transform_block_size_minus2 plus 2 specifies the minimum luma + /// transform block size. + pub log2_min_luma_transform_block_size_minus2: u8, + /// Specifies the difference between the maximum and minimum luma transform + /// block size. + pub log2_diff_max_min_luma_transform_block_size: u8, + /// Specifies the maximum hierarchy depth for transform units of coding + /// units coded in inter prediction mode. + pub max_transform_hierarchy_depth_inter: u8, + /// Specifies the maximum hierarchy depth for transform units of coding + /// units coded in intra prediction mode. + pub max_transform_hierarchy_depth_intra: u8, + /// When true, specifies that a scaling list is used for the scaling process + /// for transform coefficients. When false, specifies that scaling list is + /// not used for the scaling process for transform coefficients. + pub scaling_list_enabled_flag: bool, + /* if scaling_list_enabled_flag */ + /// When true, specifies that the scaling_list_data( ) syntax structure is + /// present in the SPS. When false, specifies that the scaling_list_data( ) + /// syntax structure is not present in the SPS. + pub scaling_list_data_present_flag: bool, + /// The scaling_list_data() syntax data. + pub scaling_list: ScalingLists, + /// When true, specifies that asymmetric motion partitions, i.e., PartMode + /// equal to PART_2NxnU, PART_2NxnD, PART_nLx2N or PART_nRx2N, may be used + /// in CTBs. When false, specifies that asymmetric motion partitions cannot + /// be used in CTBs. + pub amp_enabled_flag: bool, + /// When true, specifies that the sample adaptive offset process is applied + /// to the reconstructed picture after the deblocking filter process. When + /// false, specifies that the sample adaptive offset process is not applied + /// to the reconstructed picture after the deblocking filter process. + pub sample_adaptive_offset_enabled_flag: bool, + /// When false, specifies that PCM-related syntax + /// (pcm_sample_bit_depth_luma_minus1, pcm_sample_ bit_depth_chroma_minus1, + /// log2_min_pcm_luma_coding_block_size_minus3, log2_diff_max_min_pcm_luma_ + /// coding_block_size, pcm_loop_filter_disabled_flag, pcm_flag, + /// pcm_alignment_zero_bit syntax elements and pcm_sample( ) syntax + /// structure) is not present in the CVS. + pub pcm_enabled_flag: bool, + + /* if pcm_enabled_flag */ + pub pcm_sample_bit_depth_luma_minus1: u8, + /// Specifies the number of bits used to represent each of PCM sample values + /// of the luma component. + pub pcm_sample_bit_depth_chroma_minus1: u8, + /// Specifies the number of bits used to represent each of PCM sample values + /// of the chroma components. + pub log2_min_pcm_luma_coding_block_size_minus3: u8, + /// Specifies the difference between the maximum and minimum size of coding + /// blocks with pcm_flag equal to true. + pub log2_diff_max_min_pcm_luma_coding_block_size: u8, + /// Specifies whether the loop filter process is disabled on reconstructed + /// samples in a coding unit with pcm_flag equal to true as follows: + /// + /// – If pcm_loop_filter_disabled_flag is set, the deblocking filter and + /// sample adaptive offset filter processes on the reconstructed samples in + /// a coding unit with pcm_flag set are disabled. + /// + /// – Otherwise (pcm_loop_filter_disabled_flag value is not set), the + /// deblocking filter and sample adaptive offset filter processes on the + /// reconstructed samples in a coding unit with pcm_flag set are not + /// disabled. + pub pcm_loop_filter_disabled_flag: bool, + /// Specifies the number of st_ref_pic_set( ) syntax structures included in + /// the SPS. + pub num_short_term_ref_pic_sets: u8, + /// the st_ref_pic_set() data. + pub short_term_ref_pic_set: Vec, + /// If unset, specifies that no long-term reference picture is used for + /// inter prediction of any coded picture in the CVS. + /// If set, specifies that long-term reference pictures may be used for + /// inter prediction of one or more coded pictures in the CVS. + pub long_term_ref_pics_present_flag: bool, + + /* if long_term_ref_pics_present_flag */ + /// Specifies the number of candidate long-term reference pictures that are + /// specified in the SPS. + pub num_long_term_ref_pics_sps: u8, + /// `lt_ref_pic_poc_lsb_sps[ i ]` specifies the picture order count modulo + /// MaxPicOrderCntLsb of the i-th candidate long-term reference picture + /// specified in the SPS. + pub lt_ref_pic_poc_lsb_sps: [u32; MAX_LONG_TERM_REF_PIC_SETS], + /// `used_by_curr_pic_lt_sps_flag[ i ]` equal to false specifies that the i-th + /// candidate long-term reference picture specified in the SPS is not used + /// for reference by a picture that includes in its long-term reference + /// picture set (RPS) the i-th candidate long-term reference picture + /// specified in the SPS. + pub used_by_curr_pic_lt_sps_flag: [bool; MAX_LONG_TERM_REF_PIC_SETS], + /// When set, specifies that slice_temporal_mvp_enabled_flag is present in + /// the slice headers of non-IDR pictures in the CVS. When not set, + /// specifies that slice_temporal_mvp_enabled_flag is not present in slice + /// headers and that temporal motion vector predictors are not used in the + /// CVS. + pub temporal_mvp_enabled_flag: bool, + /// When set, specifies that bi-linear interpolation is conditionally used + /// in the intraprediction filtering process in the CVS as specified in + /// clause 8.4.4.2.3. + pub strong_intra_smoothing_enabled_flag: bool, + /// When set, specifies that the vui_parameters( ) syntax structure as + /// specified in Annex E is present. When not set, specifies that the + /// vui_parameters( ) syntax structure as specified in Annex E is not + /// present. + pub vui_parameters_present_flag: bool, + /// The vui_parameters() data. + pub vui_parameters: VuiParams, + /// When set, specifies that the syntax elements sps_range_extension_flag, + /// sps_multilayer_extension_flag, sps_3d_extension_flag, + /// sps_scc_extension_flag, and sps_extension_4bits are present in the SPS + /// RBSP syntax structure. When not set, specifies that these syntax + /// elements are not present. + pub extension_present_flag: bool, + + pub range_extension_flag: bool, + /// The sps_range_extension() data. + pub range_extension: SpsRangeExtension, + /// When set, specifies that the sps_scc_extension( ) syntax structure is + /// present in the SPS RBSP syntax structure. When not set, specifies that + /// this syntax structure is not present + pub scc_extension_flag: bool, + /// The sps_scc_extension() data. + pub scc_extension: SpsSccExtension, + + // Internal H265 variables. Computed from the bitstream. + /// Equivalent to MinCbLog2SizeY in the specification. + pub min_cb_log2_size_y: u32, + /// Equivalent to CtbLog2SizeY in the specification. + pub ctb_log2_size_y: u32, + /// Equivalent to CtbSizeY in the specification. + pub ctb_size_y: u32, + /// Equivalent to PicHeightInCtbsY in the specification. + pub pic_height_in_ctbs_y: u32, + /// Equivalent to PicWidthInCtbsY in the specification. + pub pic_width_in_ctbs_y: u32, + /// Equivalent to PicSizeInCtbsY in the specification. + pub pic_size_in_ctbs_y: u32, + /// Equivalent to ChromaArrayType in the specification. + pub chroma_array_type: u8, + /// Equivalent to WpOffsetHalfRangeY in the specification. + pub wp_offset_half_range_y: u32, + /// Equivalent to WpOffsetHalfRangeC in the specification. + pub wp_offset_half_range_c: u32, + /// Equivalent to MaxTbLog2SizeY in the specification. + pub max_tb_log2_size_y: u32, + /// Equivalent to PicSizeInSamplesY in the specification. + pub pic_size_in_samples_y: u32, + + /// The VPS referenced by this SPS, if any. + pub vps: Option>, +} + +impl Sps { + pub fn max_dpb_size(&self) -> usize { + let max_luma_ps = self.profile_tier_level.max_luma_ps(); + let max_dpb_pic_buf = self.profile_tier_level.max_dpb_pic_buf(); + + // Equation A-2 + let max = if self.pic_size_in_samples_y <= (max_luma_ps >> 2) { + std::cmp::min(4 * max_dpb_pic_buf, 16) + } else if self.pic_size_in_samples_y <= (max_luma_ps >> 1) { + std::cmp::min(2 * max_dpb_pic_buf, 16) + } else if self.pic_size_in_samples_y <= ((3 * max_luma_ps) >> 2) { + std::cmp::min(4 * max_dpb_pic_buf / 3, 16) + } else { + max_dpb_pic_buf + }; + + max as usize + } + + pub fn width(&self) -> u16 { + self.pic_width_in_luma_samples + } + + pub fn height(&self) -> u16 { + self.pic_height_in_luma_samples + } + + pub fn visible_rectangle(&self) -> Rect { + // From the specification: + // NOTE 3 – The conformance cropping window offset parameters are + // only applied at the output. All internal decoding processes are + // applied to the uncropped picture size. + if !self.conformance_window_flag { + return Rect { + min: Point { x: 0, y: 0 }, + max: Point { + x: u32::from(self.width()), + y: u32::from(self.height()), + }, + }; + } + const SUB_HEIGHT_C: [u32; 5] = [1, 2, 1, 1, 1]; + const SUB_WIDTH_C: [u32; 5] = [1, 2, 2, 1, 1]; + + let crop_unit_y = SUB_HEIGHT_C[usize::from(self.chroma_array_type)]; + let crop_unit_x = SUB_WIDTH_C[usize::from(self.chroma_array_type)]; + let crop_left = crop_unit_x * self.conf_win_left_offset; + let crop_right = crop_unit_x * self.conf_win_right_offset; + let crop_top = crop_unit_y * self.conf_win_top_offset; + let crop_bottom = crop_unit_y * self.conf_win_bottom_offset; + + Rect { + min: Point { + x: crop_left, + y: crop_top, + }, + max: Point { + x: u32::from(self.width()) - crop_left - crop_right, + y: u32::from(self.height()) - crop_top - crop_bottom, + }, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PpsSccExtension { + /// When set, specifies that a picture referring to the PPS may be included + /// in a reference picture list of a slice of the picture itself. If not + /// set, specifies that a picture referring to the PPS is never included in + /// a reference picture list of a slice of the picture itself. + pub curr_pic_ref_enabled_flag: bool, + /// When set, specifies that an adaptive colour transform may be applied to + /// the residual in the decoding process. When not set, specifies that + /// adaptive colour transform is not applied to the residual. + pub residual_adaptive_colour_transform_enabled_flag: bool, + /// When set, specifies that slice_act_y_qp_offset, slice_act_cb_qp_offset, + /// slice_act_cr_qp_offset are present in the slice header. When not set, + /// specifies that slice_act_y_qp_offset, slice_act_cb_qp_offset, + /// slice_act_cr_qp_offset are not present in the slice header. + pub slice_act_qp_offsets_present_flag: bool, + /// See the specificartion for more details. + pub act_y_qp_offset_plus5: i8, + /// See the specificartion for more details. + pub act_cb_qp_offset_plus5: i8, + /// See the specificartion for more details. + pub act_cr_qp_offset_plus3: i8, + /// When set, specifies that the palette predictor initializers used for the + /// pictures referring to the PPS are derived based on the palette predictor + /// initializers specified by the PPS. If not set, specifies that the + /// palette predictor initializers used for the pictures referring to the + /// PPS are inferred to be equal to those specified by the active SPS. + pub palette_predictor_initializers_present_flag: bool, + /// Specifies the number of entries in the picture palette predictor + /// initializer. + pub num_palette_predictor_initializers: u8, + /// When set, specifies that the pictures that refer to this PPS are + /// monochrome. If not set, specifies that the pictures that refer to this + /// PPS have multiple components. + pub monochrome_palette_flag: bool, + /// luma_bit_depth_entry_minus8 plus 8 specifies the bit depth of the luma + /// component of the entries of the palette predictor initializer. + pub luma_bit_depth_entry_minus8: u8, + /// chroma_bit_depth_entry_minus8 plus 8 specifies the bit depth of the + /// chroma components of the entries of the palette predictor initializer. + pub chroma_bit_depth_entry_minus8: u8, + /// `pps_palette_predictor_initializer[ comp ][ i ]` specifies the value of + /// the comp-th component of the i-th palette entry in the PPS that is used + /// to initialize the array PredictorPaletteEntries. + pub palette_predictor_initializer: [[u8; 128]; 3], +} + +impl Default for PpsSccExtension { + fn default() -> Self { + Self { + curr_pic_ref_enabled_flag: Default::default(), + residual_adaptive_colour_transform_enabled_flag: Default::default(), + slice_act_qp_offsets_present_flag: Default::default(), + act_y_qp_offset_plus5: Default::default(), + act_cb_qp_offset_plus5: Default::default(), + act_cr_qp_offset_plus3: Default::default(), + palette_predictor_initializers_present_flag: Default::default(), + num_palette_predictor_initializers: Default::default(), + monochrome_palette_flag: Default::default(), + luma_bit_depth_entry_minus8: Default::default(), + chroma_bit_depth_entry_minus8: Default::default(), + palette_predictor_initializer: [[0; 128]; 3], + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct PpsRangeExtension { + /// log2_max_transform_skip_block_size_minus2 plus 2 specifies the maximum + /// transform block size for which transform_skip_flag may be present in + /// coded pictures referring to the PPS. When not present, the value of + /// log2_max_transform_skip_block_size_minus2 is inferred to be equal to 0. + /// When present, the value of log2_max_transform_skip_block_size_minus2 + /// shall be less than or equal to MaxTbLog2SizeY − 2. + pub log2_max_transform_skip_block_size_minus2: u32, + /// When set, specifies that log2_res_scale_abs_plus1 and + /// res_scale_sign_flag may be present in the transform unit syntax for + /// pictures referring to the PPS. When not set, specifies that + /// log2_res_scale_abs_plus1 and res_scale_sign_flag are not present for + /// pictures referring to the PPS. + pub cross_component_prediction_enabled_flag: bool, + /// When set, specifies that the cu_chroma_qp_offset_flag may be present in + /// the transform unit syntax. When not set, specifies that the + /// cu_chroma_qp_offset_flag is not present in the transform unit syntax. + pub chroma_qp_offset_list_enabled_flag: bool, + /// Specifies the difference between the luma CTB size and the minimum luma + /// coding block size of coding units that convey cu_chroma_qp_offset_flag. + pub diff_cu_chroma_qp_offset_depth: u32, + /// chroma_qp_offset_list_len_minus1 plus 1 specifies the number of + /// `cb_qp_offset_list[ i ]` and `cr_qp_offset_list[ i ]` syntax elements that + /// are present in the PPS. + pub chroma_qp_offset_list_len_minus1: u32, + /// Specify offsets used in the derivation of Qp′Cb and Qp′Cr, respectively. + pub cb_qp_offset_list: [i32; 6], + /// Specify offsets used in the derivation of Qp′Cb and Qp′Cr, respectively. + pub cr_qp_offset_list: [i32; 6], + /// The base 2 logarithm of the scaling parameter that is used to scale + /// sample adaptive offset (SAO) offset values for luma samples. + pub log2_sao_offset_scale_luma: u32, + /// The base 2 logarithm of the scaling parameter that is used to scale SAO + /// offset values for chroma samples. + pub log2_sao_offset_scale_chroma: u32, +} + +/// A H.265 Picture Parameter Set. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Pps { + /// Identifies the PPS for reference by other syntax elements. + pub pic_parameter_set_id: u8, + /// Specifies the value of sps_seq_parameter_set_id for the active SPS. + pub seq_parameter_set_id: u8, + /// When set, specifies the presence of the syntax element + /// dependent_slice_segment_flag in the slice segment headers for coded + /// pictures referring to the PPS. When not set, specifies the absence of + /// the syntax element dependent_slice_segment_flag in the slice segment + /// headers for coded pictures referring to the PPS. + pub dependent_slice_segments_enabled_flag: bool, + /// When set, indicates that the pic_output_flag syntax element is present + /// in the associated slice headers. When not set, indicates that the + /// pic_output_flag syntax element is not present in the associated slice + /// headers. + pub output_flag_present_flag: bool, + /// Specifies the number of extra slice header bits that are present in the + /// slice header RBSP for coded pictures referring to the PPS. + pub num_extra_slice_header_bits: u8, + /// When not set, specifies that sign bit hiding is disabled. Whens set, + /// specifies that sign bit hiding is enabled. + pub sign_data_hiding_enabled_flag: bool, + /// When set, specifies that cabac_init_flag is present in slice headers + /// referring to the PPS. When not set, specifies that cabac_init_flag is + /// not present in slice headers referring to the PPS. + pub cabac_init_present_flag: bool, + /// Specifies the inferred value of num_ref_idx_l0_active_minus1 for P and B + /// slices with num_ref_idx_active_override_flag not set. + pub num_ref_idx_l0_default_active_minus1: u8, + /// Specifies the inferred value of num_ref_idx_l1_active_minus1 for B + /// slices with num_ref_idx_active_override_flag not set. + pub num_ref_idx_l1_default_active_minus1: u8, + /// init_qp_minus26 plus 26 specifies the initial value of SliceQpY for each + /// slice referring to the PPS. The initial value of SliceQpY is modified at + /// the slice segment layer when a non-zero value of slice_qp_delta is + /// decoded. + pub init_qp_minus26: i8, + /// When not set, specifies that intra prediction allows usage of residual + /// data and decoded samples of neighbouring coding blocks coded using + /// either intra or inter prediction modes. When set, specifies constrained + /// intra prediction, in which case intra prediction only uses residual data + /// and decoded samples from neighbouring coding blocks coded using intra + /// prediction modes. + pub constrained_intra_pred_flag: bool, + /// When set, specifies that transform_skip_flag may be present in the + /// residual coding syntax. When not set, specifies that transform_skip_flag + /// is not present in the residual coding syntax. + pub transform_skip_enabled_flag: bool, + /// When set, specifies that the diff_cu_qp_delta_depth syntax element is + /// present in the PPS and that cu_qp_delta_abs may be present in the + /// transform unit syntax and the palette syntax. When not set, specifies + /// that the diff_cu_qp_delta_depth syntax element is not present in the PPS + /// and that cu_qp_delta_abs is not present in the transform unit syntax and + /// the palette syntax. + pub cu_qp_delta_enabled_flag: bool, + + /*if cu_qp_delta_enabled_flag */ + /// Specifies the difference between the luma CTB size and the minimum luma + /// coding block size of coding units that convey cu_qp_delta_abs and + /// cu_qp_delta_sign_flag. + pub diff_cu_qp_delta_depth: u8, + /// Specifies the offsets to the luma quantization parameter Qp′Y used for + /// deriving Qp′Cb and Qp′Cr, respectively. + pub cb_qp_offset: i8, + /// Specifies the offsets to the luma quantization parameter Qp′Y used for + /// deriving Qp′Cb and Qp′Cr, respectively. + pub cr_qp_offset: i8, + /// When set, indicates that the slice_cb_qp_offset and slice_cr_qp_offset + /// syntax elements are present in the associated slice headers. When not + /// set, indicates that these syntax elements are not present in the + /// associated slice headers. When ChromaArrayType is equal to 0, + /// pps_slice_chroma_qp_offsets_present_flag shall be equal to 0 + pub slice_chroma_qp_offsets_present_flag: bool, + /// When not set, specifies that weighted prediction is not applied to P + /// slices. When set, specifies that weighted prediction is applied to P + /// slices. + pub weighted_pred_flag: bool, + /// When not set, specifies that the default weighted prediction is applied + /// to B slices. When set, specifies that weighted prediction is applied to + /// B slices. + pub weighted_bipred_flag: bool, + /// When set, specifies that `cu_transquant_bypass_flag` is present, When + /// not set, specifies that `cu_transquant_bypass_flag` is not present. + pub transquant_bypass_enabled_flag: bool, + /// When set, specifies that there is more than one tile in each picture + /// referring to the PPS. When not set, specifies that there is only one + /// tile in each picture referring to the PPS. + pub tiles_enabled_flag: bool, + /// When set, specifies that a specific synchronization process for context + /// variables, and when applicable, Rice parameter initialization states and + /// palette predictor variables, is invoked before decoding the CTU which + /// includes the first CTB of a row of CTBs in each tile in each picture + /// referring to the PPS, and a specific storage process for context + /// variables, and when applicable, Rice parameter initialization states and + /// palette predictor variables, is invoked after decoding the CTU which + /// includes the second CTB of a row of CTBs in each tile in each picture + /// referring to the PPS. When not set, specifies that no specific + /// synchronization process for context variables, and when applicable, Rice + /// parameter initialization states and palette predictor variables, is + /// required to be invoked before decoding the CTU which includes the first + /// CTB of a row of CTBs in each tile in each picture referring to the PPS, + /// and no specific storage process for context variables, and when + /// applicable, Rice parameter initialization states and palette predictor + /// variables, is required to be invoked after decoding the CTU which + /// includes the second CTB of a row of CTBs in each tile in each picture + /// referring to the PPS. + pub entropy_coding_sync_enabled_flag: bool, + /// num_tile_columns_minus1 plus 1 specifies the number of tile columns + /// partitioning the picture. + pub num_tile_columns_minus1: u8, + /// num_tile_rows_minus1 plus 1 specifies the number of tile rows + /// partitioning the picture. + pub num_tile_rows_minus1: u8, + /// When set, specifies that tile column boundaries and likewise tile row + /// boundaries are distributed uniformly across the picture. When not set, + /// specifies that tile column boundaries and likewise tile row boundaries + /// are not distributed uniformly across the picture but signalled + /// explicitly using the syntax elements `column_width_minus1[ i ]` and + /// `row_height_minus1[ i ]`. + pub uniform_spacing_flag: bool, + /// `column_width_minus1[ i ]` plus 1 specifies the width of the i-th tile + /// column in units of CTBs. + pub column_width_minus1: [u32; 19], + /// `row_height_minus1[ i ]` plus 1 specifies the height of the i-th tile row + /// in units of CTBs. + pub row_height_minus1: [u32; 21], + /// When set, specifies that in-loop filtering operations may be performed + /// across tile boundaries in pictures referring to the PPS. When not set, + /// specifies that in-loop filtering operations are not performed across + /// tile boundaries in pictures referring to the PPS. The in-loop filtering + /// operations include the deblocking filter and sample adaptive offset + /// filter operations. + pub loop_filter_across_tiles_enabled_flag: bool, + /// When set, specifies that in-loop filtering operations may be performed + /// across left and upper boundaries of slices referring to the PPS. When + /// not set, specifies that in-loop filtering operations are not performed + /// across left and upper boundaries of slices referring to the PPS. The in- + /// loop filtering operations include the deblocking filter and sample + /// adaptive offset filter operations. + pub loop_filter_across_slices_enabled_flag: bool, + /// When set, specifies the presence of deblocking filter control syntax + /// elements in the PPS. When not set, specifies the absence of deblocking + /// filter control syntax elements in the PPS. + pub deblocking_filter_control_present_flag: bool, + /// When set, specifies the presence of deblocking_filter_override_flag in + /// the slice headers for pictures referring to the PPS. When not set, + /// specifies the absence of deblocking_filter_override_flag in the slice + /// headers for pictures referring to the PPS. + pub deblocking_filter_override_enabled_flag: bool, + /// When set, specifies that the operation of deblocking filter is not + /// applied for slices referring to the PPS in which + /// slice_deblocking_filter_disabled_flag is not present. When not set, + /// specifies that the operation of the deblocking filter is applied for + /// slices referring to the PPS in which + /// slice_deblocking_filter_disabled_flag is not present. + pub deblocking_filter_disabled_flag: bool, + /// Specify the default deblocking parameter offsets for β and tC (divided + /// by 2) that are applied for slices referring to the PPS, unless the + /// default deblocking parameter offsets are overridden by the deblocking + /// parameter offsets present in the slice headers of the slices referring + /// to the PPS. + pub beta_offset_div2: i8, + /// Specify the default deblocking parameter offsets for β and tC (divided + /// by 2) that are applied for slices referring to the PPS, unless the + /// default deblocking parameter offsets are overridden by the deblocking + /// parameter offsets present in the slice headers of the slices referring + /// to the PPS. + pub tc_offset_div2: i8, + /// When set, specifies that the scaling list data used for the pictures + /// referring to the PPS are derived based on the scaling lists specified by + /// the active SPS and the scaling lists specified by the PPS. + /// pps_scaling_list_data_present_flag equal to 0 specifies that the scaling + /// list data used for the pictures referring to the PPS are inferred to be + /// equal to those specified by the active SPS. + pub scaling_list_data_present_flag: bool, + /// The scaling list data. + pub scaling_list: ScalingLists, + /// When set, specifies that the syntax structure + /// ref_pic_lists_modification( ) is present in the slice segment header. + /// When not set, specifies that the syntax structure + /// ref_pic_lists_modification( ) is not present in the slice segment header + pub lists_modification_present_flag: bool, + /// log2_parallel_merge_level_minus2 plus 2 specifies the value of the + /// variable Log2ParMrgLevel, which is used in the derivation process for + /// luma motion vectors for merge mode as specified in clause 8.5.3.2.2 and + /// the derivation process for spatial merging candidates as specified in + /// clause 8.5.3.2.3. + pub log2_parallel_merge_level_minus2: u8, + /// When not set, specifies that no slice segment header extension syntax + /// elements are present in the slice segment headers for coded pictures + /// referring to the PPS. When set, specifies that slice segment header + /// extension syntax elements are present in the slice segment headers for + /// coded pictures referring to the PPS. + pub slice_segment_header_extension_present_flag: bool, + /// When set, specifies that the syntax elements pps_range_extension_flag, + /// pps_multilayer_extension_flag, pps_3d_extension_flag, + /// pps_scc_extension_flag, and pps_extension_4bits are present in the + /// picture parameter set RBSP syntax structure. When not set, specifies + /// that these syntax elements are not present. + pub extension_present_flag: bool, + /// When setspecifies that the pps_range_extension( ) syntax structure is + /// present in the PPS RBSP syntax structure. When not set, specifies that + /// this syntax structure is not present. + pub range_extension_flag: bool, + /// The range extension data. + pub range_extension: PpsRangeExtension, + + pub scc_extension_flag: bool, + /// The SCC extension data. + pub scc_extension: PpsSccExtension, + + // Internal variables. + /// Equivalent to QpBdOffsetY in the specification. + pub qp_bd_offset_y: u32, + + /// The SPS referenced by this PPS. + pub sps: Rc, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ScalingLists { + /// plus 8 specifies the value of the variable `ScalingFactor[ 2 ][ matrixId + /// ] [ 0 ][ 0 ]` for the scaling list for the 16x16 size. + pub scaling_list_dc_coef_minus8_16x16: [i16; 6], + /// plus 8 specifies the value of the variable `ScalingFactor[ 3 ][ matrixId + /// ][ 0 ][ 0 ]` for the scaling list for the 32x32 size. + pub scaling_list_dc_coef_minus8_32x32: [i16; 6], + /// The 4x4 scaling list. + pub scaling_list_4x4: [[u8; 16]; 6], + /// The 8x8 scaling list. + pub scaling_list_8x8: [[u8; 64]; 6], + /// The 16x16 scaling list. + pub scaling_list_16x16: [[u8; 64]; 6], + /// The 32x32 scaling list. + pub scaling_list_32x32: [[u8; 64]; 6], +} + +impl Default for ScalingLists { + fn default() -> Self { + Self { + scaling_list_dc_coef_minus8_16x16: Default::default(), + scaling_list_dc_coef_minus8_32x32: Default::default(), + scaling_list_4x4: Default::default(), + scaling_list_8x8: [[0; 64]; 6], + scaling_list_16x16: [[0; 64]; 6], + scaling_list_32x32: [[0; 64]; 6], + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RefPicListModification { + /// Whenset, indicates that reference picture list 0 is specified explicitly + /// by a list of `list_entry_l0[ i ]` values. When not set, indicates that + /// reference picture list 0 is determined implicitly. + pub ref_pic_list_modification_flag_l0: bool, + /// `list_entry_l0[ i ]` specifies the index of the reference picture in + /// RefPicListTemp0 to be placed at the current position of reference + /// picture list 0. + pub list_entry_l0: Vec, + /// Whenset, indicates that reference picture list 1 is specified explicitly + /// by a list of `list_entry_l1[ i ]` values. When not set, indicates that + /// reference picture list 1 is determined implicitly. + pub ref_pic_list_modification_flag_l1: bool, + /// `list_entry_l1[ i ]` specifies the index of the reference picture in + /// RefPicListTemp1 to be placed at the current position of reference + /// picture list 1. + pub list_entry_l1: Vec, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct PredWeightTable { + /// The base 2 logarithm of the denominator for all luma weighting factors. + pub luma_log2_weight_denom: u8, + /// The difference of the base 2 logarithm of the denominator for all chroma + /// weighting factors. + pub delta_chroma_log2_weight_denom: i8, + /// `luma_weight_l0_flag[ i ]` set specifies that weighting factors for the + /// luma component of list 0 prediction using `RefPicList0[ i ]` are present. + /// `luma_weight_l0_flag[ i ]` not set specifies that these weighting factors + /// are not present. + pub luma_weight_l0_flag: [bool; 15], + /// `chroma_weight_l0_flag[ i ]` set specifies that weighting factors for the + /// chroma prediction values of list 0 prediction using `RefPicList0[ i ]` are + /// present. `chroma_weight_l0_flag[ i ]` not set specifies that these + /// weighting factors are not present. + pub chroma_weight_l0_flag: [bool; 15], + /// `delta_luma_weight_l0[ i ]` is the difference of the weighting factor + /// applied to the luma prediction value for list 0 prediction using + /// `RefPicList0[ i ]`. + pub delta_luma_weight_l0: [i8; 15], + /// `luma_offset_l0[ i ]` is the additive offset applied to the luma + /// prediction value for list 0 prediction using `RefPicList0[ i ]`. + pub luma_offset_l0: [i8; 15], + /// `delta_chroma_weight_l0[ i ][ j ]` is the difference of the weighting + /// factor applied to the chroma prediction values for list 0 prediction + /// using `RefPicList0[ i ]` with j equal to 0 for Cb and j equal to 1 for Cr. + pub delta_chroma_weight_l0: [[i8; 2]; 15], + /// `delta_chroma_offset_l0[ i ][ j ]` is the difference of the additive + /// offset applied to the chroma prediction values for list 0 prediction + /// using `RefPicList0[ i ]` with j equal to 0 for Cb and j equal to 1 for Cr. + pub delta_chroma_offset_l0: [[i16; 2]; 15], + + // `luma_weight_l1_flag[ i ]`, `chroma_weight_l1_flag[ i ]`, + // `delta_luma_weight_l1[ i ]`, `luma_offset_l1[ i ]`, delta_chroma_weight_l1[ i + // `][ j ]` and `delta_chroma_offset_l1[ i ]`[ j ] have the same + // `semanticsasluma_weight_l0_flag[ i ]`, `chroma_weight_l0_flag[ i ]`, + // `delta_luma_weight_l0[ i ]`, `luma_offset_l0[ i ]`, `delta_chroma_weight_l0[ i + // ][ j ]` and `delta_chroma_offset_l0[ i ][ j ]`, respectively, with `l0`, `L0`, + // `list 0` and `List0` replaced by `l1`, `L1`, `list 1` and `List1`, respectively. + pub luma_weight_l1_flag: [bool; 15], + pub chroma_weight_l1_flag: [bool; 15], + pub delta_luma_weight_l1: [i8; 15], + pub luma_offset_l1: [i8; 15], + + pub delta_chroma_weight_l1: [[i8; 2]; 15], + pub delta_chroma_offset_l1: [[i16; 2]; 15], + + // Calculated. + /// Same as ChromaLog2WeightDenom in the specification. + pub chroma_log2_weight_denom: u8, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ShortTermRefPicSet { + /// When set, specifies that the stRpsIdx-th candidate short-term RPS is + /// predicted from another candidate short-term RPS, which is referred to as + /// the source candidate short-term RPS. + pub inter_ref_pic_set_prediction_flag: bool, + /// delta_idx_minus1 plus 1 specifies the difference between the value of + /// stRpsIdx and the index, into the list of the candidate short-term RPSs + /// specified in the SPS, of the source candidate short-term RPS. + pub delta_idx_minus1: u8, + /// delta_rps_sign and abs_delta_rps_minus1 together specify the value of + /// the variable deltaRps. + pub delta_rps_sign: bool, + /// delta_rps_sign and abs_delta_rps_minus1 together specify the value of + /// the variable deltaRps. + pub abs_delta_rps_minus1: u16, + /// specifies the number of entries in the stRpsIdx-th candidate short-term + /// RPS that have picture order count values less than the picture order + /// count value of the current picture. + pub num_negative_pics: u8, + /// specifies the number of entries in the stRpsIdx-th candidate short-term + /// RPS that have picture order count values greater than the picture order + /// count value of the current picture. + pub num_positive_pics: u8, + /// Same as UsedByCurrPicS0 in the specification. + pub used_by_curr_pic_s0: [bool; MAX_SHORT_TERM_REF_PIC_SETS], + /// Same as UsedByCurrPicS1 in the specification. + pub used_by_curr_pic_s1: [bool; MAX_SHORT_TERM_REF_PIC_SETS], + /// Same as DeltaPocS0 in the specification. + pub delta_poc_s0: [i32; MAX_SHORT_TERM_REF_PIC_SETS], + /// Same as DeltaPocS1 in the specification. + pub delta_poc_s1: [i32; MAX_SHORT_TERM_REF_PIC_SETS], + /// Same as NumDeltaPocs in the specification. + pub num_delta_pocs: u32, +} + +impl Default for ShortTermRefPicSet { + fn default() -> Self { + Self { + inter_ref_pic_set_prediction_flag: Default::default(), + delta_idx_minus1: Default::default(), + delta_rps_sign: Default::default(), + abs_delta_rps_minus1: Default::default(), + num_negative_pics: Default::default(), + num_positive_pics: Default::default(), + used_by_curr_pic_s0: [false; MAX_SHORT_TERM_REF_PIC_SETS], + used_by_curr_pic_s1: [false; MAX_SHORT_TERM_REF_PIC_SETS], + delta_poc_s0: [0; MAX_SHORT_TERM_REF_PIC_SETS], + delta_poc_s1: [0; MAX_SHORT_TERM_REF_PIC_SETS], + num_delta_pocs: Default::default(), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// See table 7-7 in the specification. +pub enum SliceType { + B = 0, + P = 1, + I = 2, +} + +impl TryFrom for SliceType { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(SliceType::B), + 1 => Ok(SliceType::P), + 2 => Ok(SliceType::I), + _ => Err(format!("Invalid SliceType {}", value)), + } + } +} + +impl SliceType { + /// Whether this is a P slice. See table 7-7 in the specification. + pub fn is_p(&self) -> bool { + matches!(self, SliceType::P) + } + + /// Whether this is a B slice. See table 7-7 in the specification. + pub fn is_b(&self) -> bool { + matches!(self, SliceType::B) + } + + /// Whether this is an I slice. See table 7-7 in the specification. + pub fn is_i(&self) -> bool { + matches!(self, SliceType::I) + } +} + +impl Default for SliceType { + fn default() -> Self { + Self::P + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SliceHeader { + /// When set, specifies that the slice segment is the first slice segment of + /// the picture in decoding order. When not set, specifies that the slice + /// segment is not the first slice segment of the picture in decoding order. + pub first_slice_segment_in_pic_flag: bool, + /// Affects the output of previously-decoded pictures in the decoded picture + /// buffer after the decoding of an IDR or a BLA picture that is not the + /// first picture in the bitstream as specified in Annex C. + pub no_output_of_prior_pics_flag: bool, + /// Specifies the value of pps_pic_parameter_set_id for the PPS in use. + pub pic_parameter_set_id: u8, + /// When set, specifies that the value of each slice segment header syntax + /// element that is not present is inferred to be equal to the value of the + /// corresponding slice segment header syntax element in the slice header. + pub dependent_slice_segment_flag: bool, + /// Specifies the address of the first CTB in the slice segment, in CTB + /// raster scan of a picture. + pub segment_address: u32, + /// Specifies the coding type of the slice according to Table 7-7. + pub type_: SliceType, + /// Affects the decoded picture output and removal processes as specified in + /// Annex C. + pub pic_output_flag: bool, + /// Specifies the colour plane associated with the current slice RBSP when + /// separate_colour_plane_flag is set. The value of colour_plane_id shall be + /// in the range of 0 to 2, inclusive. colour_plane_id values 0, 1 and 2 + /// correspond to the Y, Cb and Cr planes, respectively. + pub colour_plane_id: u8, + /// Specifies the picture order count modulo MaxPicOrderCntLsb for the + /// current picture. The length of the slice_pic_order_cnt_lsb syntax + /// element is log2_max_pic_order_cnt_lsb_minus4 + 4 bits. + pub pic_order_cnt_lsb: u16, + /// When set, specifies that the short-term RPS of the current picture is + /// derived based on one of the st_ref_pic_set( ) syntax structures in the + /// active SPS that is identified by the syntax element + /// short_term_ref_pic_set_idx in the slice header. When not set, specifies + /// that the short-term RPS of the current picture is derived based on the + /// st_ref_pic_set( ) syntax structure that is directly included in the + /// slice headers of the current picture. + pub short_term_ref_pic_set_sps_flag: bool, + /// The st_ref_pic_set() data. + pub short_term_ref_pic_set: ShortTermRefPicSet, + /// Specifies the index, into the list of the st_ref_pic_set( ) syntax + /// structures included in the active SPS, of the st_ref_pic_set( ) syntax + /// structure that is used for derivation of the short-term RPS of the + /// current picture. + pub short_term_ref_pic_set_idx: u8, + /// Specifies the number of entries in the long-term RPS of the current + /// picture that are derived based on the candidate long-term reference + /// pictures specified in the active SPS. + pub num_long_term_sps: u8, + /// Specifies the number of entries in the long-term RPS of the current + /// picture that are directly signalled in the slice header. + pub num_long_term_pics: u8, + /// `lt_idx_sps[ i ]` specifies an index, into the list of candidate long-term + /// reference pictures specified in the active SPS, of the i-th entry in the + /// long-term RPS of the current picture. + pub lt_idx_sps: [u8; 16], + /// Same as PocLsbLt in the specification. + pub poc_lsb_lt: [u32; 16], + /// Same as UsedByCurrPicLt in the specification. + pub used_by_curr_pic_lt: [bool; 16], + /// When set, specifies that that `delta_poc_msb_cycle_lt[i]` is present. + pub delta_poc_msb_present_flag: [bool; 16], + /// Same as DeltaPocMsbCycleLt in the specification. + pub delta_poc_msb_cycle_lt: [u32; 16], + /// Specifies whether temporal motion vector predictors can be used for + /// inter prediction. If slice_temporal_mvp_enabled_flag is not set, the + /// syntax elements of the current picture shall be constrained such that no + /// temporal motion vector predictor is used in decoding of the current + /// picture. Otherwise (slice_temporal_mvp_enabled_flag is set), temporal + /// motion vector predictors may be used in decoding of the current picture. + pub temporal_mvp_enabled_flag: bool, + /// When set, specifies that SAO is enabled for the luma component in the + /// current slice; slice_sao_luma_flag not set specifies that SAO is + /// disabled for the luma component in the current slice. + pub sao_luma_flag: bool, + /// When set, specifies that SAO is enabled for the chroma component in the + /// current slice; When not set, specifies that SAO is disabled for the + /// chroma component in the current slice. + pub sao_chroma_flag: bool, + /// When set, specifies that the syntax element num_ref_idx_l0_active_minus1 + /// is present for P and B slices and that the syntax element + /// num_ref_idx_l1_active_minus1 is present for B slices. When not set, + /// specifies that the syntax elements num_ref_idx_l0_active_minus1 and + /// num_ref_idx_l1_active_minus1 are not present. + pub num_ref_idx_active_override_flag: bool, + /// Specifies the maximum reference index for + /// reference picture list 0 that may be used to decode the slice. + pub num_ref_idx_l0_active_minus1: u8, + /// Specifies the maximum reference index for reference picture list 1 that + /// may be used to decode the slice. + pub num_ref_idx_l1_active_minus1: u8, + /// The RefPicListModification data. + pub ref_pic_list_modification: RefPicListModification, + /// When set, indicates that the mvd_coding( x0, y0, 1 ) syntax structure is + /// not parsed and `MvdL1[ x0 ]`[ y0 `][ compIdx ]` is set equal to 0 for + /// compIdx = 0..1. When not set, indicates that the mvd_coding( x0, y0, 1 ) + /// syntax structure is parsed. + pub mvd_l1_zero_flag: bool, + /// Specifies the method for determining the initialization table used in + /// the initialization process for context variables. + pub cabac_init_flag: bool, + /// When set, specifies that the collocated picture used for temporal motion + /// vector prediction is derived from reference picture list 0. When not + /// set, specifies that the collocated picture used for temporal motion + /// vector prediction is derived from reference picture list 1. + pub collocated_from_l0_flag: bool, + /// Specifies the reference index of the collocated picture used for + /// temporal motion vector prediction. + pub collocated_ref_idx: u8, + /// The PredWeightTable data. + pub pred_weight_table: PredWeightTable, + /// Specifies the maximum number of merging motion vector prediction (MVP) + /// candidates supported in the slice subtracted from 5. + pub five_minus_max_num_merge_cand: u8, + /// Specifies that the resolution of motion vectors for inter prediction in + /// the current slice is integer. When not set, specifies + /// that the resolution of motion vectors for inter prediction in the + /// current slice that refer to pictures other than the current picture is + /// fractional with quarter-sample precision in units of luma samples. + pub use_integer_mv_flag: bool, + /// Specifies the initial value of QpY to be used for the coding blocks in + /// the slice until modified by the value of CuQpDeltaVal in the coding unit + /// layer. + pub qp_delta: i8, + /// Specifies a difference to be added to the value of pps_cb_qp_offset when + /// determining the value of the Qp′Cb quantization parameter. + pub cb_qp_offset: i8, + /// Specifies a difference to be added to the value of pps_cb_qr_offset when + /// determining the value of the Qp′Cr quantization parameter. + pub cr_qp_offset: i8, + /// Specifies offsets to the quantization parameter values qP derived in + /// clause 8.6.2 for luma, Cb, and Cr components, respectively. + pub slice_act_y_qp_offset: i8, + /// Specifies offsets to the quantization parameter values qP derived in + /// clause 8.6.2 for luma, Cb, and Cr components, respectively. + pub slice_act_cb_qp_offset: i8, + /// Specifies offsets to the quantization parameter values qP derived in + /// clause 8.6.2 for luma, Cb, and Cr components, respectively. + pub slice_act_cr_qp_offset: i8, + /// When set, specifies that the cu_chroma_qp_offset_flag may be present in + /// the transform unit syntax. When not set, specifies that the + /// cu_chroma_qp_offset_flag is not present in the transform unit syntax. + pub cu_chroma_qp_offset_enabled_flag: bool, + /// When set, specifies that deblocking parameters are present in the slice + /// header. When not set, specifies that deblocking parameters are not + /// present in the slice header. + pub deblocking_filter_override_flag: bool, + /// When set, specifies that the operation of the deblocking filter is not + /// applied for the current slice. When not set, specifies that the + /// operation of the deblocking filter is applied for the current slice. + pub deblocking_filter_disabled_flag: bool, + /// Specifies the deblocking parameter offsets for β and tC (divided by 2) + /// for the current slice. + pub beta_offset_div2: i8, + /// Specifies the deblocking parameter offsets for β and tC (divided by 2) + /// for the current slice. + pub tc_offset_div2: i8, + /// When set, specifies that in-loop filtering operations may be performed + /// across the left and upper boundaries of the current slice. When not + /// set, specifies that in-loop operations are not performed across left and + /// upper boundaries of the current slice. The in-loop filtering operations + /// include the deblocking filter and sample adaptive offset filter. + pub loop_filter_across_slices_enabled_flag: bool, + /// Specifies the number of `entry_point_offset_minus1[ i ]` syntax elements + /// in the slice header. + pub num_entry_point_offsets: u32, + /// offset_len_minus1 plus 1 specifies the length, in bits, of the + /// `entry_point_offset_minus1[ i ]` syntax elements. + pub offset_len_minus1: u8, + /// `entry_point_offset_minus1[ i ]` plus 1 specifies the i-th entry point + /// offset in bytes, and is represented by offset_len_minus1 plus 1 bits. + /// The slice segment data that follow the slice segment header consists of + /// num_entry_point_offsets + 1 subsets, with subset index values ranging + /// from 0 to num_entry_point_offsets, inclusive. See the specification for + /// more details. + pub entry_point_offset_minus1: [u32; 32], + /// Same as NumPicTotalCurr in the specification. + pub num_pic_total_curr: u32, + // Size of slice_header() in bits. + pub header_bit_size: u32, + // Number of emulation prevention bytes (EPB) in this slice_header(). + pub n_emulation_prevention_bytes: u32, + /// Same as CurrRpsIdx in the specification. + pub curr_rps_idx: u8, + /// Number of bits taken by st_ref_pic_set minus Emulation Prevention Bytes. + pub st_rps_bits: u32, +} + +impl Default for SliceHeader { + fn default() -> Self { + Self { + first_slice_segment_in_pic_flag: Default::default(), + no_output_of_prior_pics_flag: Default::default(), + pic_parameter_set_id: Default::default(), + dependent_slice_segment_flag: Default::default(), + segment_address: Default::default(), + type_: Default::default(), + pic_output_flag: true, + colour_plane_id: Default::default(), + pic_order_cnt_lsb: Default::default(), + short_term_ref_pic_set_sps_flag: Default::default(), + short_term_ref_pic_set: Default::default(), + short_term_ref_pic_set_idx: Default::default(), + num_long_term_sps: Default::default(), + num_long_term_pics: Default::default(), + lt_idx_sps: Default::default(), + poc_lsb_lt: Default::default(), + used_by_curr_pic_lt: Default::default(), + delta_poc_msb_present_flag: Default::default(), + delta_poc_msb_cycle_lt: Default::default(), + temporal_mvp_enabled_flag: Default::default(), + sao_luma_flag: Default::default(), + sao_chroma_flag: Default::default(), + num_ref_idx_active_override_flag: Default::default(), + num_ref_idx_l0_active_minus1: Default::default(), + num_ref_idx_l1_active_minus1: Default::default(), + ref_pic_list_modification: Default::default(), + mvd_l1_zero_flag: Default::default(), + cabac_init_flag: Default::default(), + collocated_from_l0_flag: true, + collocated_ref_idx: Default::default(), + pred_weight_table: Default::default(), + five_minus_max_num_merge_cand: Default::default(), + use_integer_mv_flag: Default::default(), + qp_delta: Default::default(), + cb_qp_offset: Default::default(), + cr_qp_offset: Default::default(), + slice_act_y_qp_offset: Default::default(), + slice_act_cb_qp_offset: Default::default(), + slice_act_cr_qp_offset: Default::default(), + cu_chroma_qp_offset_enabled_flag: Default::default(), + deblocking_filter_override_flag: Default::default(), + deblocking_filter_disabled_flag: Default::default(), + beta_offset_div2: Default::default(), + tc_offset_div2: Default::default(), + loop_filter_across_slices_enabled_flag: Default::default(), + num_entry_point_offsets: Default::default(), + offset_len_minus1: Default::default(), + entry_point_offset_minus1: Default::default(), + num_pic_total_curr: Default::default(), + header_bit_size: Default::default(), + n_emulation_prevention_bytes: Default::default(), + curr_rps_idx: Default::default(), + st_rps_bits: Default::default(), + } + } +} + +/// A H265 slice. An integer number of macroblocks or macroblock pairs ordered +/// consecutively in the raster scan within a particular slice group +pub struct Slice<'a> { + /// The slice header. + pub header: SliceHeader, + /// The NAL unit backing this slice. + pub nalu: Nalu<'a>, +} + +impl<'a> Slice<'a> { + /// Sets the header for dependent slices by copying from an independent + /// slice. + pub fn replace_header(&mut self, header: SliceHeader) -> Result<(), String> { + if !self.header.dependent_slice_segment_flag { + Err("Replacing the slice header is only possible for dependent slices".into()) + } else { + let first_slice_segment_in_pic_flag = self.header.first_slice_segment_in_pic_flag; + let no_output_of_prior_pics_flag = self.header.no_output_of_prior_pics_flag; + let pic_parameter_set_id = self.header.pic_parameter_set_id; + let dependent_slice_segment_flag = self.header.dependent_slice_segment_flag; + let segment_address = self.header.segment_address; + + let offset_len_minus1 = self.header.offset_len_minus1; + let entry_point_offset_minus1 = self.header.entry_point_offset_minus1; + let num_pic_total_curr = self.header.num_pic_total_curr; + let header_bit_size = self.header.header_bit_size; + let n_emulation_prevention_bytes = self.header.n_emulation_prevention_bytes; + let curr_rps_idx = self.header.curr_rps_idx; + let st_rps_bits = self.header.st_rps_bits; + + self.header = header; + + self.header.first_slice_segment_in_pic_flag = first_slice_segment_in_pic_flag; + self.header.no_output_of_prior_pics_flag = no_output_of_prior_pics_flag; + self.header.pic_parameter_set_id = pic_parameter_set_id; + self.header.dependent_slice_segment_flag = dependent_slice_segment_flag; + self.header.segment_address = segment_address; + self.header.offset_len_minus1 = offset_len_minus1; + self.header.entry_point_offset_minus1 = entry_point_offset_minus1; + self.header.num_pic_total_curr = num_pic_total_curr; + self.header.header_bit_size = header_bit_size; + self.header.n_emulation_prevention_bytes = n_emulation_prevention_bytes; + self.header.curr_rps_idx = curr_rps_idx; + self.header.st_rps_bits = st_rps_bits; + + Ok(()) + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SublayerHrdParameters { + // NOTE: The value of CpbCnt is cpb_cnt_minus1[i] + 1, and cpb_cnt_minus1 + // ranges from 0..=31 + /// `bit_rate_value_minus1[ i ]` (together with bit_rate_scale) specifies the + /// maximum input bit rate for the i-th CPB when the CPB operates at the + /// access unit level + pub bit_rate_value_minus1: [u32; 32], + /// `cpb_size_value_minus1[ i ]` is used together with cpb_size_scale to + /// specify the i-th CPB size when the CPB operates at the access unit + /// level. + pub cpb_size_value_minus1: [u32; 32], + /// `cpb_size_du_value_minus1[ i ]` is used together with cpb_size_du_scale to + /// specify the i-th CPB size when the CPB operates at sub-picture level. + pub cpb_size_du_value_minus1: [u32; 32], + /// `bit_rate_du_value_minus1[ i ]` (together with bit_rate_scale) specifies + /// the maximum input bit rate for the i-th CPB when the CPB operates at the + /// sub-picture level. + pub bit_rate_du_value_minus1: [u32; 32], + /// `cbr_flag[ i ]` not set specifies that to decode this CVS by the HRD using + /// the i-th CPB specification. + pub cbr_flag: [bool; 32], +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HrdParams { + /// When set, specifies that NAL HRD parameters (pertaining to the Type II + /// bitstream conformance point) are present in the hrd_parameters( ) syntax + /// structure. When not set, specifies that NAL HRD parameters are not + /// present in the hrd_parameters( ) syntax structure. + pub nal_hrd_parameters_present_flag: bool, + /// When set, specifies that VCL HRD parameters (pertaining to the Type I + /// bitstream conformance point) are present in the hrd_parameters( ) syntax + /// structure. When not set, specifies that VCL HRD parameters are not + /// present in the hrd_parameters( ) syntax structure. + pub vcl_hrd_parameters_present_flag: bool, + /// When set, specifies that sub-picture level HRD parameters are present + /// and the HRD may operate at access unit level or sub-picture level. When + /// not set, specifies that sub-picture level HRD parameters are not present + /// and the HRD operates at access unit level. + pub sub_pic_hrd_params_present_flag: bool, + /// Used to specify the clock sub-tick. A clock sub-tick is the minimum + /// interval of time that can be represented in the coded data when + /// sub_pic_hrd_params_present_flag is set. + pub tick_divisor_minus2: u8, + /// du_cpb_removal_delay_increment_length_minus1 plus 1 specifies the + /// length, in bits, of the `du_cpb_removal_delay_increment_minus1[ i ]` and + /// du_common_cpb_removal_delay_increment_minus1 syntax elements of the + /// picture timing SEI message and the du_spt_cpb_removal_delay_increment + /// syntax element in the decoding unit information SEI message. + pub du_cpb_removal_delay_increment_length_minus1: u8, + /// When set, specifies that sub-picture level CPB removal delay parameters + /// are present in picture timing SEI messages and no decoding unit + /// information SEI message is available (in the CVS or provided through + /// external means not specified in this Specification). When not set, + /// specifies that sub-picture level CPB removal delay parameters are + /// present in decoding unit information SEI messages and picture timing SEI + /// messages do not include sub-picture level CPB removal delay parameters. + pub sub_pic_cpb_params_in_pic_timing_sei_flag: bool, + /// dpb_output_delay_du_length_minus1 plus 1 specifies the length, in bits, + /// of the pic_dpb_output_du_delay syntax element in the picture timing SEI + /// message and the pic_spt_dpb_output_du_delay syntax element in the + /// decoding unit information SEI message. + pub dpb_output_delay_du_length_minus1: u8, + /// Together with `bit_rate_value_minus1[ i ]`, specifies the maximum input + /// bit rate of the i-th CPB. + pub bit_rate_scale: u8, + /// Together with `cpb_size_du_value_minus1[ i ]`, specifies the CPB size of + /// the i-th CPB when the CPB operates at sub-picture level. + pub cpb_size_scale: u8, + /// Together with `cpb_size_du_value_minus1[ i ]`, specifies the CPB size of + /// the i-th CPB when the CPB operates at sub-picture level. + pub cpb_size_du_scale: u8, + /// initial_cpb_removal_delay_length_minus1 plus 1 specifies the length, in + /// bits, of the `nal_initial_cpb_removal_delay[ i ]`, + /// `nal_initial_cpb_removal_offset[ i ]`, `vcl_initial_cpb_removal_delay[ i ]` + /// and `vcl_initial_cpb_removal_offset[ i ]` syntax elements of the buffering + /// period SEI message. + pub initial_cpb_removal_delay_length_minus1: u8, + /// au_cpb_removal_delay_length_minus1 plus 1 specifies the length, in bits, + /// of the cpb_delay_offset syntax element in the buffering period SEI + /// message and the au_cpb_removal_delay_minus1 syntax element in the + /// picture timing SEI message. + pub au_cpb_removal_delay_length_minus1: u8, + /// dpb_output_delay_length_minus1 plus 1 specifies the length, in bits, of + /// the dpb_delay_offset syntax element in the buffering period SEI message + /// and the pic_dpb_output_delay syntax element in the picture timing SEI + /// message. + pub dpb_output_delay_length_minus1: u8, + /// `fixed_pic_rate_general_flag[ i ]` set indicates that, when HighestTid is + /// equal to i, the temporal distance between the HRD output times of + /// consecutive pictures in output order is constrained as specified in the + /// specification. `fixed_pic_rate_general_flag[ i ]` not set indicates that + /// this constraint may not apply. + pub fixed_pic_rate_general_flag: [bool; 7], + /// `fixed_pic_rate_within_cvs_flag[ i ]` set indicates that, when HighestTid + /// is equal to i, the temporal distance between the HRD output times of + /// consecutive pictures in output order is constrained as specified in the + /// specification. `fixed_pic_rate_within_cvs_flag[ i ]` not set indicates + /// that this constraint may not apply. + pub fixed_pic_rate_within_cvs_flag: [bool; 7], + /// `elemental_duration_in_tc_minus1[ i ]` plus 1 (when present) specifies, + /// when HighestTid is equal to i, the temporal distance, in clock ticks, + /// between the elemental units that specify the HRD output times of + /// consecutive pictures in output order as specified in the specification. + pub elemental_duration_in_tc_minus1: [u32; 7], + /// `low_delay_hrd_flag[ i ]` specifies the HRD operational mode, when + /// HighestTid is equal to i, as specified in Annex C or clause F.13. + pub low_delay_hrd_flag: [bool; 7], + /// `cpb_cnt_minus1[ i ]` plus 1 specifies the number of alternative CPB + /// specifications in the bitstream of the CVS when HighestTid is equal to + /// i. + pub cpb_cnt_minus1: [u32; 7], + /// The NAL HRD data. + pub nal_hrd: [SublayerHrdParameters; 7], + /// The VCL HRD data. + pub vcl_hrd: [SublayerHrdParameters; 7], +} + +impl Default for HrdParams { + fn default() -> Self { + Self { + initial_cpb_removal_delay_length_minus1: 23, + au_cpb_removal_delay_length_minus1: 23, + dpb_output_delay_du_length_minus1: 23, + nal_hrd_parameters_present_flag: Default::default(), + vcl_hrd_parameters_present_flag: Default::default(), + sub_pic_hrd_params_present_flag: Default::default(), + tick_divisor_minus2: Default::default(), + du_cpb_removal_delay_increment_length_minus1: Default::default(), + sub_pic_cpb_params_in_pic_timing_sei_flag: Default::default(), + bit_rate_scale: Default::default(), + cpb_size_scale: Default::default(), + cpb_size_du_scale: Default::default(), + dpb_output_delay_length_minus1: Default::default(), + fixed_pic_rate_general_flag: Default::default(), + fixed_pic_rate_within_cvs_flag: Default::default(), + elemental_duration_in_tc_minus1: Default::default(), + low_delay_hrd_flag: Default::default(), + cpb_cnt_minus1: Default::default(), + nal_hrd: Default::default(), + vcl_hrd: Default::default(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VuiParams { + /// When set, specifies that aspect_ratio_idc is present. When not set, + /// specifies that aspect_ratio_idc is not present. + pub aspect_ratio_info_present_flag: bool, + /// Specifies the value of the sample aspect ratio of the luma samples. + pub aspect_ratio_idc: u32, + /// Indicates the horizontal size of the sample aspect ratio (in arbitrary + /// units). + pub sar_width: u32, + /// Indicates the vertical size of the sample aspect ratio (in arbitrary + /// units). + pub sar_height: u32, + /// When set, specifies that the overscan_appropriate_flag is present. When + /// not set, the preferred display method for the video signal is + /// unspecified. + pub overscan_info_present_flag: bool, + /// When set indicates that the cropped decoded pictures output are suitable + /// for display using overscan. When not set, indicates that the cropped + /// decoded pictures output contain visually important information in the + /// entire region out to the edges of the conformance cropping window of the + /// picture, such that the cropped decoded pictures output should not be + /// displayed using overscan. + pub overscan_appropriate_flag: bool, + /// When set, specifies that video_format, video_full_range_flag and + /// colour_description_present_flag are present. When not set, specify that + /// video_format, video_full_range_flag and colour_description_present_flag + /// are not present. + pub video_signal_type_present_flag: bool, + /// Indicates the representation of the pictures as specified in Table E.2, + /// before being coded in accordance with this Specification. + pub video_format: u8, + /// Indicates the black level and range of the luma and chroma signals as + /// derived from E′Y, E′PB, and E′PR or E′R, E′G, and E′B real-valued + /// component signals. + pub video_full_range_flag: bool, + /// When set, specifies that colour_primaries, transfer_characteristics, and + /// matrix_coeffs are present. When not set, specifies that + /// colour_primaries, transfer_characteristics, and matrix_coeffs are not + /// present. + pub colour_description_present_flag: bool, + /// Indicates the chromaticity coordinates of the source primaries as + /// specified in Table E.3 in terms of the CIE 1931 definition of x and y as + /// specified in ISO 11664-1. + pub colour_primaries: u32, + /// See table E.4 in the specification. + pub transfer_characteristics: u32, + /// Describes the matrix coefficients used in deriving luma and chroma + /// signals from the green, blue, and red, or Y, Z, and X primaries, as + /// specified in Table E.5. + pub matrix_coeffs: u32, + /// When true, specifies that chroma_sample_loc_type_top_field and + /// chroma_sample_loc_type_bottom_field are present. When false, specifies + /// that chroma_sample_loc_type_top_field and + /// chroma_sample_loc_type_bottom_field are not present. + pub chroma_loc_info_present_flag: bool, + /// See the specification for more details. + pub chroma_sample_loc_type_top_field: u32, + /// See the specification for more details. + pub chroma_sample_loc_type_bottom_field: u32, + /// When true, indicates that the value of all decoded chroma samples is + /// equal to 1 << ( BitDepthC − 1 ). When false, provides no indication of + /// decoded chroma sample values. + pub neutral_chroma_indication_flag: bool, + /// When true, indicates that the CVS conveys pictures that represent + /// fields, and specifies that a picture timing SEI message shall be present + /// in every access unit of the current CVS. When false, indicates that the + /// CVS conveys pictures that represent frames and that a picture timing SEI + /// message may or may not be present in any access unit of the current CVS. + pub field_seq_flag: bool, + /// When true, specifies that picture timing SEI messages are present for + /// every picture and include the pic_struct, source_scan_type and + /// duplicate_flag syntax elements. When false, specifies that the + /// pic_struct syntax element is not present in picture timing SEI messages. + pub frame_field_info_present_flag: bool, + /// When true, indicates that the default display window parameters follow + /// next in the VUI. When false, indicates that the default display window + /// parameters are not present. + pub default_display_window_flag: bool, + /// Specifies the samples of the pictures in the CVS that are within the + /// default display window, in terms of a rectangular region specified in + /// picture coordinates for display. + pub def_disp_win_left_offset: u32, + /// Specifies the samples of the pictures in the CVS that are within the + /// default display window, in terms of a rectangular region specified in + /// picture coordinates for display. + pub def_disp_win_right_offset: u32, + /// Specifies the samples of the pictures in the CVS that are within the + /// default display window, in terms of a rectangular region specified in + /// picture coordinates for display. + pub def_disp_win_top_offset: u32, + /// Specifies the samples of the pictures in the CVS that are within the + /// default display window, in terms of a rectangular region specified in + /// picture coordinates for display. + pub def_disp_win_bottom_offset: u32, + /// When set, specifies that vui_num_units_in_tick, vui_time_scale, + /// vui_poc_proportional_to_timing_flag and vui_hrd_parameters_present_flag + /// are present in the vui_parameters( ) syntax structure. When not set, + /// specifies that vui_num_units_in_tick, vui_time_scale, + /// vui_poc_proportional_to_timing_flag and vui_hrd_parameters_present_flag + /// are not present in the vui_parameters( ) syntax structure + pub timing_info_present_flag: bool, + /// The number of time units of a clock operating at the frequency + /// vui_time_scale Hz that corresponds to one increment (called a clock + /// tick) of a clock tick counter. + pub num_units_in_tick: u32, + /// Is the number of time units that pass in one second. For example, a time + /// coordinate system that measures time using a 27 MHz clock has a + /// vui_time_scale of 27 000 000. + pub time_scale: u32, + /// When set, indicates that the picture order count value for each picture + /// in the CVS that is not the first picture in the CVS, in decoding order, + /// is proportional to the output time of the picture relative to the output + /// time of the first picture in the CVS. When not set, indicates that the + /// picture order count value for each picture in the CVS that is not the + /// first picture in the CVS, in decoding order, may or may not be + /// proportional to the output time of the picture relative to the output + /// time of the first picture in the CVS. + pub poc_proportional_to_timing_flag: bool, + /// vui_num_ticks_poc_diff_one_minus1 plus 1 specifies the number of clock + /// ticks corresponding to a difference of picture order count values equal + /// to 1. + pub num_ticks_poc_diff_one_minus1: u32, + /// When set, specifies that the syntax structure hrd_parameters( ) is + /// present in the vui_parameters( ) syntax structure. When not set, + /// specifies that the syntax structure hrd_parameters( ) is not present in + /// the vui_parameters( ) syntax structure. + pub hrd_parameters_present_flag: bool, + /// The hrd_parameters() data. + pub hrd: HrdParams, + /// When set, specifies that the bitstream restriction parameters for the + /// CVS are present. When not set, specifies that the bitstream restriction + /// parameters for the CVS are not present. + pub bitstream_restriction_flag: bool, + /// When set, indicates that each PPS that is active in the CVS has the same + /// value of the syntax elements num_tile_columns_minus1, + /// num_tile_rows_minus1, uniform_spacing_flag, `column_width_minus1[ i ]`, + /// `row_height_minus1[ i ]` and loop_filter_across_tiles_enabled_flag, when + /// present. When not set, indicates that tiles syntax elements in different + /// PPSs may or may not have the same value + pub tiles_fixed_structure_flag: bool, + /// When not set, indicates that no sample outside the picture boundaries + /// and no sample at a fractional sample position for which the sample value + /// is derived using one or more samples outside the picture boundaries is + /// used for inter prediction of any sample. When set, indicates that one + /// or more samples outside the picture boundaries may be used in inter + /// prediction. + pub motion_vectors_over_pic_boundaries_flag: bool, + /// When set, indicates that all P and B slices (when present) that belong + /// to the same picture have an identical reference picture list 0 and that + /// all B slices (when present) that belong to the same picture have an + /// identical reference picture list 1. + pub restricted_ref_pic_lists_flag: bool, + /// When not equal to 0, establishes a bound on the maximum possible size of + /// distinct coded spatial segmentation regions in the pictures of the CVS. + pub min_spatial_segmentation_idc: u32, + /// Indicates a number of bytes not exceeded by the sum of the sizes of the + /// VCL NAL units associated with any coded picture in the CVS. + pub max_bytes_per_pic_denom: u32, + /// Indicates an upper bound for the number of coded bits of coding_unit( ) + /// data for anycoding block in any picture of the CVS. + pub max_bits_per_min_cu_denom: u32, + /// Indicate the maximum absolute value of a decoded horizontal and vertical + /// motion vector component, respectively, in quarter luma sample units, for + /// all pictures in the CVS. + pub log2_max_mv_length_horizontal: u32, + /// Indicate the maximum absolute value of a decoded horizontal and vertical + /// motion vector component, respectively, in quarter luma sample units, for + /// all pictures in the CVS. + pub log2_max_mv_length_vertical: u32, +} + +impl Default for VuiParams { + fn default() -> Self { + Self { + aspect_ratio_info_present_flag: Default::default(), + aspect_ratio_idc: Default::default(), + sar_width: Default::default(), + sar_height: Default::default(), + overscan_info_present_flag: Default::default(), + overscan_appropriate_flag: Default::default(), + video_signal_type_present_flag: Default::default(), + video_format: 5, + video_full_range_flag: Default::default(), + colour_description_present_flag: Default::default(), + colour_primaries: 2, + transfer_characteristics: 2, + matrix_coeffs: 2, + chroma_loc_info_present_flag: Default::default(), + chroma_sample_loc_type_top_field: Default::default(), + chroma_sample_loc_type_bottom_field: Default::default(), + neutral_chroma_indication_flag: Default::default(), + field_seq_flag: Default::default(), + frame_field_info_present_flag: Default::default(), + default_display_window_flag: Default::default(), + def_disp_win_left_offset: Default::default(), + def_disp_win_right_offset: Default::default(), + def_disp_win_top_offset: Default::default(), + def_disp_win_bottom_offset: Default::default(), + timing_info_present_flag: Default::default(), + num_units_in_tick: Default::default(), + time_scale: Default::default(), + poc_proportional_to_timing_flag: Default::default(), + num_ticks_poc_diff_one_minus1: Default::default(), + hrd_parameters_present_flag: Default::default(), + hrd: Default::default(), + bitstream_restriction_flag: Default::default(), + tiles_fixed_structure_flag: Default::default(), + motion_vectors_over_pic_boundaries_flag: true, + restricted_ref_pic_lists_flag: Default::default(), + min_spatial_segmentation_idc: Default::default(), + max_bytes_per_pic_denom: 2, + max_bits_per_min_cu_denom: 1, + log2_max_mv_length_horizontal: 15, + log2_max_mv_length_vertical: 15, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct Parser { + active_vpses: BTreeMap>, + active_spses: BTreeMap>, + active_ppses: BTreeMap>, +} + +impl Parser { + /// Parse a VPS NALU. + pub fn parse_vps(&mut self, nalu: &Nalu) -> Result<&Vps, String> { + if !matches!(nalu.header.type_, NaluType::VpsNut) { + return Err(format!( + "Invalid NALU type, expected {:?}, got {:?}", + NaluType::VpsNut, + nalu.header.type_ + )); + } + + let data = nalu.as_ref(); + let header = &nalu.header; + let hdr_len = header.len(); + // Skip the header + let mut r = BitReader::new(&data[hdr_len..], true); + + let mut vps = Vps { + video_parameter_set_id: r.read_bits(4)?, + base_layer_internal_flag: r.read_bit()?, + base_layer_available_flag: r.read_bit()?, + max_layers_minus1: r.read_bits(6)?, + max_sub_layers_minus1: r.read_bits(3)?, + temporal_id_nesting_flag: r.read_bit()?, + ..Default::default() + }; + + r.skip_bits(16)?; // vps_reserved_0xffff_16bits + + let ptl = &mut vps.profile_tier_level; + Self::parse_profile_tier_level(ptl, &mut r, true, vps.max_sub_layers_minus1)?; + + vps.sub_layer_ordering_info_present_flag = r.read_bit()?; + + let start = if vps.sub_layer_ordering_info_present_flag { + 0 + } else { + vps.max_sub_layers_minus1 + } as usize; + + for i in start..=usize::from(vps.max_sub_layers_minus1) { + vps.max_dec_pic_buffering_minus1[i] = r.read_ue_max(15)?; + vps.max_num_reorder_pics[i] = r.read_ue_max(vps.max_dec_pic_buffering_minus1[i])?; + vps.max_latency_increase_plus1[i] = r.read_ue()?; + + if i > 0 { + if vps.max_dec_pic_buffering_minus1[i] < vps.max_dec_pic_buffering_minus1[i - 1] { + return Err(format!( + "Invalid max_dec_pic_buffering_minus1[{}]: {}", + i, vps.max_dec_pic_buffering_minus1[i] + )); + } + + if vps.max_num_reorder_pics[i] < vps.max_num_reorder_pics[i - 1] { + return Err(format!( + "Invalid max_num_reorder_pics[{}]: {}", + i, vps.max_num_reorder_pics[i] + )); + } + } + } + + // vps_sub_layer_ordering_info_present_flag equal to 0 specifies that + // the values of vps_max_dec_pic_buffering_minus1[ + // vps_max_sub_layers_minus1 ], vps_max_num_reorder_pics[ vps_max_sub_ + // layers_minus1 ] and vps_max_latency_increase_plus1[ + // vps_max_sub_layers_minus1 ] apply to all sub-layers + if !vps.sub_layer_ordering_info_present_flag { + let max_num_sublayers = usize::from(vps.max_sub_layers_minus1); + for i in 0..max_num_sublayers { + vps.max_dec_pic_buffering_minus1[i] = + vps.max_dec_pic_buffering_minus1[max_num_sublayers]; + + vps.max_num_reorder_pics[i] = vps.max_num_reorder_pics[max_num_sublayers]; + + vps.max_latency_increase_plus1[i] = + vps.max_latency_increase_plus1[max_num_sublayers]; + } + } + + vps.max_layer_id = r.read_bits(6)?; + if vps.max_layer_id > 62 { + return Err(format!("Invalid max_layer_id {}", vps.max_layer_id)); + } + + vps.num_layer_sets_minus1 = r.read_ue_max(1023)?; + + for _ in 1..=vps.num_layer_sets_minus1 { + for _ in 0..=vps.max_layer_id { + // Skip layer_id_included_flag[i][j] for now. + r.skip_bits(1)?; + } + } + + vps.timing_info_present_flag = r.read_bit()?; + + if vps.timing_info_present_flag { + vps.num_units_in_tick = r.read_bits::(31)? << 1; + vps.num_units_in_tick |= r.read_bits::(1)?; + + vps.time_scale = r.read_bits::(31)? << 1; + vps.time_scale |= r.read_bits::(1)?; + + vps.poc_proportional_to_timing_flag = r.read_bit()?; + if vps.poc_proportional_to_timing_flag { + vps.num_ticks_poc_diff_one_minus1 = r.read_ue()?; + } + + vps.num_hrd_parameters = r.read_ue()?; + + for i in 0..vps.num_hrd_parameters as usize { + vps.hrd_layer_set_idx.push(r.read_ue()?); + if i > 0 { + vps.cprms_present_flag.push(r.read_bit()?); + } + + let mut hrd = HrdParams::default(); + Self::parse_hrd_parameters( + vps.cprms_present_flag[i], + vps.max_sub_layers_minus1, + &mut hrd, + &mut r, + )?; + + vps.hrd_parameters.push(hrd); + } + } + + vps.extension_flag = r.read_bit()?; + + if self.active_vpses.keys().len() >= MAX_VPS_COUNT { + return Err("Broken data: Number of active VPSs > MAX_VPS_COUNT".into()); + } + + let key = vps.video_parameter_set_id; + let vps = Rc::new(vps); + self.active_vpses.remove(&key); + Ok(self.active_vpses.entry(key).or_insert(vps)) + } + + fn parse_profile_tier_level( + ptl: &mut ProfileTierLevel, + r: &mut BitReader, + profile_present_flag: bool, + sps_max_sub_layers_minus_1: u8, + ) -> Result<(), String> { + if profile_present_flag { + ptl.general_profile_space = r.read_bits(2)?; + ptl.general_tier_flag = r.read_bit()?; + ptl.general_profile_idc = r.read_bits(5)?; + + for i in 0..32 { + ptl.general_profile_compatibility_flag[i] = r.read_bit()?; + } + + ptl.general_progressive_source_flag = r.read_bit()?; + ptl.general_interlaced_source_flag = r.read_bit()?; + ptl.general_non_packed_constraint_flag = r.read_bit()?; + ptl.general_frame_only_constraint_flag = r.read_bit()?; + + if ptl.general_profile_idc == 4 + || ptl.general_profile_compatibility_flag[4] + || ptl.general_profile_idc == 5 + || ptl.general_profile_compatibility_flag[5] + || ptl.general_profile_idc == 6 + || ptl.general_profile_compatibility_flag[6] + || ptl.general_profile_idc == 7 + || ptl.general_profile_compatibility_flag[7] + || ptl.general_profile_idc == 8 + || ptl.general_profile_compatibility_flag[8] + || ptl.general_profile_idc == 9 + || ptl.general_profile_compatibility_flag[9] + || ptl.general_profile_idc == 10 + || ptl.general_profile_compatibility_flag[10] + || ptl.general_profile_idc == 11 + || ptl.general_profile_compatibility_flag[11] + { + ptl.general_max_12bit_constraint_flag = r.read_bit()?; + ptl.general_max_10bit_constraint_flag = r.read_bit()?; + ptl.general_max_8bit_constraint_flag = r.read_bit()?; + ptl.general_max_422chroma_constraint_flag = r.read_bit()?; + ptl.general_max_420chroma_constraint_flag = r.read_bit()?; + ptl.general_max_monochrome_constraint_flag = r.read_bit()?; + ptl.general_intra_constraint_flag = r.read_bit()?; + ptl.general_one_picture_only_constraint_flag = r.read_bit()?; + ptl.general_lower_bit_rate_constraint_flag = r.read_bit()?; + if ptl.general_profile_idc == 5 + || ptl.general_profile_compatibility_flag[5] + || ptl.general_profile_idc == 9 + || ptl.general_profile_compatibility_flag[9] + || ptl.general_profile_idc == 10 + || ptl.general_profile_compatibility_flag[10] + || ptl.general_profile_idc == 11 + || ptl.general_profile_compatibility_flag[11] + { + ptl.general_max_14bit_constraint_flag = r.read_bit()?; + // Skip general_reserved_zero_33bits + r.skip_bits(31)?; + r.skip_bits(2)?; + } else { + // Skip general_reserved_zero_34bits + r.skip_bits(31)?; + r.skip_bits(3)?; + } + } else if ptl.general_profile_idc == 2 || ptl.general_profile_compatibility_flag[2] { + // Skip general_reserved_zero_7bits + r.skip_bits(7)?; + ptl.general_one_picture_only_constraint_flag = r.read_bit()?; + // Skip general_reserved_zero_35bits + r.skip_bits(31)?; + r.skip_bits(4)?; + } else { + r.skip_bits(31)?; + r.skip_bits(12)?; + } + + if ptl.general_profile_idc == 1 + || ptl.general_profile_compatibility_flag[1] + || ptl.general_profile_idc == 2 + || ptl.general_profile_compatibility_flag[2] + || ptl.general_profile_idc == 3 + || ptl.general_profile_compatibility_flag[3] + || ptl.general_profile_idc == 4 + || ptl.general_profile_compatibility_flag[4] + || ptl.general_profile_idc == 5 + || ptl.general_profile_compatibility_flag[5] + || ptl.general_profile_idc == 9 + || ptl.general_profile_compatibility_flag[9] + || ptl.general_profile_idc == 11 + || ptl.general_profile_compatibility_flag[11] + { + ptl.general_inbld_flag = r.read_bit()?; + } else { + r.skip_bits(1)?; + } + } + + let level: u8 = r.read_bits(8)?; + ptl.general_level_idc = Level::try_from(level)?; + + for i in 0..sps_max_sub_layers_minus_1 as usize { + ptl.sub_layer_profile_present_flag[i] = r.read_bit()?; + ptl.sub_layer_level_present_flag[i] = r.read_bit()?; + } + + if sps_max_sub_layers_minus_1 > 0 { + for _ in sps_max_sub_layers_minus_1..8 { + r.skip_bits(2)?; + } + } + + for i in 0..sps_max_sub_layers_minus_1 as usize { + if ptl.sub_layer_level_present_flag[i] { + ptl.sub_layer_profile_space[i] = r.read_bits(2)?; + ptl.sub_layer_tier_flag[i] = r.read_bit()?; + ptl.sub_layer_profile_idc[i] = r.read_bits(5)?; + for j in 0..32 { + ptl.sub_layer_profile_compatibility_flag[i][j] = r.read_bit()?; + } + ptl.sub_layer_progressive_source_flag[i] = r.read_bit()?; + ptl.sub_layer_interlaced_source_flag[i] = r.read_bit()?; + ptl.sub_layer_non_packed_constraint_flag[i] = r.read_bit()?; + ptl.sub_layer_frame_only_constraint_flag[i] = r.read_bit()?; + + if ptl.sub_layer_profile_idc[i] == 4 + || ptl.sub_layer_profile_compatibility_flag[i][4] + || ptl.sub_layer_profile_idc[i] == 5 + || ptl.sub_layer_profile_compatibility_flag[i][5] + || ptl.sub_layer_profile_idc[i] == 6 + || ptl.sub_layer_profile_compatibility_flag[i][6] + || ptl.sub_layer_profile_idc[i] == 7 + || ptl.sub_layer_profile_compatibility_flag[i][7] + || ptl.sub_layer_profile_idc[i] == 8 + || ptl.sub_layer_profile_compatibility_flag[i][8] + || ptl.sub_layer_profile_idc[i] == 9 + || ptl.sub_layer_profile_compatibility_flag[i][9] + || ptl.sub_layer_profile_idc[i] == 10 + || ptl.sub_layer_profile_compatibility_flag[i][10] + || ptl.sub_layer_profile_idc[i] == 11 + || ptl.sub_layer_profile_compatibility_flag[i][11] + { + ptl.sub_layer_max_12bit_constraint_flag[i] = r.read_bit()?; + ptl.sub_layer_max_10bit_constraint_flag[i] = r.read_bit()?; + ptl.sub_layer_max_8bit_constraint_flag[i] = r.read_bit()?; + ptl.sub_layer_max_422chroma_constraint_flag[i] = r.read_bit()?; + ptl.sub_layer_max_420chroma_constraint_flag[i] = r.read_bit()?; + ptl.sub_layer_max_monochrome_constraint_flag[i] = r.read_bit()?; + ptl.sub_layer_intra_constraint_flag[i] = r.read_bit()?; + ptl.sub_layer_one_picture_only_constraint_flag[i] = r.read_bit()?; + ptl.sub_layer_lower_bit_rate_constraint_flag[i] = r.read_bit()?; + + if ptl.sub_layer_profile_idc[i] == 5 + || ptl.sub_layer_profile_compatibility_flag[i][5] + || ptl.sub_layer_profile_idc[i] == 9 + || ptl.sub_layer_profile_compatibility_flag[i][9] + || ptl.sub_layer_profile_idc[i] == 10 + || ptl.sub_layer_profile_compatibility_flag[i][10] + || ptl.sub_layer_profile_idc[i] == 11 + || ptl.sub_layer_profile_compatibility_flag[i][11] + { + ptl.sub_layer_max_14bit_constraint_flag[i] = r.read_bit()?; + r.skip_bits(33)?; + } else { + r.skip_bits(34)?; + } + } else if ptl.sub_layer_profile_idc[i] == 2 + || ptl.sub_layer_profile_compatibility_flag[i][2] + { + r.skip_bits(7)?; + ptl.sub_layer_one_picture_only_constraint_flag[i] = r.read_bit()?; + r.skip_bits(35)?; + } else { + r.skip_bits(43)?; + } + + if ptl.sub_layer_profile_idc[i] == 1 + || ptl.sub_layer_profile_compatibility_flag[i][1] + || ptl.sub_layer_profile_idc[i] == 2 + || ptl.sub_layer_profile_compatibility_flag[i][2] + || ptl.sub_layer_profile_idc[i] == 3 + || ptl.sub_layer_profile_compatibility_flag[i][3] + || ptl.sub_layer_profile_idc[i] == 4 + || ptl.sub_layer_profile_compatibility_flag[i][4] + || ptl.sub_layer_profile_idc[i] == 5 + || ptl.sub_layer_profile_compatibility_flag[i][5] + || ptl.sub_layer_profile_idc[i] == 9 + || ptl.sub_layer_profile_compatibility_flag[i][9] + || ptl.sub_layer_profile_idc[i] == 11 + || ptl.sub_layer_profile_compatibility_flag[i][11] + { + ptl.sub_layer_inbld_flag[i] = r.read_bit()?; + } else { + r.skip_bits(1)?; + } + + if ptl.sub_layer_level_present_flag[i] { + let level: u8 = r.read_bits(8)?; + ptl.sub_layer_level_idc[i] = Level::try_from(level)?; + } + } + } + Ok(()) + } + + fn fill_default_scaling_list(sl: &mut ScalingLists, size_id: i32, matrix_id: i32) { + if size_id == 0 { + sl.scaling_list_4x4[matrix_id as usize] = DEFAULT_SCALING_LIST_0; + return; + } + + let dst = match size_id { + 1 => &mut sl.scaling_list_8x8[matrix_id as usize], + 2 => &mut sl.scaling_list_16x16[matrix_id as usize], + 3 => &mut sl.scaling_list_32x32[matrix_id as usize], + _ => panic!("Invalid size_id {}", size_id), + }; + + let src = if matrix_id < 3 { + &DEFAULT_SCALING_LIST_1 + } else if matrix_id <= 5 { + &DEFAULT_SCALING_LIST_2 + } else { + panic!("Invalid matrix_id {}", matrix_id); + }; + + *dst = *src; + + // When `scaling_list_pred_mode_flag[ sizeId ]`[ matrixId ] is equal to + // 0, scaling_list_pred_matrix_id_ `delta[ sizeId ]`[ matrixId ] is equal + // to 0 and sizeId is greater than 1, the value of + // scaling_list_dc_coef_minus8[ sizeId − 2 `][ matrixId ]` is inferred to + // be equal to 8. + // + // Since we are using a slightly different layout here, with two + // different field names (i.e. 16x16, and 32x32), we must differentiate + // between size_id == 2 or size_id == 3. + if size_id == 2 { + sl.scaling_list_dc_coef_minus8_16x16[matrix_id as usize] = 8; + } else if size_id == 3 { + sl.scaling_list_dc_coef_minus8_32x32[matrix_id as usize] = 8; + } + } + + fn parse_scaling_list_data(sl: &mut ScalingLists, r: &mut BitReader) -> Result<(), String> { + // 7.4.5 + for size_id in 0..4 { + let mut matrix_id = 0; + while matrix_id < 6 { + let scaling_list_pred_mode_flag = r.read_bit()?; + // If `scaling_list_pred_matrix_id_delta[ sizeId ]`[ matrixId ] is + // equal to 0, the scaling list is inferred from the default + // scaling list `ScalingList[ sizeId ]`[ matrixId `][ i ]` as specified + // in Table 7-5 and Table 7-6 for i = 0..Min( 63, ( 1 << ( 4 + ( + // sizeId << 1 ) ) ) − 1 ). + if !scaling_list_pred_mode_flag { + let scaling_list_pred_matrix_id_delta: u32 = r.read_ue()?; + if scaling_list_pred_matrix_id_delta == 0 { + Self::fill_default_scaling_list(sl, size_id, matrix_id); + } else { + // Equation 7-42 + let factor = if size_id == 3 { 3 } else { 1 }; + let ref_matrix_id = + matrix_id as u32 - scaling_list_pred_matrix_id_delta * factor; + if size_id == 0 { + sl.scaling_list_4x4[matrix_id as usize] = + sl.scaling_list_4x4[ref_matrix_id as usize]; + } else { + let src = match size_id { + 1 => sl.scaling_list_8x8[ref_matrix_id as usize], + 2 => sl.scaling_list_16x16[ref_matrix_id as usize], + 3 => sl.scaling_list_32x32[ref_matrix_id as usize], + _ => return Err(format!("Invalid size_id {}", size_id)), + }; + + let dst = match size_id { + 1 => &mut sl.scaling_list_8x8[matrix_id as usize], + 2 => &mut sl.scaling_list_16x16[matrix_id as usize], + 3 => &mut sl.scaling_list_32x32[matrix_id as usize], + _ => return Err(format!("Invalid size_id {}", size_id)), + }; + + *dst = src; + + if size_id == 2 { + sl.scaling_list_dc_coef_minus8_16x16[matrix_id as usize] = + sl.scaling_list_dc_coef_minus8_16x16[ref_matrix_id as usize]; + } else if size_id == 3 { + sl.scaling_list_dc_coef_minus8_32x32[matrix_id as usize] = + sl.scaling_list_dc_coef_minus8_32x32[ref_matrix_id as usize]; + } + } + } + } else { + let mut next_coef = 8i32; + let coef_num = std::cmp::min(64, 1 << (4 + (size_id << 1))); + + if size_id > 1 { + if size_id == 2 { + sl.scaling_list_dc_coef_minus8_16x16[matrix_id as usize] = + r.read_se_bounded(-7, 247)?; + next_coef = + i32::from(sl.scaling_list_dc_coef_minus8_16x16[matrix_id as usize]) + + 8; + } else if size_id == 3 { + sl.scaling_list_dc_coef_minus8_32x32[matrix_id as usize] = + r.read_se_bounded(-7, 247)?; + next_coef = + i32::from(sl.scaling_list_dc_coef_minus8_32x32[matrix_id as usize]) + + 8; + } + } + + for i in 0..coef_num as usize { + let scaling_list_delta_coef: i32 = r.read_se_bounded(-128, 127)?; + next_coef = (next_coef + scaling_list_delta_coef + 256) % 256; + match size_id { + 0 => sl.scaling_list_4x4[matrix_id as usize][i] = next_coef as _, + 1 => sl.scaling_list_8x8[matrix_id as usize][i] = next_coef as _, + 2 => sl.scaling_list_16x16[matrix_id as usize][i] = next_coef as _, + 3 => sl.scaling_list_32x32[matrix_id as usize][i] = next_coef as _, + _ => return Err(format!("Invalid size_id {}", size_id)), + } + } + } + let step = if size_id == 3 { 3 } else { 1 }; + matrix_id += step; + } + } + Ok(()) + } + + fn parse_short_term_ref_pic_set( + sps: &Sps, + st: &mut ShortTermRefPicSet, + r: &mut BitReader, + st_rps_idx: u8, + ) -> Result<(), String> { + if st_rps_idx != 0 { + st.inter_ref_pic_set_prediction_flag = r.read_bit()?; + } + + // (7-59) + if st.inter_ref_pic_set_prediction_flag { + if st_rps_idx == sps.num_short_term_ref_pic_sets { + st.delta_idx_minus1 = r.read_ue_max(st_rps_idx as u32 - 1)?; + } + + st.delta_rps_sign = r.read_bit()?; + // The value of abs_delta_rps_minus1 shall be in the range of 0 to + // 2^15 − 1, inclusive. + st.abs_delta_rps_minus1 = r.read_ue_max(32767)?; + + let ref_rps_idx = st_rps_idx - (st.delta_idx_minus1 + 1); + let delta_rps = + (1 - 2 * st.delta_rps_sign as i32) * (st.abs_delta_rps_minus1 as i32 + 1); + + let ref_st = sps + .short_term_ref_pic_set + .get(usize::from(ref_rps_idx)) + .ok_or::("Invalid ref_rps_idx".into())?; + + let mut used_by_curr_pic_flag = [false; 64]; + + // 7.4.8 - defaults to 1 if not present + let mut use_delta_flag = [true; 64]; + + for j in 0..=ref_st.num_delta_pocs as usize { + used_by_curr_pic_flag[j] = r.read_bit()?; + if !used_by_curr_pic_flag[j] { + use_delta_flag[j] = r.read_bit()?; + } + } + + // (7-61) + let mut i = 0; + // Ranges are [a,b[, but the real loop is [b, a], i.e. + // [num_positive_pics - 1, 0]. Use ..= so that b is included when + // rev() is called. + for j in (0..=isize::from(ref_st.num_positive_pics) - 1) + .rev() + .take_while(|j| *j >= 0) + .map(|j| j as usize) + { + let d_poc = ref_st.delta_poc_s1[j] + delta_rps; + if d_poc < 0 && use_delta_flag[usize::from(ref_st.num_negative_pics) + j] { + st.delta_poc_s0[i] = d_poc; + st.used_by_curr_pic_s0[i] = + used_by_curr_pic_flag[usize::from(ref_st.num_negative_pics) + j]; + + i += 1; + } + } + + if delta_rps < 0 && use_delta_flag[ref_st.num_delta_pocs as usize] { + st.delta_poc_s0[i] = delta_rps; + st.used_by_curr_pic_s0[i] = used_by_curr_pic_flag[ref_st.num_delta_pocs as usize]; + + i += 1; + } + + // Let's *not* change the original algorithm in any way. + #[allow(clippy::needless_range_loop)] + for j in 0..ref_st.num_negative_pics as usize { + let d_poc = ref_st.delta_poc_s0[j] + delta_rps; + if d_poc < 0 && use_delta_flag[j] { + st.delta_poc_s0[i] = d_poc; + st.used_by_curr_pic_s0[i] = used_by_curr_pic_flag[j]; + + i += 1; + } + } + + st.num_negative_pics = i as u8; + + // (7-62) + let mut i = 0; + // Ranges are [a,b[, but the real loop is [b, a], i.e. + // [num_negative_pics - 1, 0]. Use ..= so that b is included when + // rev() is called. + for j in (0..=isize::from(ref_st.num_negative_pics) - 1) + .rev() + .take_while(|j| *j >= 0) + .map(|j| j as usize) + { + let d_poc = ref_st.delta_poc_s0[j] + delta_rps; + if d_poc > 0 && use_delta_flag[j] { + st.delta_poc_s1[i] = d_poc; + st.used_by_curr_pic_s1[i] = used_by_curr_pic_flag[j]; + + i += 1; + } + } + + if delta_rps > 0 && use_delta_flag[ref_st.num_delta_pocs as usize] { + st.delta_poc_s1[i] = delta_rps; + st.used_by_curr_pic_s1[i] = used_by_curr_pic_flag[ref_st.num_delta_pocs as usize]; + + i += 1; + } + + for j in 0..usize::from(ref_st.num_positive_pics) { + let d_poc = ref_st.delta_poc_s1[j] + delta_rps; + if d_poc > 0 && use_delta_flag[ref_st.num_negative_pics as usize + j] { + st.delta_poc_s1[i] = d_poc; + st.used_by_curr_pic_s1[i] = + used_by_curr_pic_flag[ref_st.num_negative_pics as usize + j]; + + i += 1; + } + } + + st.num_positive_pics = i as u8; + } else { + st.num_negative_pics = r.read_ue_max(u32::from( + sps.max_dec_pic_buffering_minus1[usize::from(sps.max_sub_layers_minus1)], + ))?; + + st.num_positive_pics = r.read_ue_max(u32::from( + sps.max_dec_pic_buffering_minus1[usize::from(sps.max_sub_layers_minus1)] + - st.num_negative_pics, + ))?; + + for i in 0..usize::from(st.num_negative_pics) { + let delta_poc_s0_minus1: u32 = r.read_ue_max(32767)?; + + if i == 0 { + st.delta_poc_s0[i] = -(delta_poc_s0_minus1 as i32 + 1); + } else { + st.delta_poc_s0[i] = st.delta_poc_s0[i - 1] - (delta_poc_s0_minus1 as i32 + 1); + } + + st.used_by_curr_pic_s0[i] = r.read_bit()?; + } + + for i in 0..usize::from(st.num_positive_pics) { + let delta_poc_s1_minus1: u32 = r.read_ue_max(32767)?; + + if i == 0 { + st.delta_poc_s1[i] = delta_poc_s1_minus1 as i32 + 1; + } else { + st.delta_poc_s1[i] = st.delta_poc_s1[i - 1] + (delta_poc_s1_minus1 as i32 + 1); + } + + st.used_by_curr_pic_s1[i] = r.read_bit()?; + } + } + + st.num_delta_pocs = u32::from(st.num_negative_pics + st.num_positive_pics); + + Ok(()) + } + + fn parse_sublayer_hrd_parameters( + h: &mut SublayerHrdParameters, + cpb_cnt: u32, + sub_pic_hrd_params_present_flag: bool, + r: &mut BitReader, + ) -> Result<(), String> { + for i in 0..cpb_cnt as usize { + h.bit_rate_value_minus1[i] = r.read_ue_max((2u64.pow(32) - 2) as u32)?; + h.cpb_size_value_minus1[i] = r.read_ue_max((2u64.pow(32) - 2) as u32)?; + if sub_pic_hrd_params_present_flag { + h.cpb_size_du_value_minus1[i] = r.read_ue_max((2u64.pow(32) - 2) as u32)?; + h.bit_rate_du_value_minus1[i] = r.read_ue_max((2u64.pow(32) - 2) as u32)?; + } + + h.cbr_flag[i] = r.read_bit()?; + } + + Ok(()) + } + + fn parse_hrd_parameters( + common_inf_present_flag: bool, + max_num_sublayers_minus1: u8, + hrd: &mut HrdParams, + r: &mut BitReader, + ) -> Result<(), String> { + if common_inf_present_flag { + hrd.nal_hrd_parameters_present_flag = r.read_bit()?; + hrd.vcl_hrd_parameters_present_flag = r.read_bit()?; + if hrd.nal_hrd_parameters_present_flag || hrd.vcl_hrd_parameters_present_flag { + hrd.sub_pic_hrd_params_present_flag = r.read_bit()?; + if hrd.sub_pic_hrd_params_present_flag { + hrd.tick_divisor_minus2 = r.read_bits(8)?; + hrd.du_cpb_removal_delay_increment_length_minus1 = r.read_bits(5)?; + hrd.sub_pic_cpb_params_in_pic_timing_sei_flag = r.read_bit()?; + hrd.dpb_output_delay_du_length_minus1 = r.read_bits(5)?; + } + hrd.bit_rate_scale = r.read_bits(4)?; + hrd.cpb_size_scale = r.read_bits(4)?; + if hrd.sub_pic_hrd_params_present_flag { + hrd.cpb_size_du_scale = r.read_bits(4)?; + } + hrd.initial_cpb_removal_delay_length_minus1 = r.read_bits(5)?; + hrd.au_cpb_removal_delay_length_minus1 = r.read_bits(5)?; + hrd.dpb_output_delay_length_minus1 = r.read_bits(5)?; + } + } + + for i in 0..=max_num_sublayers_minus1 as usize { + hrd.fixed_pic_rate_general_flag[i] = r.read_bit()?; + if !hrd.fixed_pic_rate_general_flag[i] { + hrd.fixed_pic_rate_within_cvs_flag[i] = r.read_bit()?; + } + if hrd.fixed_pic_rate_within_cvs_flag[i] { + hrd.elemental_duration_in_tc_minus1[i] = r.read_ue_max(2047)?; + } else { + hrd.low_delay_hrd_flag[i] = r.read_bit()?; + } + + if !hrd.low_delay_hrd_flag[i] { + hrd.cpb_cnt_minus1[i] = r.read_ue_max(31)?; + } + + if hrd.nal_hrd_parameters_present_flag { + Self::parse_sublayer_hrd_parameters( + &mut hrd.nal_hrd[i], + hrd.cpb_cnt_minus1[i] + 1, + hrd.sub_pic_hrd_params_present_flag, + r, + )?; + } + + if hrd.vcl_hrd_parameters_present_flag { + Self::parse_sublayer_hrd_parameters( + &mut hrd.vcl_hrd[i], + hrd.cpb_cnt_minus1[i] + 1, + hrd.sub_pic_hrd_params_present_flag, + r, + )?; + } + } + + Ok(()) + } + + fn parse_vui_parameters(sps: &mut Sps, r: &mut BitReader) -> Result<(), String> { + let vui = &mut sps.vui_parameters; + + vui.aspect_ratio_info_present_flag = r.read_bit()?; + if vui.aspect_ratio_info_present_flag { + vui.aspect_ratio_idc = r.read_bits(8)?; + const EXTENDED_SAR: u32 = 255; + if vui.aspect_ratio_idc == EXTENDED_SAR { + vui.sar_width = r.read_bits(16)?; + vui.sar_height = r.read_bits(16)?; + } + } + + vui.overscan_info_present_flag = r.read_bit()?; + if vui.overscan_info_present_flag { + vui.overscan_appropriate_flag = r.read_bit()?; + } + + vui.video_signal_type_present_flag = r.read_bit()?; + if vui.video_signal_type_present_flag { + vui.video_format = r.read_bits(3)?; + vui.video_full_range_flag = r.read_bit()?; + vui.colour_description_present_flag = r.read_bit()?; + if vui.colour_description_present_flag { + vui.colour_primaries = r.read_bits(8)?; + vui.transfer_characteristics = r.read_bits(8)?; + vui.matrix_coeffs = r.read_bits(8)?; + } + } + + vui.chroma_loc_info_present_flag = r.read_bit()?; + if vui.chroma_loc_info_present_flag { + vui.chroma_sample_loc_type_top_field = r.read_ue_max(5)?; + vui.chroma_sample_loc_type_bottom_field = r.read_ue_max(5)?; + } + + vui.neutral_chroma_indication_flag = r.read_bit()?; + vui.field_seq_flag = r.read_bit()?; + vui.frame_field_info_present_flag = r.read_bit()?; + vui.default_display_window_flag = r.read_bit()?; + + if vui.default_display_window_flag { + vui.def_disp_win_left_offset = r.read_ue()?; + vui.def_disp_win_right_offset = r.read_ue()?; + vui.def_disp_win_top_offset = r.read_ue()?; + vui.def_disp_win_bottom_offset = r.read_ue()?; + } + + vui.timing_info_present_flag = r.read_bit()?; + if vui.timing_info_present_flag { + vui.num_units_in_tick = r.read_bits::(31)? << 1; + vui.num_units_in_tick |= r.read_bits::(1)?; + + if vui.num_units_in_tick == 0 { + log::warn!( + "Incompliant value for num_units_in_tick {}", + vui.num_units_in_tick + ); + } + + vui.time_scale = r.read_bits::(31)? << 1; + vui.time_scale |= r.read_bits::(1)?; + + if vui.time_scale == 0 { + log::warn!("Incompliant value for time_scale {}", vui.time_scale); + } + + vui.poc_proportional_to_timing_flag = r.read_bit()?; + if vui.poc_proportional_to_timing_flag { + vui.num_ticks_poc_diff_one_minus1 = r.read_ue_max((2u64.pow(32) - 2) as u32)?; + } + + vui.hrd_parameters_present_flag = r.read_bit()?; + if vui.hrd_parameters_present_flag { + let sps_max_sub_layers_minus1 = sps.max_sub_layers_minus1; + Self::parse_hrd_parameters(true, sps_max_sub_layers_minus1, &mut vui.hrd, r)?; + } + } + + vui.bitstream_restriction_flag = r.read_bit()?; + if vui.bitstream_restriction_flag { + vui.tiles_fixed_structure_flag = r.read_bit()?; + vui.motion_vectors_over_pic_boundaries_flag = r.read_bit()?; + vui.restricted_ref_pic_lists_flag = r.read_bit()?; + + vui.min_spatial_segmentation_idc = r.read_ue_max(4095)?; + vui.max_bytes_per_pic_denom = r.read_ue()?; + vui.max_bits_per_min_cu_denom = r.read_ue()?; + vui.log2_max_mv_length_horizontal = r.read_ue_max(16)?; + vui.log2_max_mv_length_vertical = r.read_ue_max(15)?; + } + + Ok(()) + } + + fn parse_sps_scc_extension(sps: &mut Sps, r: &mut BitReader) -> Result<(), String> { + let scc = &mut sps.scc_extension; + + scc.curr_pic_ref_enabled_flag = r.read_bit()?; + scc.palette_mode_enabled_flag = r.read_bit()?; + if scc.palette_mode_enabled_flag { + scc.palette_max_size = r.read_ue_max(64)?; + scc.delta_palette_max_predictor_size = + r.read_ue_max(128 - u32::from(scc.palette_max_size))?; + scc.palette_predictor_initializers_present_flag = r.read_bit()?; + if scc.palette_predictor_initializers_present_flag { + let max = + u32::from(scc.palette_max_size + scc.delta_palette_max_predictor_size - 1); + scc.num_palette_predictor_initializer_minus1 = r.read_ue_max(max)?; + + let num_comps = if sps.chroma_format_idc == 0 { 1 } else { 3 }; + for comp in 0..num_comps { + for i in 0..=usize::from(scc.num_palette_predictor_initializer_minus1) { + let num_bits = if comp == 0 { + sps.bit_depth_luma_minus8 + 8 + } else { + sps.bit_depth_chroma_minus8 + 8 + }; + scc.palette_predictor_initializer[comp][i] = + r.read_bits(usize::from(num_bits))?; + } + } + } + } + + scc.motion_vector_resolution_control_idc = r.read_bits(2)?; + scc.intra_boundary_filtering_disabled_flag = r.read_bit()?; + + Ok(()) + } + + fn parse_sps_range_extension(sps: &mut Sps, r: &mut BitReader) -> Result<(), String> { + let ext = &mut sps.range_extension; + + ext.transform_skip_rotation_enabled_flag = r.read_bit()?; + ext.transform_skip_context_enabled_flag = r.read_bit()?; + ext.implicit_rdpcm_enabled_flag = r.read_bit()?; + ext.explicit_rdpcm_enabled_flag = r.read_bit()?; + ext.extended_precision_processing_flag = r.read_bit()?; + ext.intra_smoothing_disabled_flag = r.read_bit()?; + ext.high_precision_offsets_enabled_flag = r.read_bit()?; + ext.persistent_rice_adaptation_enabled_flag = r.read_bit()?; + ext.cabac_bypass_alignment_enabled_flag = r.read_bit()?; + + Ok(()) + } + + /// Parse a SPS NALU. + pub fn parse_sps(&mut self, nalu: &Nalu) -> Result<&Sps, String> { + if !matches!(nalu.header.type_, NaluType::SpsNut) { + return Err(format!( + "Invalid NALU type, expected {:?}, got {:?}", + NaluType::SpsNut, + nalu.header.type_ + )); + } + + let data = nalu.as_ref(); + let header = &nalu.header; + let hdr_len = header.len(); + // Skip the header + let mut r = BitReader::new(&data[hdr_len..], true); + + let video_parameter_set_id = r.read_bits(4)?; + + // A non-existing VPS means the SPS is not using any VPS. + let vps = self.get_vps(video_parameter_set_id).cloned(); + + let mut sps = Sps { + video_parameter_set_id, + max_sub_layers_minus1: r.read_bits(3)?, + temporal_id_nesting_flag: r.read_bit()?, + vps, + ..Default::default() + }; + + Self::parse_profile_tier_level( + &mut sps.profile_tier_level, + &mut r, + true, + sps.max_sub_layers_minus1, + )?; + + sps.seq_parameter_set_id = r.read_ue_max(MAX_SPS_COUNT as u32 - 1)?; + sps.chroma_format_idc = r.read_ue_max(3)?; + + if sps.chroma_format_idc == 3 { + sps.separate_colour_plane_flag = r.read_bit()?; + } + + sps.chroma_array_type = if sps.separate_colour_plane_flag { + 0 + } else { + sps.chroma_format_idc + }; + + sps.pic_width_in_luma_samples = r.read_ue_bounded(1, 16888)?; + sps.pic_height_in_luma_samples = r.read_ue_bounded(1, 16888)?; + + sps.conformance_window_flag = r.read_bit()?; + if sps.conformance_window_flag { + sps.conf_win_left_offset = r.read_ue()?; + sps.conf_win_right_offset = r.read_ue()?; + sps.conf_win_top_offset = r.read_ue()?; + sps.conf_win_bottom_offset = r.read_ue()?; + } + + sps.bit_depth_luma_minus8 = r.read_ue_max(6)?; + sps.bit_depth_chroma_minus8 = r.read_ue_max(6)?; + sps.log2_max_pic_order_cnt_lsb_minus4 = r.read_ue_max(12)?; + sps.sub_layer_ordering_info_present_flag = r.read_bit()?; + + { + let i = if sps.sub_layer_ordering_info_present_flag { + 0 + } else { + sps.max_sub_layers_minus1 + }; + + for j in i..=sps.max_sub_layers_minus1 { + sps.max_dec_pic_buffering_minus1[j as usize] = r.read_ue_max(16)?; + sps.max_num_reorder_pics[j as usize] = + r.read_ue_max(sps.max_dec_pic_buffering_minus1[j as usize] as _)?; + sps.max_latency_increase_plus1[j as usize] = r.read_ue_max(u32::MAX - 1)?; + } + } + + sps.log2_min_luma_coding_block_size_minus3 = r.read_ue_max(3)?; + sps.log2_diff_max_min_luma_coding_block_size = r.read_ue_max(6)?; + sps.log2_min_luma_transform_block_size_minus2 = r.read_ue_max(3)?; + sps.log2_diff_max_min_luma_transform_block_size = r.read_ue_max(3)?; + + // (7-10) + sps.min_cb_log2_size_y = u32::from(sps.log2_min_luma_coding_block_size_minus3 + 3); + // (7-11) + sps.ctb_log2_size_y = + sps.min_cb_log2_size_y + u32::from(sps.log2_diff_max_min_luma_coding_block_size); + // (7-12) + sps.ctb_size_y = 1 << sps.ctb_log2_size_y; + // (7-17) + sps.pic_height_in_ctbs_y = + (sps.pic_height_in_luma_samples as f64 / sps.ctb_size_y as f64).ceil() as u32; + // (7-15) + sps.pic_width_in_ctbs_y = + (sps.pic_width_in_luma_samples as f64 / sps.ctb_size_y as f64).ceil() as u32; + + sps.max_tb_log2_size_y = u32::from( + sps.log2_min_luma_transform_block_size_minus2 + + 2 + + sps.log2_diff_max_min_luma_transform_block_size, + ); + + sps.pic_size_in_samples_y = + u32::from(sps.pic_width_in_luma_samples) * u32::from(sps.pic_height_in_luma_samples); + + if sps.max_tb_log2_size_y > std::cmp::min(sps.ctb_log2_size_y, 5) { + return Err(format!( + "Invalid value for MaxTbLog2SizeY: {}", + sps.max_tb_log2_size_y + )); + } + + sps.pic_size_in_ctbs_y = sps.pic_width_in_ctbs_y * sps.pic_height_in_ctbs_y; + + sps.max_transform_hierarchy_depth_inter = r.read_ue_max(4)?; + sps.max_transform_hierarchy_depth_intra = r.read_ue_max(4)?; + + sps.scaling_list_enabled_flag = r.read_bit()?; + if sps.scaling_list_enabled_flag { + sps.scaling_list_data_present_flag = r.read_bit()?; + if sps.scaling_list_data_present_flag { + Self::parse_scaling_list_data(&mut sps.scaling_list, &mut r)?; + } + } + + sps.amp_enabled_flag = r.read_bit()?; + sps.sample_adaptive_offset_enabled_flag = r.read_bit()?; + + sps.pcm_enabled_flag = r.read_bit()?; + if sps.pcm_enabled_flag { + sps.pcm_sample_bit_depth_luma_minus1 = r.read_bits(4)?; + sps.pcm_sample_bit_depth_chroma_minus1 = r.read_bits(4)?; + sps.log2_min_pcm_luma_coding_block_size_minus3 = r.read_ue_max(2)?; + sps.log2_diff_max_min_pcm_luma_coding_block_size = r.read_ue_max(2)?; + sps.pcm_loop_filter_disabled_flag = r.read_bit()?; + } + + sps.num_short_term_ref_pic_sets = r.read_ue_max(64)?; + + for i in 0..sps.num_short_term_ref_pic_sets { + let mut st = ShortTermRefPicSet::default(); + Self::parse_short_term_ref_pic_set(&sps, &mut st, &mut r, i)?; + sps.short_term_ref_pic_set.push(st); + } + + sps.long_term_ref_pics_present_flag = r.read_bit()?; + if sps.long_term_ref_pics_present_flag { + sps.num_long_term_ref_pics_sps = r.read_ue_max(32)?; + for i in 0..usize::from(sps.num_long_term_ref_pics_sps) { + sps.lt_ref_pic_poc_lsb_sps[i] = + r.read_bits(usize::from(sps.log2_max_pic_order_cnt_lsb_minus4) + 4)?; + sps.used_by_curr_pic_lt_sps_flag[i] = r.read_bit()?; + } + } + + sps.temporal_mvp_enabled_flag = r.read_bit()?; + sps.strong_intra_smoothing_enabled_flag = r.read_bit()?; + + sps.vui_parameters_present_flag = r.read_bit()?; + if sps.vui_parameters_present_flag { + Self::parse_vui_parameters(&mut sps, &mut r)?; + } + + sps.extension_present_flag = r.read_bit()?; + if sps.extension_present_flag { + sps.range_extension_flag = r.read_bit()?; + if sps.range_extension_flag { + Self::parse_sps_range_extension(&mut sps, &mut r)?; + } + + let multilayer_extension_flag = r.read_bit()?; + if multilayer_extension_flag { + return Err("Multilayer extension not supported.".into()); + } + + let three_d_extension_flag = r.read_bit()?; + if three_d_extension_flag { + return Err("3D extension not supported.".into()); + } + + sps.scc_extension_flag = r.read_bit()?; + if sps.scc_extension_flag { + Self::parse_sps_scc_extension(&mut sps, &mut r)?; + } + } + + let shift = if sps.range_extension.high_precision_offsets_enabled_flag { + sps.bit_depth_luma_minus8 + 7 + } else { + 7 + }; + + sps.wp_offset_half_range_y = 1 << shift; + + let shift = if sps.range_extension.high_precision_offsets_enabled_flag { + sps.bit_depth_chroma_minus8 + 7 + } else { + 7 + }; + + sps.wp_offset_half_range_c = 1 << shift; + + log::debug!( + "Parsed SPS({}), resolution: ({}, {}): NAL size was {}", + sps.seq_parameter_set_id, + sps.width(), + sps.height(), + nalu.size + ); + + if self.active_spses.keys().len() >= MAX_SPS_COUNT { + return Err("Broken data: Number of active SPSs > MAX_SPS_COUNT".into()); + } + + let key = sps.seq_parameter_set_id; + let sps = Rc::new(sps); + self.active_spses.remove(&key); + Ok(self.active_spses.entry(key).or_insert(sps)) + } + + fn parse_pps_scc_extension(pps: &mut Pps, sps: &Sps, r: &mut BitReader) -> Result<(), String> { + let scc = &mut pps.scc_extension; + scc.curr_pic_ref_enabled_flag = r.read_bit()?; + scc.residual_adaptive_colour_transform_enabled_flag = r.read_bit()?; + if scc.residual_adaptive_colour_transform_enabled_flag { + scc.slice_act_qp_offsets_present_flag = r.read_bit()?; + scc.act_y_qp_offset_plus5 = r.read_se_bounded(-7, 17)?; + scc.act_cb_qp_offset_plus5 = r.read_se_bounded(-7, 17)?; + scc.act_cr_qp_offset_plus3 = r.read_se_bounded(-9, 15)?; + } + + scc.palette_predictor_initializers_present_flag = r.read_bit()?; + if scc.palette_predictor_initializers_present_flag { + let max = sps.scc_extension.palette_max_size + + sps.scc_extension.delta_palette_max_predictor_size; + scc.num_palette_predictor_initializers = r.read_ue_max(max.into())?; + if scc.num_palette_predictor_initializers > 0 { + scc.monochrome_palette_flag = r.read_bit()?; + scc.luma_bit_depth_entry_minus8 = r.read_ue_bounded( + sps.bit_depth_luma_minus8.into(), + sps.bit_depth_luma_minus8.into(), + )?; + if !scc.monochrome_palette_flag { + scc.chroma_bit_depth_entry_minus8 = r.read_ue_bounded( + sps.bit_depth_chroma_minus8.into(), + sps.bit_depth_chroma_minus8.into(), + )?; + } + + let num_comps = if scc.monochrome_palette_flag { 1 } else { 3 }; + for comp in 0..num_comps { + let num_bits = if comp == 0 { + scc.luma_bit_depth_entry_minus8 + 8 + } else { + scc.chroma_bit_depth_entry_minus8 + 8 + }; + for i in 0..usize::from(scc.num_palette_predictor_initializers) { + scc.palette_predictor_initializer[comp][i] = + r.read_bits(num_bits.into())?; + } + } + } + } + Ok(()) + } + + fn parse_pps_range_extension( + pps: &mut Pps, + sps: &Sps, + r: &mut BitReader, + ) -> Result<(), String> { + let rext = &mut pps.range_extension; + + if pps.transform_skip_enabled_flag { + rext.log2_max_transform_skip_block_size_minus2 = + r.read_ue_max(sps.max_tb_log2_size_y - 2)?; + } + + rext.cross_component_prediction_enabled_flag = r.read_bit()?; + rext.chroma_qp_offset_list_enabled_flag = r.read_bit()?; + if rext.chroma_qp_offset_list_enabled_flag { + rext.diff_cu_chroma_qp_offset_depth = r.read_ue()?; + rext.chroma_qp_offset_list_len_minus1 = r.read_ue_max(5)?; + for i in 0..=rext.chroma_qp_offset_list_len_minus1 as usize { + rext.cb_qp_offset_list[i] = r.read_se_bounded(-12, 12)?; + rext.cr_qp_offset_list[i] = r.read_se_bounded(-12, 12)?; + } + } + + let bit_depth_y = sps.bit_depth_luma_minus8 + 8; + let max = u32::from(std::cmp::max(0, bit_depth_y - 10)); + + rext.log2_sao_offset_scale_luma = r.read_ue_max(max)?; + rext.log2_sao_offset_scale_chroma = r.read_ue_max(max)?; + + Ok(()) + } + + /// Parse a PPS NALU. + pub fn parse_pps(&mut self, nalu: &Nalu) -> Result<&Pps, String> { + if !matches!(nalu.header.type_, NaluType::PpsNut) { + return Err(format!( + "Invalid NALU type, expected {:?}, got {:?}", + NaluType::PpsNut, + nalu.header.type_ + )); + } + + let data = nalu.as_ref(); + let header = &nalu.header; + let hdr_len = header.len(); + // Skip the header + let mut r = BitReader::new(&data[hdr_len..], true); + + let pic_parameter_set_id = r.read_ue_max(MAX_PPS_COUNT as u32 - 1)?; + let seq_parameter_set_id = r.read_ue_max(MAX_SPS_COUNT as u32 - 1)?; + + let sps = self.get_sps(seq_parameter_set_id).ok_or::(format!( + "Could not get SPS for seq_parameter_set_id {}", + seq_parameter_set_id + ))?; + + let mut pps = Pps { + pic_parameter_set_id, + seq_parameter_set_id, + dependent_slice_segments_enabled_flag: Default::default(), + output_flag_present_flag: Default::default(), + num_extra_slice_header_bits: Default::default(), + sign_data_hiding_enabled_flag: Default::default(), + cabac_init_present_flag: Default::default(), + num_ref_idx_l0_default_active_minus1: Default::default(), + num_ref_idx_l1_default_active_minus1: Default::default(), + init_qp_minus26: Default::default(), + constrained_intra_pred_flag: Default::default(), + transform_skip_enabled_flag: Default::default(), + cu_qp_delta_enabled_flag: Default::default(), + diff_cu_qp_delta_depth: Default::default(), + cb_qp_offset: Default::default(), + cr_qp_offset: Default::default(), + slice_chroma_qp_offsets_present_flag: Default::default(), + weighted_pred_flag: Default::default(), + weighted_bipred_flag: Default::default(), + transquant_bypass_enabled_flag: Default::default(), + tiles_enabled_flag: Default::default(), + entropy_coding_sync_enabled_flag: Default::default(), + num_tile_columns_minus1: Default::default(), + num_tile_rows_minus1: Default::default(), + uniform_spacing_flag: true, + column_width_minus1: Default::default(), + row_height_minus1: Default::default(), + loop_filter_across_tiles_enabled_flag: true, + loop_filter_across_slices_enabled_flag: Default::default(), + deblocking_filter_control_present_flag: Default::default(), + deblocking_filter_override_enabled_flag: Default::default(), + deblocking_filter_disabled_flag: Default::default(), + beta_offset_div2: Default::default(), + tc_offset_div2: Default::default(), + scaling_list_data_present_flag: Default::default(), + scaling_list: Default::default(), + lists_modification_present_flag: Default::default(), + log2_parallel_merge_level_minus2: Default::default(), + slice_segment_header_extension_present_flag: Default::default(), + extension_present_flag: Default::default(), + range_extension_flag: Default::default(), + range_extension: Default::default(), + qp_bd_offset_y: Default::default(), + scc_extension: Default::default(), + scc_extension_flag: Default::default(), + sps: Rc::clone(sps), + }; + + pps.dependent_slice_segments_enabled_flag = r.read_bit()?; + pps.output_flag_present_flag = r.read_bit()?; + pps.num_extra_slice_header_bits = r.read_bits(3)?; + pps.sign_data_hiding_enabled_flag = r.read_bit()?; + pps.cabac_init_present_flag = r.read_bit()?; + + // 7.4.7.1 + pps.num_ref_idx_l0_default_active_minus1 = r.read_ue_max(14)?; + pps.num_ref_idx_l1_default_active_minus1 = r.read_ue_max(14)?; + + // (7-5) + let qp_bd_offset_y = 6 * i32::from(sps.bit_depth_luma_minus8); + + pps.init_qp_minus26 = r.read_se_bounded(-(26 + qp_bd_offset_y), 25)?; + pps.qp_bd_offset_y = qp_bd_offset_y as u32; + pps.constrained_intra_pred_flag = r.read_bit()?; + pps.transform_skip_enabled_flag = r.read_bit()?; + pps.cu_qp_delta_enabled_flag = r.read_bit()?; + + if pps.cu_qp_delta_enabled_flag { + pps.diff_cu_qp_delta_depth = + r.read_ue_max(u32::from(sps.log2_diff_max_min_luma_coding_block_size))?; + } + + pps.cb_qp_offset = r.read_se_bounded(-12, 12)?; + pps.cr_qp_offset = r.read_se_bounded(-12, 12)?; + + pps.slice_chroma_qp_offsets_present_flag = r.read_bit()?; + pps.weighted_pred_flag = r.read_bit()?; + pps.weighted_bipred_flag = r.read_bit()?; + pps.transquant_bypass_enabled_flag = r.read_bit()?; + pps.tiles_enabled_flag = r.read_bit()?; + pps.entropy_coding_sync_enabled_flag = r.read_bit()?; + + // A mix of the rbsp data and the algorithm in 6.5.1 + if pps.tiles_enabled_flag { + pps.num_tile_columns_minus1 = r.read_ue_max(sps.pic_width_in_ctbs_y - 1)?; + pps.num_tile_rows_minus1 = r.read_ue_max(sps.pic_height_in_ctbs_y - 1)?; + pps.uniform_spacing_flag = r.read_bit()?; + if !pps.uniform_spacing_flag { + pps.column_width_minus1[usize::from(pps.num_tile_columns_minus1)] = + sps.pic_width_in_ctbs_y - 1; + + for i in 0..usize::from(pps.num_tile_columns_minus1) { + pps.column_width_minus1[i] = r.read_ue_max( + pps.column_width_minus1[usize::from(pps.num_tile_columns_minus1)] - 1, + )?; + pps.column_width_minus1[usize::from(pps.num_tile_columns_minus1)] -= + pps.column_width_minus1[i] + 1; + } + + pps.row_height_minus1[usize::from(pps.num_tile_rows_minus1)] = + sps.pic_height_in_ctbs_y - 1; + + for i in 0..usize::from(pps.num_tile_rows_minus1) { + pps.row_height_minus1[i] = r.read_ue_max( + pps.row_height_minus1[usize::from(pps.num_tile_rows_minus1)] - 1, + )?; + pps.row_height_minus1[usize::from(pps.num_tile_rows_minus1)] -= + pps.row_height_minus1[i] + 1; + } + } else { + let nrows = u32::from(pps.num_tile_rows_minus1) + 1; + let ncols = u32::from(pps.num_tile_columns_minus1) + 1; + + for j in 0..ncols { + pps.column_width_minus1[j as usize] = ((j + 1) * sps.pic_width_in_ctbs_y) + / ncols + - j * sps.pic_width_in_ctbs_y / ncols + - 1; + } + + for j in 0..nrows { + pps.row_height_minus1[j as usize] = ((j + 1) * sps.pic_height_in_ctbs_y) + / nrows + - j * sps.pic_height_in_ctbs_y / nrows + - 1; + } + } + + pps.loop_filter_across_tiles_enabled_flag = r.read_bit()?; + } + + pps.loop_filter_across_slices_enabled_flag = r.read_bit()?; + pps.deblocking_filter_control_present_flag = r.read_bit()?; + + if pps.deblocking_filter_control_present_flag { + pps.deblocking_filter_override_enabled_flag = r.read_bit()?; + pps.deblocking_filter_disabled_flag = r.read_bit()?; + if !pps.deblocking_filter_disabled_flag { + pps.beta_offset_div2 = r.read_se_bounded(-6, 6)?; + pps.tc_offset_div2 = r.read_se_bounded(-6, 6)?; + } + } + + pps.scaling_list_data_present_flag = r.read_bit()?; + + if pps.scaling_list_data_present_flag { + Self::parse_scaling_list_data(&mut pps.scaling_list, &mut r)?; + } else { + for size_id in 0..4 { + let mut matrix_id = 0; + while matrix_id < 6 { + Self::fill_default_scaling_list(&mut pps.scaling_list, size_id, matrix_id); + let step = if size_id == 3 { 3 } else { 1 }; + matrix_id += step; + } + } + } + + pps.lists_modification_present_flag = r.read_bit()?; + pps.log2_parallel_merge_level_minus2 = r.read_ue_max(sps.ctb_log2_size_y - 2)?; + pps.slice_segment_header_extension_present_flag = r.read_bit()?; + + pps.extension_present_flag = r.read_bit()?; + if pps.extension_present_flag { + pps.range_extension_flag = r.read_bit()?; + + if pps.range_extension_flag { + Self::parse_pps_range_extension(&mut pps, sps, &mut r)?; + } + + let multilayer_extension_flag = r.read_bit()?; + if multilayer_extension_flag { + return Err("Multilayer extension is not supported".into()); + } + + let three_d_extension_flag = r.read_bit()?; + if three_d_extension_flag { + return Err("3D extension is not supported".into()); + } + + pps.scc_extension_flag = r.read_bit()?; + if pps.scc_extension_flag { + Self::parse_pps_scc_extension(&mut pps, sps, &mut r)?; + } + + r.skip_bits(4)?; // pps_extension_4bits + } + + log::debug!( + "Parsed PPS({}), NAL size was {}", + pps.pic_parameter_set_id, + nalu.size + ); + + if self.active_ppses.keys().len() >= MAX_PPS_COUNT { + return Err("Broken Data: number of active PPSs > MAX_PPS_COUNT".into()); + } + + let key = pps.pic_parameter_set_id; + let pps = Rc::new(pps); + self.active_ppses.remove(&key); + Ok(self.active_ppses.entry(key).or_insert(pps)) + } + + fn parse_pred_weight_table( + hdr: &mut SliceHeader, + r: &mut BitReader, + sps: &Sps, + ) -> Result<(), String> { + let pwt = &mut hdr.pred_weight_table; + + pwt.luma_log2_weight_denom = r.read_ue_max(7)?; + if sps.chroma_array_type != 0 { + pwt.delta_chroma_log2_weight_denom = r.read_se()?; + pwt.chroma_log2_weight_denom = (pwt.luma_log2_weight_denom as i32 + + pwt.delta_chroma_log2_weight_denom as i32) + .try_into() + .map_err(|_| { + String::from("Integer overflow on chroma_log2_weight_denom calculation") + })?; + } + + for i in 0..=usize::from(hdr.num_ref_idx_l0_active_minus1) { + pwt.luma_weight_l0_flag[i] = r.read_bit()?; + } + + if sps.chroma_array_type != 0 { + for i in 0..=usize::from(hdr.num_ref_idx_l0_active_minus1) { + pwt.chroma_weight_l0_flag[i] = r.read_bit()?; + } + } + + for i in 0..=usize::from(hdr.num_ref_idx_l0_active_minus1) { + if pwt.luma_weight_l0_flag[i] { + pwt.delta_luma_weight_l0[i] = r.read_se_bounded(-128, 127)?; + pwt.luma_offset_l0[i] = r.read_se_bounded(-128, 127)?; + } + + if pwt.chroma_weight_l0_flag[i] { + for j in 0..2 { + pwt.delta_chroma_weight_l0[i][j] = r.read_se_bounded(-128, 127)?; + pwt.delta_chroma_offset_l0[i][j] = r.read_se_bounded( + -4 * sps.wp_offset_half_range_c as i32, + 4 * sps.wp_offset_half_range_c as i32 - 1, + )?; + } + } + } + + if hdr.type_.is_b() { + for i in 0..=usize::from(hdr.num_ref_idx_l1_active_minus1) { + pwt.luma_weight_l1_flag[i] = r.read_bit()?; + } + + if sps.chroma_format_idc != 0 { + for i in 0..=usize::from(hdr.num_ref_idx_l1_active_minus1) { + pwt.chroma_weight_l1_flag[i] = r.read_bit()?; + } + } + + for i in 0..=usize::from(hdr.num_ref_idx_l1_active_minus1) { + if pwt.luma_weight_l1_flag[i] { + pwt.delta_luma_weight_l1[i] = r.read_se_bounded(-128, 127)?; + pwt.luma_offset_l1[i] = r.read_se_bounded(-128, 127)?; + } + + if pwt.chroma_weight_l1_flag[i] { + for j in 0..2 { + pwt.delta_chroma_weight_l1[i][j] = r.read_se_bounded(-128, 127)?; + pwt.delta_chroma_offset_l1[i][j] = r.read_se_bounded( + -4 * sps.wp_offset_half_range_c as i32, + 4 * sps.wp_offset_half_range_c as i32 - 1, + )?; + } + } + } + } + + Ok(()) + } + + fn parse_ref_pic_lists_modification( + hdr: &mut SliceHeader, + r: &mut BitReader, + ) -> Result<(), String> { + let rplm = &mut hdr.ref_pic_list_modification; + + rplm.ref_pic_list_modification_flag_l0 = r.read_bit()?; + if rplm.ref_pic_list_modification_flag_l0 { + for _ in 0..=hdr.num_ref_idx_l0_active_minus1 { + let num_bits = (hdr.num_pic_total_curr as f64).log2().ceil() as _; + + let entry = r.read_bits(num_bits)?; + + if entry > hdr.num_pic_total_curr - 1 { + return Err(format!( + "Invalid list_entry_l0 {}, expected at max NumPicTotalCurr - 1: {}", + entry, + hdr.num_pic_total_curr - 1 + )); + } + + rplm.list_entry_l0.push(entry); + } + } + + if hdr.type_.is_b() { + rplm.ref_pic_list_modification_flag_l1 = r.read_bit()?; + if rplm.ref_pic_list_modification_flag_l1 { + for _ in 0..=hdr.num_ref_idx_l1_active_minus1 { + let num_bits = (hdr.num_pic_total_curr as f64).log2().ceil() as _; + + let entry = r.read_bits(num_bits)?; + + if entry > hdr.num_pic_total_curr - 1 { + return Err(format!( + "Invalid list_entry_l1 {}, expected at max NumPicTotalCurr - 1: {}", + entry, + hdr.num_pic_total_curr - 1 + )); + } + + rplm.list_entry_l1.push(entry); + } + } + } + + Ok(()) + } + + /// Further sets default values given `sps` and `pps`. + pub fn slice_header_set_defaults(hdr: &mut SliceHeader, sps: &Sps, pps: &Pps) { + // Set some defaults that can't be defined in Default::default(). + hdr.deblocking_filter_disabled_flag = pps.deblocking_filter_disabled_flag; + hdr.beta_offset_div2 = pps.beta_offset_div2; + hdr.tc_offset_div2 = pps.tc_offset_div2; + hdr.loop_filter_across_slices_enabled_flag = pps.loop_filter_across_slices_enabled_flag; + hdr.curr_rps_idx = sps.num_short_term_ref_pic_sets; + hdr.use_integer_mv_flag = sps.scc_extension.motion_vector_resolution_control_idc != 0; + } + + /// Parses a slice header from a slice NALU. + pub fn parse_slice_header<'a>(&mut self, nalu: Nalu<'a>) -> Result, String> { + if !matches!( + nalu.header.type_, + NaluType::TrailN + | NaluType::TrailR + | NaluType::TsaN + | NaluType::TsaR + | NaluType::StsaN + | NaluType::StsaR + | NaluType::RadlN + | NaluType::RadlR + | NaluType::RaslN + | NaluType::RaslR + | NaluType::BlaWLp + | NaluType::BlaWRadl + | NaluType::BlaNLp + | NaluType::IdrWRadl + | NaluType::IdrNLp + | NaluType::CraNut, + ) { + return Err(format!( + "Invalid NALU type: {:?} is not a slice NALU", + nalu.header.type_ + )); + } + + let data = nalu.as_ref(); + let nalu_header = &nalu.header; + let hdr_len = nalu_header.len(); + // Skip the header + let mut r = BitReader::new(&data[hdr_len..], true); + + let mut hdr = SliceHeader { + first_slice_segment_in_pic_flag: r.read_bit()?, + ..Default::default() + }; + + if nalu.header.type_.is_irap() { + hdr.no_output_of_prior_pics_flag = r.read_bit()?; + } + + hdr.pic_parameter_set_id = r.read_ue_max(63)?; + + let pps = self + .get_pps(hdr.pic_parameter_set_id) + .ok_or::(format!( + "Could not get PPS for pic_parameter_set_id {}", + hdr.pic_parameter_set_id + ))?; + + let sps = &pps.sps; + + Self::slice_header_set_defaults(&mut hdr, sps, pps); + + if !hdr.first_slice_segment_in_pic_flag { + if pps.dependent_slice_segments_enabled_flag { + hdr.dependent_slice_segment_flag = r.read_bit()?; + } + + let num_bits = (sps.pic_size_in_ctbs_y as f64).log2().ceil() as _; + hdr.segment_address = r.read_bits(num_bits)?; + + if hdr.segment_address > sps.pic_size_in_ctbs_y - 1 { + return Err(format!( + "Invalid slice_segment_address {}", + hdr.segment_address + )); + } + } + + if !hdr.dependent_slice_segment_flag { + r.skip_bits(usize::from(pps.num_extra_slice_header_bits))?; + + let slice_type: u32 = r.read_ue()?; + hdr.type_ = SliceType::try_from(slice_type)?; + + if pps.output_flag_present_flag { + hdr.pic_output_flag = r.read_bit()?; + } + + if sps.separate_colour_plane_flag { + hdr.colour_plane_id = r.read_bits(2)?; + } + + if !matches!(nalu_header.type_, NaluType::IdrWRadl | NaluType::IdrNLp) { + let num_bits = usize::from(sps.log2_max_pic_order_cnt_lsb_minus4 + 4); + hdr.pic_order_cnt_lsb = r.read_bits(num_bits)?; + + if u32::from(hdr.pic_order_cnt_lsb) + > 2u32.pow(u32::from(sps.log2_max_pic_order_cnt_lsb_minus4 + 4)) + { + return Err(format!( + "Invalid pic_order_cnt_lsb {}", + hdr.pic_order_cnt_lsb + )); + } + + hdr.short_term_ref_pic_set_sps_flag = r.read_bit()?; + + if !hdr.short_term_ref_pic_set_sps_flag { + let epb_before = r.num_epb(); + let bits_left_before = r.num_bits_left(); + + let st_rps_idx = sps.num_short_term_ref_pic_sets; + + Self::parse_short_term_ref_pic_set( + sps, + &mut hdr.short_term_ref_pic_set, + &mut r, + st_rps_idx, + )?; + + hdr.st_rps_bits = ((bits_left_before - r.num_bits_left()) + - 8 * (r.num_epb() - epb_before)) + as u32; + } else if sps.num_short_term_ref_pic_sets > 1 { + let num_bits = (sps.num_short_term_ref_pic_sets as f64).log2().ceil() as _; + hdr.short_term_ref_pic_set_idx = r.read_bits(num_bits)?; + + if hdr.short_term_ref_pic_set_idx > sps.num_short_term_ref_pic_sets - 1 { + return Err(format!( + "Invalid short_term_ref_pic_set_idx {}", + hdr.short_term_ref_pic_set_idx + )); + } + } + + if hdr.short_term_ref_pic_set_sps_flag { + hdr.curr_rps_idx = hdr.short_term_ref_pic_set_idx; + } + + if sps.long_term_ref_pics_present_flag { + if sps.num_long_term_ref_pics_sps > 0 { + hdr.num_long_term_sps = + r.read_ue_max(u32::from(sps.num_long_term_ref_pics_sps))?; + } + + hdr.num_long_term_pics = r.read_ue_max( + MAX_LONG_TERM_REF_PIC_SETS as u32 - u32::from(hdr.num_long_term_sps), + )?; + + let num_lt = hdr.num_long_term_sps + hdr.num_long_term_pics; + // The long-term RPS arrays in SliceHeader (poc_lsb_lt, + // used_by_curr_pic_lt, delta_poc_msb_present_flag, + // delta_poc_msb_cycle_lt, lt_idx_sps) hold 16 entries — the DPB + // bound — while the reads above admit up to + // MAX_LONG_TERM_REF_PIC_SETS (32) combined; the loop below would + // index out of bounds on such a header. See PROVENANCE.md + // deviation 7. + if usize::from(num_lt) > hdr.poc_lsb_lt.len() { + return Err(format!( + "Invalid num_long_term_sps + num_long_term_pics: {}", + num_lt + )); + } + for i in 0..usize::from(num_lt) { + // The variables `PocLsbLt[ i ]` and `UsedByCurrPicLt[ i ]` are derived as follows: + // + // – If i is less than num_long_term_sps, `PocLsbLt[ i ]` is set equal to + // lt_ref_pic_poc_lsb_sps[ `lt_idx_sps[ i ]` ] and `UsedByCurrPicLt[ i ]` is set equal + // to used_by_curr_pic_lt_sps_flag[ `lt_idx_sps[ i ]` ]. + // + // – Otherwise, `PocLsbLt[ i ]` + // is set equal to `poc_lsb_lt[ i ]` and `UsedByCurrPicLt[ i ]` is set equal to + // `used_by_curr_pic_lt_flag[ i ]`. + if i < usize::from(hdr.num_long_term_sps) { + if sps.num_long_term_ref_pics_sps > 1 { + let num_bits = + (sps.num_long_term_ref_pics_sps as f64).log2().ceil() as _; + + hdr.lt_idx_sps[i] = r.read_bits(num_bits)?; + + if hdr.lt_idx_sps[i] > sps.num_long_term_ref_pics_sps - 1 { + return Err(format!( + "Invalid lt_idx_sps[{}] {}", + i, hdr.lt_idx_sps[i] + )); + } + } + + hdr.poc_lsb_lt[i] = + sps.lt_ref_pic_poc_lsb_sps[usize::from(hdr.lt_idx_sps[i])]; + hdr.used_by_curr_pic_lt[i] = + sps.used_by_curr_pic_lt_sps_flag[usize::from(hdr.lt_idx_sps[i])]; + } else { + let num_bits = usize::from(sps.log2_max_pic_order_cnt_lsb_minus4) + 4; + hdr.poc_lsb_lt[i] = r.read_bits(num_bits)?; + hdr.used_by_curr_pic_lt[i] = r.read_bit()?; + } + + hdr.delta_poc_msb_present_flag[i] = r.read_bit()?; + if hdr.delta_poc_msb_present_flag[i] { + // The value of `delta_poc_msb_cycle_lt[ i ]` shall be + // in the range of 0 to 2(32 − + // log2_max_pic_order_cnt_lsb_minus4 − 4 ), + // inclusive. When `delta_poc_msb_cycle_lt[ i ]` is + // not present, it is inferred to be equal to 0. + let max = + 2u32.pow(32 - u32::from(sps.log2_max_pic_order_cnt_lsb_minus4) - 4); + hdr.delta_poc_msb_cycle_lt[i] = r.read_ue_max(max)?; + } + // Equation 7-52 (simplified) + if i != 0 && i != usize::from(hdr.num_long_term_sps) { + hdr.delta_poc_msb_cycle_lt[i] += hdr.delta_poc_msb_cycle_lt[i - 1]; + } + } + } + + if sps.temporal_mvp_enabled_flag { + hdr.temporal_mvp_enabled_flag = r.read_bit()?; + } + } + + if sps.sample_adaptive_offset_enabled_flag { + hdr.sao_luma_flag = r.read_bit()?; + if sps.chroma_array_type != 0 { + hdr.sao_chroma_flag = r.read_bit()?; + } + } + + if hdr.type_.is_p() || hdr.type_.is_b() { + hdr.num_ref_idx_active_override_flag = r.read_bit()?; + if hdr.num_ref_idx_active_override_flag { + hdr.num_ref_idx_l0_active_minus1 = r.read_ue_max(MAX_REF_IDX_ACTIVE - 1)?; + if hdr.type_.is_b() { + hdr.num_ref_idx_l1_active_minus1 = r.read_ue_max(MAX_REF_IDX_ACTIVE - 1)?; + } + } else { + hdr.num_ref_idx_l0_active_minus1 = pps.num_ref_idx_l0_default_active_minus1; + hdr.num_ref_idx_l1_active_minus1 = pps.num_ref_idx_l1_default_active_minus1; + } + + // 7-57 + let mut num_pic_total_curr = 0; + let rps = if hdr.short_term_ref_pic_set_sps_flag { + sps.short_term_ref_pic_set + .get(usize::from(hdr.curr_rps_idx)) + .ok_or::("Invalid RPS".into())? + } else { + &hdr.short_term_ref_pic_set + }; + + for i in 0..usize::from(rps.num_negative_pics) { + if rps.used_by_curr_pic_s0[i] { + num_pic_total_curr += 1; + } + } + + for i in 0..usize::from(rps.num_positive_pics) { + if rps.used_by_curr_pic_s1[i] { + num_pic_total_curr += 1; + } + } + + for i in 0..usize::from(hdr.num_long_term_sps + hdr.num_long_term_pics) { + if hdr.used_by_curr_pic_lt[i] { + num_pic_total_curr += 1; + } + } + + if pps.scc_extension.curr_pic_ref_enabled_flag { + num_pic_total_curr += 1; + } + + hdr.num_pic_total_curr = num_pic_total_curr; + + if pps.lists_modification_present_flag && hdr.num_pic_total_curr > 1 { + Self::parse_ref_pic_lists_modification(&mut hdr, &mut r)?; + } + + if hdr.type_.is_b() { + hdr.mvd_l1_zero_flag = r.read_bit()?; + } + + if pps.cabac_init_present_flag { + hdr.cabac_init_flag = r.read_bit()?; + } + + if hdr.temporal_mvp_enabled_flag { + if hdr.type_.is_b() { + hdr.collocated_from_l0_flag = r.read_bit()?; + } + + if (hdr.collocated_from_l0_flag && hdr.num_ref_idx_l0_active_minus1 > 0) + || (!hdr.collocated_from_l0_flag && hdr.num_ref_idx_l1_active_minus1 > 0) + { + let max = if (hdr.type_.is_p() || hdr.type_.is_b()) + && hdr.collocated_from_l0_flag + { + hdr.num_ref_idx_l0_active_minus1 + } else if hdr.type_.is_b() && !hdr.collocated_from_l0_flag { + hdr.num_ref_idx_l1_active_minus1 + } else { + return Err("Invalid value for collocated_ref_idx".into()); + }; + + { + hdr.collocated_ref_idx = r.read_ue_max(u32::from(max))?; + } + } + } + + if (pps.weighted_pred_flag && hdr.type_.is_p()) + || (pps.weighted_bipred_flag && hdr.type_.is_b()) + { + Self::parse_pred_weight_table(&mut hdr, &mut r, sps)?; + } + + hdr.five_minus_max_num_merge_cand = r.read_ue()?; + + if sps.scc_extension.motion_vector_resolution_control_idc == 2 { + hdr.use_integer_mv_flag = r.read_bit()?; + } + } + + hdr.qp_delta = r.read_se_bounded(-87, 77)?; + + let slice_qp_y = (26 + pps.init_qp_minus26 + hdr.qp_delta) as i32; + if slice_qp_y < -(pps.qp_bd_offset_y as i32) || slice_qp_y > 51 { + return Err(format!("Invalid slice_qp_delta: {}", hdr.qp_delta)); + } + + if pps.slice_chroma_qp_offsets_present_flag { + hdr.cb_qp_offset = r.read_se_bounded(-12, 12)?; + + let qp_offset = pps.cb_qp_offset + hdr.cb_qp_offset; + if !(-12..=12).contains(&qp_offset) { + return Err(format!( + "Invalid value for slice_cb_qp_offset: {}", + hdr.cb_qp_offset + )); + } + + hdr.cr_qp_offset = r.read_se_bounded(-12, 12)?; + + let qp_offset = pps.cr_qp_offset + hdr.cr_qp_offset; + if !(-12..=12).contains(&qp_offset) { + return Err(format!( + "Invalid value for slice_cr_qp_offset: {}", + hdr.cr_qp_offset + )); + } + } + + if pps.scc_extension.slice_act_qp_offsets_present_flag { + hdr.slice_act_y_qp_offset = r.read_se_bounded(-12, 12)?; + hdr.slice_act_cb_qp_offset = r.read_se_bounded(-12, 12)?; + hdr.slice_act_cr_qp_offset = r.read_se_bounded(-12, 12)?; + } + + if pps.range_extension.chroma_qp_offset_list_enabled_flag { + hdr.cu_chroma_qp_offset_enabled_flag = r.read_bit()?; + } + + if pps.deblocking_filter_override_enabled_flag { + hdr.deblocking_filter_override_flag = r.read_bit()?; + } + + if hdr.deblocking_filter_override_flag { + hdr.deblocking_filter_disabled_flag = r.read_bit()?; + if !hdr.deblocking_filter_disabled_flag { + hdr.beta_offset_div2 = r.read_se_bounded(-6, 6)?; + hdr.tc_offset_div2 = r.read_se_bounded(-6, 6)?; + } + } + + if pps.loop_filter_across_slices_enabled_flag + && (hdr.sao_luma_flag + || hdr.sao_chroma_flag + || !hdr.deblocking_filter_disabled_flag) + { + hdr.loop_filter_across_slices_enabled_flag = r.read_bit()?; + } + } + + if pps.tiles_enabled_flag || pps.entropy_coding_sync_enabled_flag { + let max = if !pps.tiles_enabled_flag && pps.entropy_coding_sync_enabled_flag { + sps.pic_height_in_ctbs_y - 1 + } else if pps.tiles_enabled_flag && !pps.entropy_coding_sync_enabled_flag { + u32::from((pps.num_tile_columns_minus1 + 1) * (pps.num_tile_rows_minus1 + 1) - 1) + } else { + (u32::from(pps.num_tile_columns_minus1) + 1) * sps.pic_height_in_ctbs_y - 1 + }; + + hdr.num_entry_point_offsets = r.read_ue_max(max)?; + if hdr.num_entry_point_offsets > 0 { + hdr.offset_len_minus1 = r.read_ue_max(31)?; + for i in 0..hdr.num_entry_point_offsets as usize { + let num_bits = usize::from(hdr.offset_len_minus1 + 1); + hdr.entry_point_offset_minus1[i] = r.read_bits(num_bits)?; + } + } + } + + if pps.slice_segment_header_extension_present_flag { + let segment_header_extension_length = r.read_ue_max(256)?; + for _ in 0..segment_header_extension_length { + r.skip_bits(8)?; // slice_segment_header_extension_data_byte[i] + } + } + + // byte_alignment() + r.skip_bits(1)?; // Alignment bit + let num_bits = r.num_bits_left() % 8; + r.skip_bits(num_bits)?; + + let epb = r.num_epb(); + hdr.header_bit_size = ((nalu.size - epb) * 8 - r.num_bits_left()) as u32; + + hdr.n_emulation_prevention_bytes = epb as u32; + + log::debug!( + "Parsed slice {:?}, NAL size was {}", + nalu_header.type_, + nalu.size + ); + + Ok(Slice { header: hdr, nalu }) + } + + /// Returns a previously parsed vps given `vps_id`, if any. + pub fn get_vps(&self, vps_id: u8) -> Option<&Rc> { + self.active_vpses.get(&vps_id) + } + + /// Returns a previously parsed sps given `sps_id`, if any. + pub fn get_sps(&self, sps_id: u8) -> Option<&Rc> { + self.active_spses.get(&sps_id) + } + + /// Returns a previously parsed pps given `pps_id`, if any. + pub fn get_pps(&self, pps_id: u8) -> Option<&Rc> { + self.active_ppses.get(&pps_id) + } +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use crate::codec::h264::nalu::Nalu; + use crate::codec::h265::parser::Level; + use crate::codec::h265::parser::NaluHeader; + use crate::codec::h265::parser::NaluType; + use crate::codec::h265::parser::Parser; + use crate::codec::h265::parser::SliceType; + + const STREAM_BEAR: &[u8] = include_bytes!("test_data/bear.h265"); + const STREAM_BEAR_NUM_NALUS: usize = 35; + + const STREAM_BBB: &[u8] = include_bytes!("test_data/bbb.h265"); + const STREAM_BBB_NUM_NALUS: usize = 64; + + const STREAM_TEST25FPS: &[u8] = include_bytes!("test_data/test-25fps.h265"); + const STREAM_TEST25FPS_NUM_NALUS: usize = 254; + + const STREAM_TEST_25_FPS_SLICE_0: &[u8] = + include_bytes!("test_data/test-25fps-h265-slice-data-0.bin"); + const STREAM_TEST_25_FPS_SLICE_1: &[u8] = + include_bytes!("test_data/test-25fps-h265-slice-data-1.bin"); + + fn dispatch_parse_call(parser: &mut Parser, nalu: Nalu) -> Result<(), String> { + match nalu.header.type_ { + NaluType::TrailN + | NaluType::TrailR + | NaluType::TsaN + | NaluType::TsaR + | NaluType::StsaN + | NaluType::StsaR + | NaluType::RadlN + | NaluType::RadlR + | NaluType::RaslN + | NaluType::RaslR + | NaluType::BlaWLp + | NaluType::BlaWRadl + | NaluType::BlaNLp + | NaluType::IdrWRadl + | NaluType::IdrNLp + | NaluType::CraNut => { + parser.parse_slice_header(nalu).unwrap(); + } + NaluType::VpsNut => { + parser.parse_vps(&nalu).unwrap(); + } + NaluType::SpsNut => { + parser.parse_sps(&nalu).unwrap(); + } + NaluType::PpsNut => { + parser.parse_pps(&nalu).unwrap(); + } + _ => { /* ignore */ } + } + Ok(()) + } + + fn find_nalu_by_type( + bitstream: &[u8], + nalu_type: NaluType, + mut nskip: i32, + ) -> Option> { + let mut cursor = Cursor::new(bitstream); + while let Ok(nalu) = Nalu::::next(&mut cursor) { + if nalu.header.type_ == nalu_type { + if nskip == 0 { + return Some(nalu); + } else { + nskip -= 1; + } + } + } + + None + } + + /// This test is adapted from chromium, available at media/video/h265_parser_unittest.cc + #[test] + fn parse_nalus_from_stream_file() { + let mut cursor = Cursor::new(STREAM_BEAR); + let mut num_nalus = 0; + while Nalu::::next(&mut cursor).is_ok() { + num_nalus += 1; + } + + assert_eq!(num_nalus, STREAM_BEAR_NUM_NALUS); + + let mut cursor = Cursor::new(STREAM_BBB); + let mut num_nalus = 0; + while Nalu::::next(&mut cursor).is_ok() { + num_nalus += 1; + } + + assert_eq!(num_nalus, STREAM_BBB_NUM_NALUS); + + let mut cursor = Cursor::new(STREAM_TEST25FPS); + let mut num_nalus = 0; + while Nalu::::next(&mut cursor).is_ok() { + num_nalus += 1; + } + + assert_eq!(num_nalus, STREAM_TEST25FPS_NUM_NALUS); + } + + /// Parse the syntax, making sure we can parse the files without crashing. + /// Does not check whether the parsed values are correct. + #[test] + fn parse_syntax_from_nals() { + let mut cursor = Cursor::new(STREAM_BBB); + let mut parser = Parser::default(); + + while let Ok(nalu) = Nalu::::next(&mut cursor) { + dispatch_parse_call(&mut parser, nalu).unwrap(); + } + + let mut cursor = Cursor::new(STREAM_BEAR); + let mut parser = Parser::default(); + + while let Ok(nalu) = Nalu::::next(&mut cursor) { + dispatch_parse_call(&mut parser, nalu).unwrap(); + } + + let mut cursor = Cursor::new(STREAM_TEST25FPS); + let mut parser = Parser::default(); + + while let Ok(nalu) = Nalu::::next(&mut cursor) { + dispatch_parse_call(&mut parser, nalu).unwrap(); + } + } + + /// Adapted from Chromium (media/video/h265_parser_unittest.cc::VpsParsing()) + #[test] + fn chromium_vps_parsing() { + let mut cursor = Cursor::new(STREAM_BEAR); + let mut parser = Parser::default(); + + let vps_nalu = Nalu::::next(&mut cursor).unwrap(); + let vps = parser.parse_vps(&vps_nalu).unwrap(); + + assert!(vps.base_layer_internal_flag); + assert!(vps.base_layer_available_flag); + assert_eq!(vps.max_layers_minus1, 0); + assert_eq!(vps.max_sub_layers_minus1, 0); + assert!(vps.temporal_id_nesting_flag); + assert_eq!(vps.profile_tier_level.general_profile_idc, 1); + assert_eq!(vps.profile_tier_level.general_level_idc, Level::L2); + assert_eq!(vps.max_dec_pic_buffering_minus1[0], 4); + assert_eq!(vps.max_num_reorder_pics[0], 2); + assert_eq!(vps.max_latency_increase_plus1[0], 0); + for i in 1..7 { + assert_eq!(vps.max_dec_pic_buffering_minus1[i], 0); + assert_eq!(vps.max_num_reorder_pics[i], 0); + assert_eq!(vps.max_latency_increase_plus1[i], 0); + } + assert_eq!(vps.max_layer_id, 0); + assert_eq!(vps.num_layer_sets_minus1, 0); + assert!(!vps.timing_info_present_flag); + } + + /// Adapted from Chromium (media/video/h265_parser_unittest.cc::SpsParsing()) + #[test] + fn chromium_sps_parsing() { + let mut parser = Parser::default(); + let sps_nalu = find_nalu_by_type(STREAM_BEAR, NaluType::SpsNut, 0).unwrap(); + let sps = parser.parse_sps(&sps_nalu).unwrap(); + + assert_eq!(sps.max_sub_layers_minus1, 0); + assert_eq!(sps.profile_tier_level.general_profile_idc, 1); + assert_eq!(sps.profile_tier_level.general_level_idc, Level::L2); + assert_eq!(sps.seq_parameter_set_id, 0); + assert_eq!(sps.chroma_format_idc, 1); + assert!(!sps.separate_colour_plane_flag); + assert_eq!(sps.pic_width_in_luma_samples, 320); + assert_eq!(sps.pic_height_in_luma_samples, 184); + assert_eq!(sps.conf_win_left_offset, 0); + assert_eq!(sps.conf_win_right_offset, 0); + assert_eq!(sps.conf_win_top_offset, 0); + assert_eq!(sps.conf_win_bottom_offset, 2); + assert_eq!(sps.bit_depth_luma_minus8, 0); + assert_eq!(sps.bit_depth_chroma_minus8, 0); + assert_eq!(sps.log2_max_pic_order_cnt_lsb_minus4, 4); + assert_eq!(sps.max_dec_pic_buffering_minus1[0], 4); + assert_eq!(sps.max_num_reorder_pics[0], 2); + assert_eq!(sps.max_latency_increase_plus1[0], 0); + for i in 1..7 { + assert_eq!(sps.max_dec_pic_buffering_minus1[i], 0); + assert_eq!(sps.max_num_reorder_pics[i], 0); + assert_eq!(sps.max_latency_increase_plus1[i], 0); + } + assert_eq!(sps.log2_min_luma_coding_block_size_minus3, 0); + assert_eq!(sps.log2_diff_max_min_luma_coding_block_size, 3); + assert_eq!(sps.log2_min_luma_transform_block_size_minus2, 0); + assert_eq!(sps.log2_diff_max_min_luma_transform_block_size, 3); + assert_eq!(sps.max_transform_hierarchy_depth_inter, 0); + assert_eq!(sps.max_transform_hierarchy_depth_intra, 0); + assert!(!sps.scaling_list_enabled_flag); + assert!(!sps.scaling_list_data_present_flag); + assert!(!sps.amp_enabled_flag); + assert!(sps.sample_adaptive_offset_enabled_flag); + assert!(!sps.pcm_enabled_flag); + assert_eq!(sps.pcm_sample_bit_depth_luma_minus1, 0); + assert_eq!(sps.pcm_sample_bit_depth_chroma_minus1, 0); + assert_eq!(sps.log2_min_pcm_luma_coding_block_size_minus3, 0); + assert_eq!(sps.log2_diff_max_min_pcm_luma_coding_block_size, 0); + assert!(!sps.pcm_loop_filter_disabled_flag); + assert_eq!(sps.num_short_term_ref_pic_sets, 0); + assert_eq!(sps.num_long_term_ref_pics_sps, 0); + assert!(sps.temporal_mvp_enabled_flag); + assert!(sps.strong_intra_smoothing_enabled_flag); + assert_eq!(sps.vui_parameters.sar_width, 0); + assert_eq!(sps.vui_parameters.sar_height, 0); + assert!(!sps.vui_parameters.video_full_range_flag); + assert!(!sps.vui_parameters.colour_description_present_flag); + + // Note: the original test has 0 for the three variables below, but they + // have valid defaults in the spec (i.e.: 2). + assert_eq!(sps.vui_parameters.colour_primaries, 2); + assert_eq!(sps.vui_parameters.transfer_characteristics, 2); + assert_eq!(sps.vui_parameters.matrix_coeffs, 2); + + assert_eq!(sps.vui_parameters.def_disp_win_left_offset, 0); + assert_eq!(sps.vui_parameters.def_disp_win_right_offset, 0); + assert_eq!(sps.vui_parameters.def_disp_win_top_offset, 0); + assert_eq!(sps.vui_parameters.def_disp_win_bottom_offset, 0); + } + + /// Adapted from Chromium (media/video/h265_parser_unittest.cc::PpsParsing()) + #[test] + fn chromium_pps_parsing() { + let mut parser = Parser::default(); + + // Have to parse the SPS to set up the parser's internal state. + let sps_nalu = find_nalu_by_type(STREAM_BEAR, NaluType::SpsNut, 0).unwrap(); + parser.parse_sps(&sps_nalu).unwrap(); + + let pps_nalu = find_nalu_by_type(STREAM_BEAR, NaluType::PpsNut, 0).unwrap(); + let pps = parser.parse_pps(&pps_nalu).unwrap(); + + assert_eq!(pps.pic_parameter_set_id, 0); + assert_eq!(pps.seq_parameter_set_id, 0); + assert!(!pps.dependent_slice_segments_enabled_flag); + assert!(!pps.output_flag_present_flag); + assert_eq!(pps.num_extra_slice_header_bits, 0); + assert!(pps.sign_data_hiding_enabled_flag); + assert!(!pps.cabac_init_present_flag); + assert_eq!(pps.num_ref_idx_l0_default_active_minus1, 0); + assert_eq!(pps.num_ref_idx_l1_default_active_minus1, 0); + assert_eq!(pps.init_qp_minus26, 0); + assert!(!pps.constrained_intra_pred_flag); + assert!(!pps.transform_skip_enabled_flag); + assert!(pps.cu_qp_delta_enabled_flag); + assert_eq!(pps.diff_cu_qp_delta_depth, 0); + assert_eq!(pps.cb_qp_offset, 0); + assert_eq!(pps.cr_qp_offset, 0); + assert!(!pps.slice_chroma_qp_offsets_present_flag); + assert!(pps.weighted_pred_flag); + assert!(!pps.weighted_bipred_flag); + assert!(!pps.transquant_bypass_enabled_flag); + assert!(!pps.tiles_enabled_flag); + assert!(pps.entropy_coding_sync_enabled_flag); + assert!(pps.loop_filter_across_tiles_enabled_flag); + assert!(!pps.scaling_list_data_present_flag); + assert!(!pps.lists_modification_present_flag); + assert_eq!(pps.log2_parallel_merge_level_minus2, 0); + assert!(!pps.slice_segment_header_extension_present_flag); + } + + /// Adapted from Chromium (media/video/h265_parser_unittest.cc::SliceHeaderParsing()) + #[test] + fn chromium_slice_header_parsing() { + let mut parser = Parser::default(); + + // Have to parse the SPS/VPS/PPS to set up the parser's internal state. + let vps_nalu = find_nalu_by_type(STREAM_BEAR, NaluType::VpsNut, 0).unwrap(); + parser.parse_vps(&vps_nalu).unwrap(); + + let sps_nalu = find_nalu_by_type(STREAM_BEAR, NaluType::SpsNut, 0).unwrap(); + parser.parse_sps(&sps_nalu).unwrap(); + + let pps_nalu = find_nalu_by_type(STREAM_BEAR, NaluType::PpsNut, 0).unwrap(); + parser.parse_pps(&pps_nalu).unwrap(); + + // Just like the Chromium test, do an IDR slice, then a non IDR slice. + let slice_nalu = find_nalu_by_type(STREAM_BEAR, NaluType::IdrWRadl, 0).unwrap(); + let slice = parser.parse_slice_header(slice_nalu).unwrap(); + let hdr = &slice.header; + assert!(hdr.first_slice_segment_in_pic_flag); + assert!(!hdr.no_output_of_prior_pics_flag); + assert_eq!(hdr.pic_parameter_set_id, 0); + assert!(!hdr.dependent_slice_segment_flag); + assert_eq!(hdr.type_, SliceType::I); + assert!(hdr.sao_luma_flag); + assert!(hdr.sao_chroma_flag); + assert_eq!(hdr.qp_delta, 8); + assert!(hdr.loop_filter_across_slices_enabled_flag); + + let slice_nalu = find_nalu_by_type(STREAM_BEAR, NaluType::TrailR, 0).unwrap(); + let slice = parser.parse_slice_header(slice_nalu).unwrap(); + let hdr = &slice.header; + assert!(hdr.first_slice_segment_in_pic_flag); + assert_eq!(hdr.pic_parameter_set_id, 0); + assert!(!hdr.dependent_slice_segment_flag); + assert_eq!(hdr.type_, SliceType::P); + assert_eq!(hdr.pic_order_cnt_lsb, 4); + assert!(!hdr.short_term_ref_pic_set_sps_flag); + assert_eq!(hdr.short_term_ref_pic_set.num_negative_pics, 1); + assert_eq!(hdr.short_term_ref_pic_set.num_positive_pics, 0); + assert_eq!(hdr.short_term_ref_pic_set.delta_poc_s0[0], -4); + assert!(hdr.short_term_ref_pic_set.used_by_curr_pic_s0[0]); + assert!(hdr.temporal_mvp_enabled_flag); + assert!(hdr.sao_luma_flag); + assert!(hdr.sao_chroma_flag); + assert!(!hdr.num_ref_idx_active_override_flag); + assert_eq!(hdr.pred_weight_table.luma_log2_weight_denom, 0); + assert_eq!(hdr.pred_weight_table.delta_chroma_log2_weight_denom, 7); + assert_eq!(hdr.pred_weight_table.delta_luma_weight_l0[0], 0); + assert_eq!(hdr.pred_weight_table.luma_offset_l0[0], -2); + assert_eq!(hdr.pred_weight_table.delta_chroma_weight_l0[0][0], -9); + assert_eq!(hdr.pred_weight_table.delta_chroma_weight_l0[0][1], -9); + assert_eq!(hdr.pred_weight_table.delta_chroma_offset_l0[0][0], 0); + assert_eq!(hdr.pred_weight_table.delta_chroma_offset_l0[0][1], 0); + assert_eq!(hdr.five_minus_max_num_merge_cand, 3); + assert_eq!(hdr.qp_delta, 8); + assert!(hdr.loop_filter_across_slices_enabled_flag); + } + + /// A custom test for VPS parsing with data manually extracted from + /// GStreamer using GDB. + #[test] + fn test25fps_vps_header_parsing() { + let mut cursor = Cursor::new(STREAM_TEST25FPS); + let mut parser = Parser::default(); + + let vps_nalu = Nalu::::next(&mut cursor).unwrap(); + let vps = parser.parse_vps(&vps_nalu).unwrap(); + assert!(vps.base_layer_internal_flag); + assert!(vps.base_layer_available_flag); + assert_eq!(vps.max_layers_minus1, 0); + assert_eq!(vps.max_sub_layers_minus1, 0); + assert!(vps.temporal_id_nesting_flag); + assert_eq!(vps.profile_tier_level.general_profile_space, 0); + assert!(!vps.profile_tier_level.general_tier_flag); + assert_eq!(vps.profile_tier_level.general_profile_idc, 1); + for i in 0..32 { + let val = i == 1 || i == 2; + assert_eq!( + vps.profile_tier_level.general_profile_compatibility_flag[i], + val + ); + } + assert!(vps.profile_tier_level.general_progressive_source_flag); + assert!(!vps.profile_tier_level.general_interlaced_source_flag); + assert!(!vps.profile_tier_level.general_non_packed_constraint_flag,); + assert!(vps.profile_tier_level.general_frame_only_constraint_flag,); + assert!(!vps.profile_tier_level.general_max_12bit_constraint_flag,); + assert!(!vps.profile_tier_level.general_max_10bit_constraint_flag,); + assert!(!vps.profile_tier_level.general_max_8bit_constraint_flag,); + assert!(!vps.profile_tier_level.general_max_422chroma_constraint_flag,); + assert!(!vps.profile_tier_level.general_max_420chroma_constraint_flag,); + assert!( + !vps.profile_tier_level + .general_max_monochrome_constraint_flag, + ); + assert!(!vps.profile_tier_level.general_intra_constraint_flag); + assert!( + !vps.profile_tier_level + .general_one_picture_only_constraint_flag, + ); + assert!( + !vps.profile_tier_level + .general_lower_bit_rate_constraint_flag, + ); + assert!(!vps.profile_tier_level.general_max_14bit_constraint_flag,); + assert_eq!(vps.profile_tier_level.general_level_idc, Level::L2); + + assert!(vps.sub_layer_ordering_info_present_flag); + assert_eq!(vps.max_dec_pic_buffering_minus1[0], 4); + assert_eq!(vps.max_num_reorder_pics[0], 2); + assert_eq!(vps.max_latency_increase_plus1[0], 5); + for i in 1..7 { + assert_eq!(vps.max_dec_pic_buffering_minus1[i], 0); + assert_eq!(vps.max_num_reorder_pics[i], 0); + assert_eq!(vps.max_latency_increase_plus1[i], 0); + } + + assert_eq!(vps.max_layer_id, 0); + assert_eq!(vps.num_layer_sets_minus1, 0); + assert!(!vps.timing_info_present_flag); + assert_eq!(vps.num_units_in_tick, 0); + assert_eq!(vps.time_scale, 0); + assert!(!vps.poc_proportional_to_timing_flag); + assert_eq!(vps.num_ticks_poc_diff_one_minus1, 0); + assert_eq!(vps.num_hrd_parameters, 0); + } + + /// A custom test for SPS parsing with data manually extracted from + /// GStreamer using GDB. + #[test] + fn test25fps_sps_header_parsing() { + let mut parser = Parser::default(); + + let sps_nalu = find_nalu_by_type(STREAM_TEST25FPS, NaluType::SpsNut, 0).unwrap(); + let sps = parser.parse_sps(&sps_nalu).unwrap(); + + assert_eq!(sps.max_sub_layers_minus1, 0); + + assert_eq!(sps.profile_tier_level.general_profile_space, 0); + assert!(!sps.profile_tier_level.general_tier_flag); + assert_eq!(sps.profile_tier_level.general_profile_idc, 1); + for i in 0..32 { + let val = i == 1 || i == 2; + assert_eq!( + sps.profile_tier_level.general_profile_compatibility_flag[i], + val + ); + } + assert!(sps.profile_tier_level.general_progressive_source_flag); + assert!(!sps.profile_tier_level.general_interlaced_source_flag); + assert!(!sps.profile_tier_level.general_non_packed_constraint_flag,); + assert!(sps.profile_tier_level.general_frame_only_constraint_flag,); + assert!(!sps.profile_tier_level.general_max_12bit_constraint_flag,); + assert!(!sps.profile_tier_level.general_max_10bit_constraint_flag,); + assert!(!sps.profile_tier_level.general_max_8bit_constraint_flag,); + assert!(!sps.profile_tier_level.general_max_422chroma_constraint_flag,); + assert!(!sps.profile_tier_level.general_max_420chroma_constraint_flag,); + assert!( + !sps.profile_tier_level + .general_max_monochrome_constraint_flag, + ); + assert!(!sps.profile_tier_level.general_intra_constraint_flag); + assert!( + !sps.profile_tier_level + .general_one_picture_only_constraint_flag, + ); + assert!( + !sps.profile_tier_level + .general_lower_bit_rate_constraint_flag, + ); + assert!(!sps.profile_tier_level.general_max_14bit_constraint_flag,); + assert_eq!(sps.profile_tier_level.general_level_idc, Level::L2); + + assert_eq!(sps.seq_parameter_set_id, 0); + assert_eq!(sps.chroma_format_idc, 1); + assert!(!sps.separate_colour_plane_flag); + assert_eq!(sps.pic_width_in_luma_samples, 320); + assert_eq!(sps.pic_height_in_luma_samples, 240); + assert_eq!(sps.conf_win_left_offset, 0); + assert_eq!(sps.conf_win_right_offset, 0); + assert_eq!(sps.conf_win_top_offset, 0); + assert_eq!(sps.conf_win_bottom_offset, 0); + assert_eq!(sps.bit_depth_luma_minus8, 0); + assert_eq!(sps.bit_depth_chroma_minus8, 0); + assert_eq!(sps.log2_max_pic_order_cnt_lsb_minus4, 4); + assert!(sps.sub_layer_ordering_info_present_flag); + assert_eq!(sps.max_dec_pic_buffering_minus1[0], 4); + assert_eq!(sps.max_num_reorder_pics[0], 2); + assert_eq!(sps.max_latency_increase_plus1[0], 5); + for i in 1..7 { + assert_eq!(sps.max_dec_pic_buffering_minus1[i], 0); + assert_eq!(sps.max_num_reorder_pics[i], 0); + assert_eq!(sps.max_latency_increase_plus1[i], 0); + } + assert_eq!(sps.log2_min_luma_coding_block_size_minus3, 0); + assert_eq!(sps.log2_diff_max_min_luma_coding_block_size, 3); + assert_eq!(sps.log2_min_luma_transform_block_size_minus2, 0); + assert_eq!(sps.log2_diff_max_min_luma_transform_block_size, 3); + assert_eq!(sps.max_transform_hierarchy_depth_inter, 0); + assert_eq!(sps.max_transform_hierarchy_depth_intra, 0); + assert!(!sps.scaling_list_enabled_flag); + assert!(!sps.scaling_list_data_present_flag); + assert!(!sps.amp_enabled_flag); + assert!(sps.sample_adaptive_offset_enabled_flag); + assert!(!sps.pcm_enabled_flag); + assert_eq!(sps.pcm_sample_bit_depth_luma_minus1, 0); + assert_eq!(sps.pcm_sample_bit_depth_chroma_minus1, 0); + assert_eq!(sps.log2_min_pcm_luma_coding_block_size_minus3, 0); + assert_eq!(sps.log2_diff_max_min_pcm_luma_coding_block_size, 0); + assert!(!sps.pcm_loop_filter_disabled_flag); + assert_eq!(sps.num_short_term_ref_pic_sets, 0); + assert_eq!(sps.num_long_term_ref_pics_sps, 0); + assert!(sps.temporal_mvp_enabled_flag); + assert!(sps.strong_intra_smoothing_enabled_flag); + assert_eq!(sps.vui_parameters.sar_width, 0); + assert_eq!(sps.vui_parameters.sar_height, 0); + assert!(!sps.vui_parameters.video_full_range_flag); + assert!(!sps.vui_parameters.colour_description_present_flag); + assert!(sps.vui_parameters.video_signal_type_present_flag); + assert!(sps.vui_parameters.timing_info_present_flag); + assert_eq!(sps.vui_parameters.num_units_in_tick, 1); + assert_eq!(sps.vui_parameters.time_scale, 25); + assert!(!sps.vui_parameters.poc_proportional_to_timing_flag); + assert_eq!(sps.vui_parameters.num_ticks_poc_diff_one_minus1, 0); + assert!(!sps.vui_parameters.hrd_parameters_present_flag); + assert_eq!(sps.vui_parameters.colour_primaries, 2); + assert_eq!(sps.vui_parameters.transfer_characteristics, 2); + assert_eq!(sps.vui_parameters.matrix_coeffs, 2); + assert_eq!(sps.vui_parameters.def_disp_win_left_offset, 0); + assert_eq!(sps.vui_parameters.def_disp_win_right_offset, 0); + assert_eq!(sps.vui_parameters.def_disp_win_top_offset, 0); + assert_eq!(sps.vui_parameters.def_disp_win_bottom_offset, 0); + } + + /// A custom test for PPS parsing with data manually extracted from + /// GStreamer using GDB. + #[test] + fn test25fps_pps_header_parsing() { + let mut parser = Parser::default(); + + let sps_nalu = find_nalu_by_type(STREAM_TEST25FPS, NaluType::SpsNut, 0).unwrap(); + parser.parse_sps(&sps_nalu).unwrap(); + + let pps_nalu = find_nalu_by_type(STREAM_TEST25FPS, NaluType::PpsNut, 0).unwrap(); + let pps = parser.parse_pps(&pps_nalu).unwrap(); + + assert!(!pps.dependent_slice_segments_enabled_flag); + assert!(!pps.output_flag_present_flag); + assert_eq!(pps.num_extra_slice_header_bits, 0); + assert!(pps.sign_data_hiding_enabled_flag); + assert!(!pps.cabac_init_present_flag); + assert_eq!(pps.num_ref_idx_l0_default_active_minus1, 0); + assert_eq!(pps.num_ref_idx_l1_default_active_minus1, 0); + assert_eq!(pps.init_qp_minus26, 0); + assert!(!pps.constrained_intra_pred_flag); + assert!(!pps.transform_skip_enabled_flag); + assert!(pps.cu_qp_delta_enabled_flag); + assert_eq!(pps.diff_cu_qp_delta_depth, 1); + assert_eq!(pps.cb_qp_offset, 0); + assert_eq!(pps.cr_qp_offset, 0); + assert!(!pps.slice_chroma_qp_offsets_present_flag); + assert!(pps.weighted_pred_flag); + assert!(!pps.weighted_bipred_flag); + assert!(!pps.transquant_bypass_enabled_flag); + assert!(!pps.tiles_enabled_flag); + assert!(pps.entropy_coding_sync_enabled_flag); + assert_eq!(pps.num_tile_rows_minus1, 0); + assert_eq!(pps.num_tile_columns_minus1, 0); + assert!(pps.uniform_spacing_flag); + assert_eq!(pps.column_width_minus1, [0; 19]); + assert_eq!(pps.row_height_minus1, [0; 21]); + assert!(pps.loop_filter_across_slices_enabled_flag); + assert!(pps.loop_filter_across_tiles_enabled_flag); + assert!(!pps.deblocking_filter_control_present_flag); + assert!(!pps.deblocking_filter_override_enabled_flag); + assert!(!pps.deblocking_filter_disabled_flag); + assert_eq!(pps.beta_offset_div2, 0); + assert_eq!(pps.tc_offset_div2, 0); + assert!(!pps.lists_modification_present_flag); + assert_eq!(pps.log2_parallel_merge_level_minus2, 0); + assert!(!pps.slice_segment_header_extension_present_flag); + assert!(!pps.extension_present_flag); + } + + /// A custom test for slice header parsing with data manually extracted from + /// GStreamer using GDB. + #[test] + fn test25fps_slice_header_parsing() { + let mut parser = Parser::default(); + + // Have to parse the SPS/VPS/PPS to set up the parser's internal state. + let vps_nalu = find_nalu_by_type(STREAM_TEST25FPS, NaluType::VpsNut, 0).unwrap(); + parser.parse_vps(&vps_nalu).unwrap(); + + let sps_nalu = find_nalu_by_type(STREAM_TEST25FPS, NaluType::SpsNut, 0).unwrap(); + parser.parse_sps(&sps_nalu).unwrap(); + + let pps_nalu = find_nalu_by_type(STREAM_TEST25FPS, NaluType::PpsNut, 0).unwrap(); + parser.parse_pps(&pps_nalu).unwrap(); + + let slice_nalu = find_nalu_by_type(STREAM_TEST25FPS, NaluType::IdrNLp, 0).unwrap(); + let slice = parser.parse_slice_header(slice_nalu).unwrap(); + let hdr = &slice.header; + + assert!(hdr.first_slice_segment_in_pic_flag); + assert!(!hdr.no_output_of_prior_pics_flag); + assert!(!hdr.dependent_slice_segment_flag); + assert_eq!(hdr.type_, SliceType::I); + assert!(hdr.pic_output_flag); + assert_eq!(hdr.colour_plane_id, 0); + assert_eq!(hdr.pic_order_cnt_lsb, 0); + assert!(!hdr.short_term_ref_pic_set_sps_flag); + assert_eq!(hdr.lt_idx_sps, [0; 16]); + assert_eq!(hdr.poc_lsb_lt, [0; 16]); + assert_eq!(hdr.used_by_curr_pic_lt, [false; 16]); + assert_eq!(hdr.delta_poc_msb_cycle_lt, [0; 16]); + assert_eq!(hdr.delta_poc_msb_present_flag, [false; 16]); + assert!(!hdr.temporal_mvp_enabled_flag); + assert!(hdr.sao_luma_flag); + assert!(hdr.sao_chroma_flag); + assert!(!hdr.num_ref_idx_active_override_flag); + assert_eq!(hdr.num_ref_idx_l0_active_minus1, 0); + assert_eq!(hdr.num_ref_idx_l1_active_minus1, 0); + assert!(!hdr.cabac_init_flag); + assert!(hdr.collocated_from_l0_flag); + assert_eq!(hdr.five_minus_max_num_merge_cand, 0); + assert!(!hdr.use_integer_mv_flag); + assert_eq!(hdr.qp_delta, 7); + assert_eq!(hdr.cb_qp_offset, 0); + assert_eq!(hdr.cr_qp_offset, 0); + assert!(!hdr.cu_chroma_qp_offset_enabled_flag); + assert!(!hdr.deblocking_filter_override_flag); + assert!(!hdr.deblocking_filter_override_flag); + assert_eq!(hdr.beta_offset_div2, 0); + assert_eq!(hdr.tc_offset_div2, 0); + assert!(hdr.loop_filter_across_slices_enabled_flag); + assert_eq!(hdr.num_entry_point_offsets, 3); + assert_eq!(hdr.offset_len_minus1, 11); + assert_eq!(hdr.num_pic_total_curr, 0); + + // Remove the 2 bytes from the NALU header. + assert_eq!(hdr.header_bit_size - 16, 72); + + assert_eq!(hdr.n_emulation_prevention_bytes, 0); + + assert_eq!(slice.nalu.as_ref(), STREAM_TEST_25_FPS_SLICE_0); + + // Next slice + let slice_nalu = find_nalu_by_type(STREAM_TEST25FPS, NaluType::TrailR, 0).unwrap(); + let slice = parser.parse_slice_header(slice_nalu).unwrap(); + let hdr = &slice.header; + + assert!(hdr.first_slice_segment_in_pic_flag); + assert!(!hdr.no_output_of_prior_pics_flag); + assert!(!hdr.dependent_slice_segment_flag); + assert_eq!(hdr.type_, SliceType::P); + assert!(hdr.pic_output_flag); + assert_eq!(hdr.colour_plane_id, 0); + assert_eq!(hdr.pic_order_cnt_lsb, 3); + assert!(!hdr.short_term_ref_pic_set_sps_flag); + assert_eq!(hdr.short_term_ref_pic_set.num_delta_pocs, 1); + assert_eq!(hdr.short_term_ref_pic_set.num_negative_pics, 1); + assert_eq!(hdr.short_term_ref_pic_set.num_positive_pics, 0); + assert!(hdr.short_term_ref_pic_set.used_by_curr_pic_s0[0]); + assert_eq!(hdr.short_term_ref_pic_set.delta_poc_s0[0], -3); + assert_eq!(hdr.lt_idx_sps, [0; 16]); + assert_eq!(hdr.poc_lsb_lt, [0; 16]); + assert_eq!(hdr.used_by_curr_pic_lt, [false; 16]); + assert_eq!(hdr.delta_poc_msb_cycle_lt, [0; 16]); + assert_eq!(hdr.delta_poc_msb_present_flag, [false; 16]); + assert!(hdr.temporal_mvp_enabled_flag); + assert!(hdr.sao_luma_flag); + assert!(hdr.sao_chroma_flag); + assert!(!hdr.num_ref_idx_active_override_flag); + assert_eq!(hdr.num_ref_idx_l0_active_minus1, 0); + assert_eq!(hdr.num_ref_idx_l1_active_minus1, 0); + assert!(!hdr.cabac_init_flag); + assert!(hdr.collocated_from_l0_flag); + assert_eq!(hdr.pred_weight_table.luma_log2_weight_denom, 7); + assert_eq!(hdr.five_minus_max_num_merge_cand, 2); + assert!(!hdr.use_integer_mv_flag); + assert_eq!(hdr.num_entry_point_offsets, 3); + assert_eq!(hdr.qp_delta, 7); + assert_eq!(hdr.cb_qp_offset, 0); + assert_eq!(hdr.cr_qp_offset, 0); + assert!(!hdr.cu_chroma_qp_offset_enabled_flag); + assert!(!hdr.deblocking_filter_override_flag); + assert!(!hdr.deblocking_filter_override_flag); + assert_eq!(hdr.beta_offset_div2, 0); + assert_eq!(hdr.tc_offset_div2, 0); + assert!(!hdr.loop_filter_across_slices_enabled_flag); + assert_eq!(hdr.num_entry_point_offsets, 3); + assert_eq!(hdr.offset_len_minus1, 10); + assert_eq!(hdr.num_pic_total_curr, 1); + + assert_eq!(slice.nalu.size, 2983); + // Subtract 2 bytes to account for the header size. + assert_eq!(hdr.header_bit_size - 16, 96); + assert_eq!(slice.nalu.as_ref(), STREAM_TEST_25_FPS_SLICE_1); + + // Next slice + let slice_nalu = find_nalu_by_type(STREAM_TEST25FPS, NaluType::TrailR, 1).unwrap(); + let slice = parser.parse_slice_header(slice_nalu).unwrap(); + let hdr = &slice.header; + + assert_eq!(slice.nalu.size, 290); + // Subtract 2 bytes to account for the header size. + assert_eq!(hdr.header_bit_size - 16, 80); + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/picture.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/picture.rs new file mode 100644 index 00000000..9b9d01dd --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/picture.rs @@ -0,0 +1,158 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use crate::codec::h265::parser::NaluType; +use crate::codec::h265::parser::Slice; + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum Reference { + #[default] + None, + ShortTerm, + LongTerm, +} + +/// Data associated with an h.265 picture. Most fields are extracted from the +/// slice header and kept for future processing. +#[derive(Debug, Default, Clone, Eq, PartialEq)] +pub struct PictureData { + // Fields extracted from the slice header. These are the CamelCase + // variables, unless noted otherwise. + pub nalu_type: NaluType, + pub no_rasl_output_flag: bool, + pub pic_output_flag: bool, + pub valid_for_prev_tid0_pic: bool, + pub slice_pic_order_cnt_lsb: i32, + pub pic_order_cnt_msb: i32, + pub pic_order_cnt_val: i32, + pub no_output_of_prior_pics_flag: bool, + + // Internal state. + pub first_picture_after_eos: bool, + reference: Reference, + pub pic_latency_cnt: i32, + pub needed_for_output: bool, + pub short_term_ref_pic_set_size_bits: u32, +} + +impl PictureData { + /// Instantiates a new `PictureData` from a slice. + /// + /// See 8.1.3 Decoding process for a coded picture with nuh_layer_id equal + /// to 0. + /// + /// This will also call the picture order count process (clause 8.3.1) to + /// correctly initialize the POC values. + pub fn new_from_slice( + slice: &Slice, + first_picture_in_bitstream: bool, + first_picture_after_eos: bool, + prev_tid0_pic: Option<&PictureData>, + max_pic_order_cnt_lsb: i32, + ) -> Self { + let hdr = &slice.header; + let nalu_type = slice.nalu.header.type_; + + // We assume HandleCraAsBlafFLag == 0, as it is only set through + // external means, which we do not provide. + + let mut pic_order_cnt_msb = 0; + let slice_pic_order_cnt_lsb: i32 = hdr.pic_order_cnt_lsb.into(); + + // Compute the output flags: + // The value of NoRaslOutputFlag is equal to 1 for each IDR access + // unit, each BLA access unit, and each CRA access unit that is the + // first access unit in the bitstream in decoding order, is the first + // access unit that follows an end of sequence NAL unit in decoding + // order, or has HandleCraAsBlaFlag equal to 1. + let no_rasl_output_flag = nalu_type.is_idr() + || nalu_type.is_bla() + || (nalu_type.is_cra() && first_picture_in_bitstream) + || first_picture_after_eos; + + let pic_output_flag = if slice.nalu.header.type_.is_rasl() && no_rasl_output_flag { + false + } else { + hdr.pic_output_flag + }; + + // Compute the Picture Order Count. See 8.3.1 Decoding Process for + // Picture Order Count + if !(nalu_type.is_irap() && no_rasl_output_flag) { + if let Some(prev_tid0_pic) = prev_tid0_pic { + // Equation (8-1) + let prev_pic_order_cnt_lsb = prev_tid0_pic.slice_pic_order_cnt_lsb; + let prev_pic_order_cnt_msb = prev_tid0_pic.pic_order_cnt_msb; + if (slice_pic_order_cnt_lsb < prev_pic_order_cnt_lsb) + && (prev_pic_order_cnt_lsb - slice_pic_order_cnt_lsb) + >= (max_pic_order_cnt_lsb / 2) + { + pic_order_cnt_msb = prev_pic_order_cnt_msb + max_pic_order_cnt_lsb; + } else if (slice_pic_order_cnt_lsb > prev_pic_order_cnt_lsb) + && (slice_pic_order_cnt_lsb - prev_pic_order_cnt_lsb) + > (max_pic_order_cnt_lsb / 2) + { + pic_order_cnt_msb = prev_pic_order_cnt_msb - max_pic_order_cnt_lsb; + } else { + pic_order_cnt_msb = prev_pic_order_cnt_msb; + } + } + } + + // Compute whether this picture will be a valid prevTid0Pic, i.e.: + // + // Let prevTid0Pic be the previous picture in decoding order that has + // TemporalId equal to 0 and that is not a RASL, RADL or SLNR picture. + // + // Use this flag to correctly set up the field in the decoder during + // `finish_picture`. + let valid_for_prev_tid0_pic = slice.nalu.header.nuh_temporal_id() == 0 + && !nalu_type.is_radl() + && !nalu_type.is_rasl() + && !nalu_type.is_slnr(); + + let no_output_of_prior_pics_flag = + if nalu_type.is_irap() && no_rasl_output_flag && !first_picture_in_bitstream { + nalu_type.is_cra() || hdr.no_output_of_prior_pics_flag + } else { + false + }; + + Self { + nalu_type, + no_rasl_output_flag, + no_output_of_prior_pics_flag, + pic_output_flag, + valid_for_prev_tid0_pic, + slice_pic_order_cnt_lsb, + pic_order_cnt_msb, + // Equation (8-2) + pic_order_cnt_val: pic_order_cnt_msb + slice_pic_order_cnt_lsb, + first_picture_after_eos, + reference: Default::default(), + pic_latency_cnt: 0, + needed_for_output: false, + short_term_ref_pic_set_size_bits: hdr.st_rps_bits, + } + } + + /// Whether the current picture is a reference, either ShortTerm or LongTerm. + pub fn is_ref(&self) -> bool { + !matches!(self.reference, Reference::None) + } + + pub fn set_reference(&mut self, reference: Reference) { + log::debug!( + "Set reference of POC {} to {:?}", + self.pic_order_cnt_val, + reference + ); + + self.reference = reference; + } + + pub fn reference(&self) -> &Reference { + &self.reference + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P-B-P.h265 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P-B-P.h265 new file mode 100644 index 00000000..f3ca72ba Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P-B-P.h265 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P-B-P.h265.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P-B-P.h265.crc new file mode 100644 index 00000000..f8e992b3 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P-B-P.h265.crc @@ -0,0 +1,3 @@ +2904d4d2 +d8e11777 +a1108fed diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P-B-P.h265.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P-B-P.h265.md5 new file mode 100644 index 00000000..3cab8425 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P-B-P.h265.md5 @@ -0,0 +1,3 @@ +c45648e1d3dd68913e998bd6ddb0f633 +8a44f604b2518c6caa7bb422907c8d17 +fe6ac1f24e248f3dba54087245380dd8 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P.h265 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P.h265 new file mode 100644 index 00000000..50935e39 Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P.h265 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P.h265.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P.h265.crc new file mode 100644 index 00000000..47096b06 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P.h265.crc @@ -0,0 +1,2 @@ +2407c115 +396fe8d4 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P.h265.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P.h265.md5 new file mode 100644 index 00000000..5011175a --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P.h265.md5 @@ -0,0 +1,2 @@ +afcabbf0be007e76b1f496e199eb07fa +cebc9437bfc8501523c432d3fee3621d diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I.h265 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I.h265 new file mode 100644 index 00000000..d7be2a40 Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I.h265 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I.h265.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I.h265.crc new file mode 100644 index 00000000..319743d2 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I.h265.crc @@ -0,0 +1 @@ +a5a83a48 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I.h265.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I.h265.md5 new file mode 100644 index 00000000..e1c0e019 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I.h265.md5 @@ -0,0 +1 @@ +15caee73c93560200b9e240c6f5d7d0f diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/README.md b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/README.md new file mode 100644 index 00000000..c9cfeaaf --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/README.md @@ -0,0 +1,17 @@ +# H.265 Test Data + +This document lists the test data used by the H.265 parser. + +## bear.hevc + +Same as Chromium's `bbb.hevc`. + +## bear.hevc + +Same as Chromium's `bear.hevc`. + +## test-25fps.hevc + +Same as Chromium's `test-25fps.hevc`. + +The slice data for the first two slices in this stream was extracted manually from GStreamer using GDB. diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bbb.h265 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bbb.h265 new file mode 100644 index 00000000..f82236f3 Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bbb.h265 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bbb.h265.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bbb.h265.crc new file mode 100644 index 00000000..dc790819 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bbb.h265.crc @@ -0,0 +1,60 @@ +97255112 +c317718d +bd113d45 +186209fe +75726ee3 +e4a90dbd +1da8aec0 +f537bcee +bdcead02 +edd12fd6 +9814391e +ea813192 +8e3f606c +7298a718 +c25ed8f9 +54585aa5 +c5d6811c +83ffc178 +ad9b6189 +20e3b289 +ea852c44 +38af63d3 +222e116b +cbf5b144 +5cc11d3f +561aba19 +c1b0021e +97465654 +3f69533a +95724dac +4fd1865b +00fc6666 +262e9ecc +753e21c5 +73820a63 +6023ad07 +6baf0154 +6b658956 +c7f3d4d0 +b7fe7729 +652e5667 +76b64fad +98f934ef +64a737be +e561fe00 +2bd0799f +53d86edb +07cad162 +c4857692 +9ab13929 +c445eb2a +f0e890de +1db971fc +d03cc3fa +69d2cdba +172562d2 +356c55d0 +515bfbd6 +5d8dfbc2 +ec0874fb diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bbb.h265.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bbb.h265.md5 new file mode 100644 index 00000000..c572a900 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bbb.h265.md5 @@ -0,0 +1,60 @@ +e4519fce4b5a50a8ae38d2b42864e550 +c7f6707ea7b2aaa3a9c027e00bf2064b +21c016eb7a57804d8637b03bc7910b31 +f744d8b67d4836629beac1ca4efdad9b +8e593571ad75b9b19c3b14b44033bc03 +3f76c8397f7829d948ddb50c7050b4bb +d91e343dbdb64dfd15f09abc4c460b06 +62d5c1de7a7ff8b4cdfb51cb33746bcd +01a4c4d5f770d3cdc5429264f6a1a742 +760de93c5a25439a89378fbe50c26f3d +d56f52f9252745321c723121824e5d21 +be626892c70d6fd412d7bff504852529 +ae49266f3b90dd1238f7ad83ae86886e +7a9fe15547cbcb51557bfd0b8370e0ee +a5b81f4e52fe54f67d109caa200486ec +34395343b832af13dbe37ebe745506e0 +36d6e24c8a000b4f8d5c1aabba85c185 +9be3b6e5b0b77c136b5abe8a22bded59 +5122057efd33ed5634b755a7b0581382 +abaf35e215001a6abe0dbc63f5b4ace1 +62ba3a52072f80993c24cf9a3abda06f +21b225675443d0142e104ec88f7e9569 +b561e6555b65ba6cebf0d73eefe4f319 +ab2b1a568bed6f9bafe79cf15dfd85b5 +e8c3645830f80e082e8b02e3cc599cc2 +82d655fb233fea3fc3377b4d3dfc11ff +4a7c2a51149c1939cffc8cfc87fdb953 +32295652a5ff998c90fa18fbfc066fdc +ce8974e2519a2babc43fb29106459cd2 +589fa92d80565556776c2108f676c439 +2bc1878534bf66caa6d686d0dad5a936 +b3d81309bb62ed349bed2c0fb17a5593 +c941ae94ab205aa1c27b9b06df4b57de +e782fa5f5a7302b8246c29880e9c2f9d +eeabb3a6c5fa1ea0f17a9fb911f5b612 +26f27eafea985de929a48287162fc641 +da4f4da7a16a4326c0c458f3dba12ef6 +7697150fa654713af6a6ecc07ad7d3dc +67696b8c27169cbdbc18d093f794b328 +96ba8f5d17e7b35e78f5ac78a816a364 +1ab2c0d535d26c72d730e101600f42af +f8b9d8fc231ccd64c551c829ef25089a +eb004174e79dc8dbb533922639a725e2 +fd519318eb8c47e5fac6fc767246bfed +471dbd17c2a374ab7dcaf64c345c1884 +fc28a8f484573893f6f501a4ade8ac37 +1e2dd38a14d5c13ef76e89d142f73951 +0fd6a338f4ecb3df053d105bc95e6ee1 +a42fd1e8bb069ab5c501fbfd655fc1b0 +f252d07215ddd6687fb3019f2710c939 +c8df7b9cbc16a26d782494b8cd312be4 +966a2f3fcb066179feef80e68f13e212 +b99b1100b6950144e70fa47310451340 +9b3502c7dc1a4ca2579a29f6b17af84d +9a81be9720c64ce3b343fd20bc17bb14 +e5a67048a9c1ae74b8a97c5a27d6d2b2 +1527a719633c75b67ae6c36ebe550812 +86b1329ec84332fec1a75330e7d7a174 +6fa5b5faf93d70e96ffa215e4628ad9a +ed6279f7b90727abc8a193745170a318 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bear.h265 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bear.h265 new file mode 100644 index 00000000..f2a0545c Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bear.h265 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bear.h265.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bear.h265.crc new file mode 100644 index 00000000..a3c5b67f --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bear.h265.crc @@ -0,0 +1,30 @@ +0a6ac0dd +350b4669 +61a8b9ad +f566d7c6 +8d8ab332 +52468dae +44bdbf5f +935e3db3 +814109bc +68dc2234 +d5b47d8b +852f3e1f +3f54c08b +9e33e4ae +047c4f0d +10cebb43 +87774650 +08adc4be +837797a6 +17f80256 +221e943f +e06ff206 +7fb43061 +e5dc2425 +37928778 +3a787cf0 +14ea1afd +0331b84f +bdfa1606 +28216ab7 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bear.h265.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bear.h265.md5 new file mode 100644 index 00000000..4163d712 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/bear.h265.md5 @@ -0,0 +1,30 @@ +037e7ff0ab754f16dc254edbecaf5bc0 +47d9c4d6427726b74dd8586aeea147e4 +1fae73f309640f704f8358fc0bae4c39 +579c2fe2b1fa83ab3082e30daebba728 +a4e01659d6bc12626d445a02fa5f977e +3bcadc5d9985e44338edca617fd84541 +ac92b5ee1f79b5bda8f662eefab40085 +03d24eba6675f51496b9338d97c39308 +77f9d5f1e2e5bd40164336d9fe7970ef +355f2e0f8e8c4702ad45c790415004df +1d9563a8ccdf00b2116edb508b665e7e +44eb70aeba2c5eba6b0ce931d6fff3e5 +c5fab1da55bd8177342d4e174bdc1474 +30f0d9a37231141722ba65c61bd59d09 +a1e8fdc8b33af447e4486c17d028b706 +11cd37f438399a595745dff608965aa2 +8025ff7ac2d9c9cda4039900834c1182 +e934c10259100457b44bd13a8630a3e0 +9f745c40aae4b6c31d57a0a43b39a4d2 +041ea32d187a011baafda69c1f079b2e +d8a172fed23b9f0b4213bc7f8f1375a9 +22e9e048f8e04410307068b6bc2e79d7 +3c9d59aa49756060f8e78a5d608db8e6 +5482d7c76a6ff60c9c32fffc27c39873 +44a4d11b3ecdc827cc21ff732e1c7c7d +aeaaff7c5b23b4f03d939de6c12e5006 +afa6ece7a21db66eece97e2e0713f29c +d84b7794f4a4968ea7fadf5cba7333d0 +f9644eca3981a123b917938c7a4963fb +e02c049b9e516e10c05ce4469856e37d diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/gen_crcs.sh b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/gen_crcs.sh new file mode 100755 index 00000000..4c7db423 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/gen_crcs.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Generates the CRCs for all .h265 files in the current directory using ffmpeg. + +for f in `ls *.h265`; do + ffmpeg -i $f -pix_fmt nv12 -f framehash -hash crc32 - |grep -v '^#' |awk '{print $6}' >$f.crc + ffmpeg -i $f -pix_fmt nv12 -f framehash -hash md5 - |grep -v '^#' |awk '{print $6}' >$f.md5 +done diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps-h265-slice-data-0.bin b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps-h265-slice-data-0.bin new file mode 100644 index 00000000..b06651aa Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps-h265-slice-data-0.bin differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps-h265-slice-data-1.bin b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps-h265-slice-data-1.bin new file mode 100644 index 00000000..03c6a152 Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps-h265-slice-data-1.bin differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265 new file mode 100644 index 00000000..34bf55a3 Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265.crc new file mode 100644 index 00000000..dbad0a6a --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265.crc @@ -0,0 +1,250 @@ +97f807e6 +ee7f1354 +6fa0cec6 +9285fe16 +b291ad06 +155fee7a +01427b08 +37610d09 +404fa256 +2935ba29 +fe05c389 +f4330069 +549eff98 +5150e3fb +10a15359 +0673d70d +6041a014 +104cba40 +47a31b16 +b5bc8a04 +3a16605a +da5384bc +156479bd +dc555188 +458bffed +2cc6fc99 +15abaa1b +bdddda11 +fffa939a +54a83ce1 +82ba8102 +0ac14f5a +49e4c2c8 +4ba7c6e5 +44f0bcb2 +c370a456 +6361c839 +5c0fdfc5 +dcd0b111 +adad6054 +67b38f70 +8decebb8 +d64dacb4 +4025f908 +d7154e44 +93fbddde +be5cf4ed +d4492e49 +44d25551 +fcfbdbcf +8698aa50 +00025a9a +45f6d961 +507fa2e0 +bda2b113 +d735c28e +13d54b15 +2b0e2c70 +fd1938c9 +e7b016c9 +511bbb25 +52754511 +f4c71342 +9572d1c1 +279aa775 +bee7e9aa +0e916552 +6bd17554 +0616896f +f2fbed3a +f2c2f64e +b34fea4e +fb9f27d0 +7bf93797 +58fe6b72 +d6dbb367 +a4351497 +6001755c +54e3ef07 +04bdff16 +f50bb46e +619d9976 +c7438682 +71186e2d +9f90810b +26fa0f46 +e276d45f +de8947eb +98c35c3e +5cd09c8d +6ffcf799 +d0af871b +b6f20a85 +b6064e3d +f8af1a1d +6d749e92 +484768ab +779f6743 +f98c027a +1579feb1 +8efcd176 +b7ab75fd +e7911455 +1878ec1f +db6c2d82 +8ad48e21 +9757e7ab +fd22cb3b +e441e63e +12bcd5e3 +f76a42e8 +261856b7 +ab00bc6f +03c25d9f +65ef13d7 +b66512c9 +415b3d05 +0dff93cb +cb541aaa +0ee6c61b +d6276a2c +900d44ad +0c408967 +2d539b08 +5479da37 +298f03dd +bac454b8 +81b81c33 +a99b8c77 +2cc2e8af +4053184a +24be8904 +5c46ee1c +30f2825f +311a5956 +8379cadc +16b91eea +9e765763 +c1115845 +8bf28db3 +40b89f88 +c785befb +b863d8a7 +d65c8552 +1571d9c1 +38c47640 +a04a8084 +d0881cc9 +e76534e9 +1b7b3ec0 +fa89a1e6 +91a77797 +50eabb2b +1ffea9fd +36da14d7 +4d21ead9 +0e9f3a19 +5c3bc82b +e408152c +aff45223 +4016bb65 +9055be73 +966f4f40 +1b232cde +19d409a7 +9cf5ada9 +b983a480 +4329c339 +1b8c3818 +0a7e1997 +cebc35c9 +a5483e3c +aa1ff50f +ad76567f +c1b9bd1a +8340988a +ecd60321 +8c12fdcf +e3744c63 +99c0fa4c +e1bdfb1f +a047f435 +7e3b8ebb +106e6518 +fb09be12 +d535a54d +23e91694 +1e68c0cb +53844c1a +c8180fa6 +a58b3a20 +bf8a3c82 +fbd86177 +27041f33 +88fe20d5 +c0dbae26 +1677f210 +1d40f687 +e37f402a +35d23b41 +8d0bd70e +1439b309 +bbe21347 +e03ae4c5 +13b745d7 +e5116c4e +70569ef4 +c85f4731 +b5b6de1f +9dc8c6ba +d7e7ec20 +fd5fdeb9 +1de6cf05 +968fe4ea +c658ebd9 +a3427991 +1d5aa7ef +6ae06a27 +41abbdb5 +a9104c0c +db2726a1 +d7f4ce5c +9ce1a83d +70252457 +6744fda6 +a5b475f8 +5718ff91 +d40f157d +50e7af4c +a49ec566 +1eda24b9 +b77eda17 +aac39c22 +2352e08d +a701b790 +cd916c2e +5ef244e6 +b9ee5202 +4a8b8eec +60a8976e +f98cbcb1 +d5a8094f +7b7c1585 +c679bf1d +e6bb3547 +4f05e0eb +d63111c5 +b8b260c0 +a18d9b72 +a7c1ca74 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265.json b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265.json new file mode 100644 index 00000000..145262a0 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265.json @@ -0,0 +1,261 @@ +{ + "profile": "HEVCPROFILE_MAIN", + "width": 320, + "height": 240, + "frame_rate": 25, + "num_frames": 250, + "num_fragments": 254, + "md5_checksums": [ + "9d45302f59628259021c0d580f38e577", + "cfafb8cdd693af7b24292d98e28d6966", + "a9e93b65a59b752063f259c40e7d9fe7", + "8b2445ed2bde8128fe090ebf112299b3", + "af2fb4344c89078811eea7979ea87be8", + "031f754980442cb3d8c59e83f637f16b", + "3d579a05e18b85c5bf8e1079ebdb76d5", + "9414c26b10b707ce582bea5e067be2b4", + "18599153c70945dfde292dae24b81521", + "7a324a7822e3a94df8b5a767f42ae5b9", + "83ff81efb040a2d247abd639a66b76b7", + "0e5b73811f510ac2ed2b976b27d4a3d9", + "787e4f184a285d5c3071c14085ca528d", + "8f9d7714962d32e211ca88e3f1c7f46d", + "a40888217689af118a13d33ba1acee02", + "0dc38b3c25ef2e35a1a5d000f24066bd", + "f8bfaca952ef6158cac2b4139079ff3c", + "04dd4c10c44fe65111eec52cef42b79e", + "52e07c85f11add8e08473228c97d9e96", + "5d2d01028f9549b67452343e1a37159a", + "3e86b75a1c9cd4fc82371091dc9270ef", + "091c1d95044708b1699a704f7fe00e97", + "566eabd3b2112b1e60687c2646a03d7d", + "7febb218fcfcd622e78cadc575e00d74", + "c661462a644194e0e3f48cb76180f991", + "8a98f0f76e624e718ab7ee1f8b3d06d7", + "a74cee3ff950de4dfa7c049e72372d8a", + "2003bb8048e0778cd7b684d3d290e238", + "9583a569bef2a181e4b14ef9d7515965", + "28fc9c431f8895224ee76a7a6a069644", + "1f86380ff7e1ce3a549ec193f5442915", + "8ce13d9e3659b928c7b768c470863673", + "3e842fddf70acc3989b99a784d2d3a64", + "2161a6b08f354b9213673af0f10595a4", + "706ae034f4622cb729016b5efbf71761", + "9ab6c4ec2708fc446bd6e57c0943f9ce", + "77905702a6b7b1ad3a364914115ae1c1", + "f35817cdbb7fd60cd39363f8116474a5", + "ae41c4d453cf07939f652f79c6f2cc23", + "938bd9a612612c55a018f6a746eee14e", + "72b4d96412d20cdecce2861eacdf7e85", + "f392537536b52626f5bc338f8d5f6261", + "e7efe9c3e54ee42cfb1552c512844283", + "2560af598073488d3c49ef8c59d6010d", + "8a78183c164f50e206a7f376264103ab", + "3f4235a1536c838c0bdc273314ded61e", + "5887551fbb1436f5bc9fa6e63b9d9da0", + "77fb5a36e251287e8b93e8c12279f54f", + "36c1d5aef11aa43bff133907478abc07", + "7988ba44232599cc54acaa71bfcac8ee", + "a420156487134f7fd8b2bffc86c2c466", + "a27f4e7c47212d83dce57ba52e4bf728", + "651da9f2f35798255fa618affc9d5140", + "736f41a6fc11267e14d865a44a5024d5", + "5a0c9f520021a5e027287905443326c0", + "838c3ef02a0d419f79b0a5cb96100712", + "5d135fae4da8f33e842fe1d56b771baa", + "9f5b762f5d0a9e6b4373aca87c04f872", + "6a28827f8b5217636a003eba52bff2ab", + "117254f82ee37acee64c1695da6efa61", + "b9c023f5a72cbf120872794f94025c55", + "70ed7dfa85fa637e446a4aa941f2b9fe", + "7dcfc63d25fe08bc05c0c1b919148b57", + "830c4e8958e84338f7b0a2ae8ecac5b7", + "518e7456c889f30d1a4afdf2d0618f4b", + "8a5aa2e84a77756adf020b9b5570a47e", + "906878b088da07f0b5b61e2975aed09f", + "6f8a49de618fceee2c2284037ccb4181", + "f0f751f49e75b38373c2a5a4f2a8d9b8", + "a4ae82030d893596129f19c51158e595", + "35d0a14a2c4086c4ca4f010d2e9f0adb", + "9c39936e95c658ada88bb6e8885f4639", + "11bac53610bf107cfa9a80cc7d40d7ef", + "3b1377f3a2bbc03be2a04ccce3f26d02", + "520e6cb06673ece706cc96db877bd606", + "8ab7f21cf1de8ce902f15ce812bdbdbe", + "f140e1564aac1f71728922702083f9e9", + "51828833d2bcb377fa0be61b7cb82579", + "426f15aa7c01101d16237eb33995e4bf", + "8f00d6311bb2ce1d290433126e480114", + "1feaca010259e859bd3363b5c1d04f35", + "2bb0e4dd7b3a115bc0f445497e35dc08", + "fc640243683109158e0dff5dcea50842", + "867f0ecd82aae7740bd9b251b9179350", + "85111785a9ed8b2b8b70b09e12154428", + "40ab1b731a9bf8f8e441c70ccc19b1dd", + "3100c669e824c8aec2b2d67561db1349", + "d05552052cd675c02caf5e1c671be35b", + "47fc5cd75aca27be5d6dadde030fa3d5", + "da2cc8eafa37e9a057e9a569ee541637", + "d2b4a325071110060e847fd3e8ef1f52", + "1dcccb0cf3a74b3153cb9b7c09a7f377", + "8c91eb6aa2594899c0101d537505a121", + "13abb158d7c22362ef1b73b5f15bfecf", + "35ad01fb07169fc0050cdb996de53d9e", + "d25ce524d14d95fd99607d42e13b7937", + "c2ff502083b013873ab203d2af83337e", + "68567f7ce7b9b19e5f85872442c54f79", + "5ae94fee674131ecfd45a6775854ec96", + "1c9f092f1e02caca84086b9873e1af89", + "6510b165141b38e47d7cdc23e44cf39c", + "913b03bbb72f8ccba56440632377b342", + "3cf908dcf12bd787901571e601af20d7", + "25518a7a40234f05661aaefea13518b2", + "6d3512a84091b9a8934da296e8dfc5c4", + "8ca79a3ace49c1126687674fffaaa5a2", + "e644740d09c3e2f3598fce3839497dab", + "8342af1710d8fec8f10a64da2ca9ea01", + "7dc733dc534e2cca2422027db2a3e34a", + "50fd7033960cf1d08bb558f7f7b7743d", + "0cab05f4bc4f716df0c741a34a128f3f", + "ff12d8589ee2ba883462db3496625218", + "cfb4baf656a92ffad4f8e29e086a22ed", + "6276eaef79a25eeb77e15c26121b522e", + "5af976088fc10e9b263bf12d27af2131", + "27510f90e0599ab31df00771f712cfe7", + "5e120f23d16415907b3bca93c61a7798", + "f0822f6593be64c890a73b5e19fa0aba", + "6f00bf23a01f93f6cdea4c5ec9d429ba", + "acf06eafb385d75956ac11b4394d8a96", + "99f22f1087710da1e13fa51638b1cd1b", + "206d93afe06167faf8560300ae7d9e02", + "f0ac45f756a2ea398aa8c5cc6557048f", + "314169ee8e31d3d654e6b60b63ea6b94", + "b709914ad6a828552323fd7814aa3a69", + "16eedf463a872c4e4f9645e2e7f731c2", + "9ecaeec7815eda54aeb7ac937df6688e", + "d3f880557ed0e69595d2887a879c46bf", + "79afbc938ea2f2e0968b9cf31c7d653c", + "4e76d541aa8796cfe4409da6f2d2afbc", + "5a9b81dee85caeaeb1e5eccb8a5017cf", + "977e990539d1cf3088ffe511f7346953", + "2e0e69b8c37d91453b95cd3e4635ab4f", + "3768fcaea9f36b873b054d02e7386a44", + "600c3f478e859e5114fca6188e0832c3", + "7a02082f7f1f104c3f01be9707be6978", + "b5ba5c7504bdee1e10d74c3ebdd6d6b8", + "baecb5bc56ac7ff840164b35bdf555c4", + "33584ab96f9c3b6d7808e3e117b5b8a2", + "3cfbd17bcdec301728b833feb9f3c7fa", + "74daa5525ff24e90e6a1772a8e3055cf", + "a37c2933e7445a765c8f3f18f3384c93", + "e2b7199fcef9de93d4296cb3667bceae", + "adc2b4e150ece651e7bc1174a78f79c2", + "8568addcb675d4086158303460594703", + "81a2b4bf06a0bbcc851779890cd73756", + "c3f3f15b6ab9e47ce9242e32ff4951a6", + "0495b5f57f93e37a3cfe3eff9f52319e", + "36ffcdf358c7a42c427a6c827117a428", + "9bc6ade4706bcd242b6b9b4a50fab72c", + "584b7f4b14d531f8c8ac045a505da5d2", + "393180967f07275e834e21127cf40ff6", + "5dc3b033f2f26a4c0f113c60fe2d30bb", + "157e3c22f8f6a319bd77a7c7e9689d96", + "24395b2b073cb0d7448a7eec6ecb5edb", + "6fc1d9efc86af03f5939f3aa1a1f9dfd", + "ded02a863d25e29f9d1a78e8953cca16", + "275c63d4d60e0bcb63613bec7d3ebba3", + "042a6db4563ec75d5ff675a1e0e06647", + "872755f0743242d0b8fdc42ac5b81d4d", + "0aee140a64076aa43c8f9fb74d18796b", + "ba738328a5377253b7067843fe48cbe4", + "ba4fcdd5085f5bb3b33420c0eb97b5ab", + "c393ac063ba4ee7370595f1528b00e98", + "39ef16ba57122f6dd3bbec8aa83c596f", + "ce7baa5845683d115d17e2822238ea56", + "afd200eb4ed02c2ff93a8e04503490ac", + "df783cda0056926ae9687bcdff01d6cd", + "24129bf695474e4944cbf79c27dc49d5", + "bb62851ee3852094c3722b71c469764d", + "04a64b8e4f5a26bb03eeed2d6e63e7ad", + "54732dc13d72d9f57580ebf69943d927", + "b88ad31f39b05cf5f305be2ddb073f6f", + "9cd5b43a79eb5ee177c8bba8ac15d168", + "950909eefb9170dee1d45b3969e00427", + "23e208e28a0bde09a3f046aa167bed38", + "ed2706a02ba04c9b4a1c1b8cfffb04a3", + "588a2708e07d0b338a2ed07cbea6af80", + "23059a68238c316e7822233ed2f6eb7a", + "e7cebf9ac14981339822d58b36cd4167", + "3428cef2bda3072f21dcc48bb54d3042", + "2110db4e61e860db243e6ddfd48e84d0", + "0f2ea09489ff2b0b7202ece325580741", + "ab6aad6bcb8fb77126ee5a2ebb039d23", + "36882f3914929a863054d0a216dc570d", + "e6b9a67f2314c460d0e8dadc84e979d8", + "582bdd8b90609e15502a5c9f70ca9baa", + "9fa97542b318a264b496d49c73bb2d19", + "8da8a03cf4017c330226f4718ed449f7", + "b7f15e01dca2d411491444b724ffbd68", + "0c336bd2a32f5fb269803611d3e623a4", + "c1b129d76a277373c6748927436794a1", + "f303f6cb6a53b5998b3df6d273e502fb", + "99b90cdc84ff584deff8b85be4de29f2", + "3beb71970b6ad6d42ad08ac2b077bb23", + "f4952ee5cf74f1a1c65ca51c1dcac9b4", + "1636578dcfcfb9fc7dbabe14e3f85322", + "20242c41b958e6fcaf16ce3c5e5ac2df", + "1f5398fde49a13097929bb51ee4c6c5c", + "6b426035d763bbfd14b24ccea8e7bfd1", + "66123deb830f5d697ffdfb48e8310ed6", + "189ba318c114f516bb9c7598319facb6", + "a41c4331dd165fed91a4520efefa60c9", + "96e3507c43dd3966884ba5f841673dc4", + "a4308628349e5b94d6151d128d62f8f9", + "24b9cb3b410c8bff36af293af5f1f6c4", + "c5f17e29d80e18551fa11edc9c533e3d", + "ca89232bbc3b590a12c21e4392437a92", + "150542ffea542c14a0ec1ca46add7d83", + "f0074cb65b559bc01f92ee0b2b2d851c", + "c20e6eaa9005a856a1295c5c43b849b5", + "61c579376b7d08d44982cd58efd2f2f3", + "cd7c18087349de540dc8c7ee53bca62e", + "545dda07c02e9ef635d10da90129712e", + "af193247eb37e7e2b237bec00a68fea5", + "a1a1368be1b97d9eb671dc59d2529e72", + "e51153a36891bc8c22edae72954af0af", + "82a2a2e229ee457b2a50e747fc05db45", + "6988aa461cb3dc59fb5b04aaff4af962", + "7617af042cf22ef700c27e3893878474", + "d84e5115883391ae8b4136de46eeb056", + "0674289ea495ed353988d4a2a39b1622", + "bbf870e8ff3e047fe87b37895ccb30a8", + "c62e8b15d60c0f45522c9607dcaef52c", + "6a2535d22f6c34f09868be21ada6063d", + "b54d58f9772149b9e40ba765a6f0c7a0", + "7a2bb00940c3ea825a5994fc6e1a5862", + "daccd8eb96035f7427c2ac984ef2669b", + "e93b78e2adbf25f11a13e4476cb5eb56", + "4621a7e39895cc326486e84f4000438a", + "f6cfda926b4214d72983d1abb8d7f0bf", + "15222df4f32a096b2178364382bb02b5", + "5825ed69a82645fda8a6e86b4c48aeaa", + "690fbef93df5dbc0316eff2a04e30244", + "0eeee51628e3a3d6943266a0a95bf4ff", + "7204f8130ec97d750287c03a613bdf23", + "fade1a84465a00a62c91325ecfdb788e", + "e061571ce0382db61922852144786a27", + "e369e19a152688b1dcc910784d41739d", + "bdc6190b3e945bea34219fc4a1849926", + "c0dd752a1b1143e6b41d411fd423ae17", + "08fe1c3af42005990a61685a2f35383c", + "029de50a465d0f816070c3d5f17bde37", + "bc75689a45409bffc566ff7f099030c2", + "2e577e6c6a15e33a7b3f26b01ab1497a", + "08038a3f13e60ba8bdde60a09ae429a3", + "1f6e4ac664308335b10cb31da28f03c3", + "e7c9e3c8ff869ffba99c64385dd58db5", + "855c9cc4f404f747d2af60dff6b3db3a", + "b6ae874fc675a3fd0f69cf36515cf8e5" + ] +} + diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265.md5 new file mode 100644 index 00000000..b460f218 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265.md5 @@ -0,0 +1,250 @@ +3aa1404b3c67f08effee1b22a1edfe35 +126904a814874104c62aaf430100013e +57a22d9cefafaf079f065bfb4c30153a +5e03e8591340813fe3f84b0f02a31570 +6ead9483dd1cb7e15c325525081aec55 +379af5d20d2745f7ef89ee902651d159 +d3ad92e815413ac0df5a152dff1513ef +a5a2774087b82062477b966c956ee507 +6372aea236df163e92abf32a23955586 +99f10be97458407433c6bc7e76e453c0 +699aa7d9609dde914f7a315da43b85a4 +b29a0c1424632d002dd6fc65a85ae702 +fbb0bbe60d3574300c4324350dd9cf94 +abd49b7181e7533e32d582cb89f4a890 +ece092ba9c23de4fd597d1d590b2fd5c +71fbd2135bcb03651fa3fce07fe212c2 +ab2f4e90aa360da6a67eb0b4b94d89a5 +c3ebfa439f56a5d4a1274642ccf5fe88 +e3646565b25d19be356d725c9ccdf571 +10ad345a9199940edc4cf259fd740d67 +b25f027b25711e949ca188e9b7c071b7 +79f38a78fa14b251c3d13b25829be484 +2f471f7f9e841bf8d0a71fec8c99edab +4386d538763d73d5f7ca8727aa0a1d0d +be88a7beb8bfd95c5eb03493f0a3d5ad +0beaeb365979f8355db831d8a39805c2 +c854fb790d5189b93bae7f5a8ee3c634 +557d551050575b0d7c36c103e587ed51 +bf1c245bf54695757f00d9a04defe688 +9f21b22a7e887e382b83a5fc070212e1 +97f63b86fb8dcdd3eb4e80be92672475 +9f6df014df10bd5983c15aebc5d9b0ae +9c8437e809cf388325101c45720d2965 +caf27527cf3cdef68449a8515bc271ab +1d2ffaed9291656e526ba9f33cc6edba +85a827b0a35ed63bf1ccd33aa8fa2fbb +c8d67d86ea8b4ff56591288f3b354881 +a690a754d9a51ae87a000f41d6157324 +cb31f37e307feeaeff6aa0df28ec4a63 +0ae07bc4b807aa17d831312ac7f8be00 +4a73a078bab877b40f4230d905b2c657 +4f99a4b8b320f540ac2ce76ee08a5131 +0e8043071109eaa1f983fa9f89c9893d +9827495323c5df10b900bed10caac240 +cf1e190b0cbe8a395630d9dbb8926f3e +aa8511fa4ddfcfb9c68d7cafb8dea11c +a49a2fc1c52f716d1fdb4eb75a148658 +5f25b4f699445d8af438b77a58554eb6 +734344be019030baf3801a2c238a3b81 +9d82b03748e1f39e795ed2e7c8048233 +26656158f2dc3e44b8ecfc302b6ba50d +2bdc0637fb6f1834bca11ed7a884d3d5 +a4c41fb0b47337bec3f98732bc2d72d6 +21b8fec70f82c45281e86651f3c10b4a +4bd700ca4c31f0b67c85a11d57013310 +d3c6545314d53d68ba842d5f65784230 +2f1826430cdc477c82bbf48c7178d4d0 +11cb5dcc7932047eff832efca8dabff9 +0d00bd23075c075240df89baf1533931 +7b2e8f4c9ce1edd3d88613bd38e1347a +224d1d502dfe17e65280afdb0944a768 +55f5e305c52979a4280310e91eb99c3e +7b021942409264048613a0bac9f261fa +98962ca6ee07c708b1cebf93d92ec706 +5901a19c4a240037bb1b1efe7ee0afb5 +6f49b4efe4155cf2e209c17d9e4bed3a +5fb93030e6f55756004b5abf3e5a9f47 +b6f1473717ef4ef16cff8721a51f53ce +e66e2f9382528cbbf4041dc1b315e8b3 +5104df3dd59e8f8c029ceb1da0dbe170 +aac0cd3df566a3b7d5a72bc5202c7752 +b97493d2bd5da6a25462f09dbe86a8a6 +c9dc6f8e721de915864d6a39be93171d +c67a196f47a9d8aca36d33dabd1c60c6 +f98dca89ac1d9da1e4ecd2f00748f8ca +4b223546c3f83f24c50ef023bdcfe096 +4a8d486fb79d01706cd51a2c7d446afb +a25a13b07a66c230edd3ac6aa97f2d70 +a478b43a9b2f7cb8086056ff981f3d82 +3a86ca51898cb11bfbf47f32c4e1948e +d48f1ec0f0740d506553472b5038001c +1e0597118f13b08c78fcb8d3e445e89d +0d3a037c2ea82a7adf739987eb77a4b3 +70cf1159356ad666fbff7eb33a4e220b +49aca942deb5340e899ec93a70c467bb +a4d8a4fe1963990af2167ddd193d2a2e +7da7b5197dcb138788d66742913d7238 +a806177db926b0c4b469b52329c32bc2 +3b0da86ca6b69266628b474ba4c6dbf5 +7547a5b2c8b5d4613e81ee3b6d7003f2 +80afa451394825bf9f61b000bd001857 +40807c933935dd92abb61e2b2da1fcc8 +508fdefd102f9e1f7804d39eae916655 +91d9ee56f07f46d1ec191e662bd935f8 +807c81583b0073a2746d752a4b1f42df +30734e3d8a710b269f5a29ce589b8a57 +4cdde1ffffc3dabe5b7e8f5c0ff8f084 +be6c0266091a2f3363dae1df8f724c9c +8858b0f16fb5babda61c4a6beb0523ca +16ffb13b8ff3aec78e8066cef06913f6 +3aed790a2749291489b12520da443dad +c57ba43539016f901db55073386d5cd9 +0f8f41751bd1fdc06b72c4d82d350d84 +aef1772f3fdcef057f6924a40da7c2d8 +f5f8b1460650b41f06cf8391bc18e426 +a729195174db4ceea1973eb6165a9d6e +e5022f7d2feaf57faa1e38ff1fc5bf68 +c694bf08869619a1bcc289127a408ed0 +0caaeabd4abbcc6d3bbca0b8b5d7cdb1 +971bdb6165a17e75773adeff5cf65fba +5bcba13d46fd61c73341d2d362962872 +95bc9f0f82248551d9b41d799beb08ac +7495973421c51dd51958d54b542ecc29 +7205d43bd4aeb3675aafb0b257d43136 +0720c6b6c9936f8457aeb9172c030a53 +6560c536d9c02d726f2f1d3737087ece +83dbde13fa583e0e6444dd6efb011b34 +23080ac9e24b9698a2abf602ccfd2d09 +ff9c9e733f7e8ba1d74fcc40b7f89762 +65a541bfb561ec6cd945283e00175039 +4c57d75ca3f96e852f24f11b5b9944f8 +1e42dc0503db109faa0ce0a4c2d1eb5b +284fc104d59d56c359631879d4ca5ae4 +a433eeff0d24300f106528ff3ee97dd9 +8b3cc99a2ba8b51ab37ff8e43ec39d88 +c883537d57780448f19b1fa22b731ddc +34ebc05648788fa25e14aecc00c941b0 +2a522bbb0f652b825f4546bd483b002a +1cd101eecab9fa491786310326aa02aa +c34b70539d2cf79ba8c4814ca0b2b2f4 +6a445c83d65f30a519d6f1257de3a5d8 +7d006c2ced504c486d09fbb5c70d8f48 +1ecaaf8b8b79c0134d224408feb4b12e +4f6fc08223fa794700c7fe2e13552aef +89c50b145e15de68a635acdc56118fdf +cf7d298515c15f7b969998211ed76d5c +22007d9a686417c64151ce79d890f1f6 +091caca7499775a91fba09b45d0efeba +6141cbf235f3d5f9ee7921795ac12948 +b5f9f13c06aaba11aba62edd8680a619 +cb81aeb1845c13741d7306e05ec620b5 +9e38b9005b5f87093069b0733186f92a +2cb31fe3452d1e1b188ac8071bbd2145 +38e1dc763e76422f652b4791ab0d00a0 +491418db6354101d9e9221f9a366ef4e +9dd1f141bac7936ce04b31d0d0e80db7 +f53ea317e8e16c5c556454b5139c6a3f +e4b632d8bd400354cf8d03bfac61987f +61195150e317268a4d8809db46344c01 +ff66c261ab7e00745d5f6dbd696dc4de +5e090a50613045d8f71460a9de77547e +83b8182ba7a6b77117eeb729f9d6fd53 +941a4dfa7c33d3a84993505f406e58aa +5b2dab7f75283b9d05a1baeb465afb8f +ccccd7a54b1b162b47267ad3c633588b +b649cc2512d02ded5f8fb1a5e6c4e468 +36d2229b2a85ca5eb5e627d4fefe5d0e +58f856aabbd3bb8df15d226c2c86f53a +6218b94752b4936b2a9332ad78ed3c78 +46317cf4bccec620087f52c7da5af936 +6f1d6769eafa76baa638f5eacd68bea8 +240996e0719b183d04808ed92b36ec02 +24cb70c1de6c09acb298836f591ccf60 +62abeb55d923017366c3c0b5913b290c +2074b8a3aaed5d41493578617834f865 +9bab5c6da1b475040a1ef3ad96039f16 +61a3914b0a1c2b563cc6be02f7c52c39 +9214566cdf649774366168904ba5a631 +b29a21da4b59070f8938175cf982610e +712b8106393fa454db36d409829fd446 +7303c34c4ee40273c96d97aa193254d1 +34aac7ac642a02409cea8ddc0e79ca5a +c905bb704eb57a5803b5f628a1885e65 +cfaeb64926d2cc463d2b039e25796e2c +631aa39682688ca2a9fa1b5ac046c84b +35e029947bb6ca6436f2287e56194910 +9559137575c4bdf3e4e32794c30d566e +b26eb2e52cd85d5c62d02ceef8beca5e +eda954629e3db49ce785db968dfd7435 +0c4db062f0a361a085e02a82c62ae623 +25505fb97d5a024c6a8c756b94417c85 +7145b53631fcdaf67d46f578dc061709 +86196a74f056323957abe0db608e063b +4a2cec3448951953c76676511ce24068 +34b6f3e81afd9002411772b291d376f9 +0a889a8d77a4f7d001734e5590a1143d +b2dd18c9a76ff8df04202b70a08e8480 +4966084a71252407273f57638148c878 +a9a0c3cd10496210ad5f423c67fd388c +c7cf572ff8d7d859e255a1865012af47 +2d3fad5c6403435842002f02012c23ed +e45d5b0fa1f296600bd8f6bdb91d59ff +b308537943d50f9cf6afb9289826f21f +1a6eacecb41a9984e8324900b7eafe43 +cee6bc8991e350ff29b2002d47e473c6 +fcb9de16c63bb0504dc97c3936f01479 +20ba7f47ea85ec2c1ede90415f69f96c +edfc01d0213d0da38817b95c7f2090fd +e89dd0d6f9982683375fd43fc10f8d33 +3937797142661006c3287488b3218b12 +33f13996becd2161301afdc6397c0582 +d8df3b5bbfd905c12c244c0252e49760 +fbd6b8d0fe1da3e6fe5464720212b94f +b48262d193c09a00ba217cf056832696 +ca7bb0ae9e2baca1669dee5b4181cba3 +556adb32715d449d18032f2b1b0d5bb8 +2cda45ece79f91c91d97dbecbfdb77ff +02be10a3e8fcef9ddaf31ec127117346 +a1bd20b8e168dca0743acee02e266bb8 +37959f49652a6a63c78700852ec32f99 +612cbca363753f225e4236e4ef7cbf08 +3593229c535f68e9285c13b91d2a84d0 +76dfdfdcae9abe98b65b0183f56d81b0 +b01c94b49b38b5f1b3cefcb0bb5baf4e +aa41e02d44472fc5bf4b7d70eedf6317 +1f08d61e6780d63803337b2ee8120ac3 +d96c5b0c7616a32308a3873a93055940 +fbedfad1f84c85373ed740fa7d9faee9 +8900bc3aea596ba8957fc57edd2590d5 +660524d84aa663b32198423942525e2a +ce749f243fdaa7ddbf79ef3ee145f8bb +bb22ba1c54cb0c1535d64a1b947ffcff +fe1af0925d781c8e733d0bbb93234d6a +6d29a1bdb45482834fc37adbfa4de834 +77f09e7535a1745cfde19b7e262b4a6f +cb6d2d2d5e94ab426166ccc46b8df0ad +a49a9e733db88096180a27411c66d211 +3d7caaddb5f71b238fb2c253ab835396 +7feab55c011e16389d52ee203187f55b +c06bc6b54bad2330d11c7ad711c8ee9a +49569cc2cd80e956e0bd0ee178abee42 +392eadde235ec3f44c16e347040e7dc3 +d358513ffb159bbe5d93f3cebf24f4e7 +b2b6c35f846328ffe100c2d6bcff2091 +868971765ea4221a6f8854847c65b2b3 +3b0d0a5b426029b3ccc7eba61e4a8fdb +0230e17adf575c13decdd0d225fed9a8 +24ccb608f3f4ee193f9553709ac112c0 +8da1c9cddc689c17b6a40df060769c6f +52c6bd707266df417b6ca83e83befe49 +95fea0bccde2991e6fe7476444517c3b +bd0cd099fa62bc0db33593e28c2b140c +04c8032d5f34150d4f011c6322fdc5c6 +71735c0de711d3e34f053927e25c1e26 +98853c3c86949059572bd5d711b91b26 +cc9a35cb0bde5916476212203f7af380 +5346c2a8f3fee6ff9a3068b52bcf590f +b13980ab3982fb964f80606acdf0dfb4 +d3137b6453aa267bc67be303db77630e +e71431bb7d535ddad95ed24bcea40451 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9.rs new file mode 100644 index 00000000..d26271ec --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9.rs @@ -0,0 +1,10 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#![warn(clippy::missing_panics_doc)] +#![warn(clippy::panic)] +#![warn(clippy::unwrap_used)] + +pub mod lookups; +pub mod parser; diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/lookups.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/lookups.rs new file mode 100644 index 00000000..4a189de4 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/lookups.rs @@ -0,0 +1,116 @@ +// Copyright 2022 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The DC Quantization lookup table for bit_depth = 8, as per "8.6.1 Dequantization functions" +pub const DC_QLOOKUP: [i16; 256] = [ + 4, 8, 8, 9, 10, 11, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 23, 24, 25, 26, 26, 27, + 28, 29, 30, 31, 32, 32, 33, 34, 35, 36, 37, 38, 38, 39, 40, 41, 42, 43, 43, 44, 45, 46, 47, 48, + 48, 49, 50, 51, 52, 53, 53, 54, 55, 56, 57, 57, 58, 59, 60, 61, 62, 62, 63, 64, 65, 66, 66, 67, + 68, 69, 70, 70, 71, 72, 73, 74, 74, 75, 76, 77, 78, 78, 79, 80, 81, 81, 82, 83, 84, 85, 85, 87, + 88, 90, 92, 93, 95, 96, 98, 99, 101, 102, 104, 105, 107, 108, 110, 111, 113, 114, 116, 117, + 118, 120, 121, 123, 125, 127, 129, 131, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, + 156, 158, 161, 164, 166, 169, 172, 174, 177, 180, 182, 185, 187, 190, 192, 195, 199, 202, 205, + 208, 211, 214, 217, 220, 223, 226, 230, 233, 237, 240, 243, 247, 250, 253, 257, 261, 265, 269, + 272, 276, 280, 284, 288, 292, 296, 300, 304, 309, 313, 317, 322, 326, 330, 335, 340, 344, 349, + 354, 359, 364, 369, 374, 379, 384, 389, 395, 400, 406, 411, 417, 423, 429, 435, 441, 447, 454, + 461, 467, 475, 482, 489, 497, 505, 513, 522, 530, 539, 549, 559, 569, 579, 590, 602, 614, 626, + 640, 654, 668, 684, 700, 717, 736, 755, 775, 796, 819, 843, 869, 896, 925, 955, 988, 1022, + 1058, 1098, 1139, 1184, 1232, 1282, 1336, +]; + +/// The DC Quantization lookup table for bit_depth = 10, as per "8.6.1 Dequantization functions" +pub const DC_QLOOKUP_10: [i16; 256] = [ + 4, 9, 10, 13, 15, 17, 20, 22, 25, 28, 31, 34, 37, 40, 43, 47, 50, 53, 57, 60, 64, 68, 71, 75, + 78, 82, 86, 90, 93, 97, 101, 105, 109, 113, 116, 120, 124, 128, 132, 136, 140, 143, 147, 151, + 155, 159, 163, 166, 170, 174, 178, 182, 185, 189, 193, 197, 200, 204, 208, 212, 215, 219, 223, + 226, 230, 233, 237, 241, 244, 248, 251, 255, 259, 262, 266, 269, 273, 276, 280, 283, 287, 290, + 293, 297, 300, 304, 307, 310, 314, 317, 321, 324, 327, 331, 334, 337, 343, 350, 356, 362, 369, + 375, 381, 387, 394, 400, 406, 412, 418, 424, 430, 436, 442, 448, 454, 460, 466, 472, 478, 484, + 490, 499, 507, 516, 525, 533, 542, 550, 559, 567, 576, 584, 592, 601, 609, 617, 625, 634, 644, + 655, 666, 676, 687, 698, 708, 718, 729, 739, 749, 759, 770, 782, 795, 807, 819, 831, 844, 856, + 868, 880, 891, 906, 920, 933, 947, 961, 975, 988, 1001, 1015, 1030, 1045, 1061, 1076, 1090, + 1105, 1120, 1137, 1153, 1170, 1186, 1202, 1218, 1236, 1253, 1271, 1288, 1306, 1323, 1342, 1361, + 1379, 1398, 1416, 1436, 1456, 1476, 1496, 1516, 1537, 1559, 1580, 1601, 1624, 1647, 1670, 1692, + 1717, 1741, 1766, 1791, 1817, 1844, 1871, 1900, 1929, 1958, 1990, 2021, 2054, 2088, 2123, 2159, + 2197, 2236, 2276, 2319, 2363, 2410, 2458, 2508, 2561, 2616, 2675, 2737, 2802, 2871, 2944, 3020, + 3102, 3188, 3280, 3375, 3478, 3586, 3702, 3823, 3953, 4089, 4236, 4394, 4559, 4737, 4929, 5130, + 5347, +]; + +/// The DC Quantization lookup table for bit_depth = 12, as per "8.6.1 Dequantization functions" +pub const DC_QLOOKUP_12: [i16; 256] = [ + 4, 12, 18, 25, 33, 41, 50, 60, 70, 80, 91, 103, 115, 127, 140, 153, 166, 180, 194, 208, 222, + 237, 251, 266, 281, 296, 312, 327, 343, 358, 374, 390, 405, 421, 437, 453, 469, 484, 500, 516, + 532, 548, 564, 580, 596, 611, 627, 643, 659, 674, 690, 706, 721, 737, 752, 768, 783, 798, 814, + 829, 844, 859, 874, 889, 904, 919, 934, 949, 964, 978, 993, 1008, 1022, 1037, 1051, 1065, 1080, + 1094, 1108, 1122, 1136, 1151, 1165, 1179, 1192, 1206, 1220, 1234, 1248, 1261, 1275, 1288, 1302, + 1315, 1329, 1342, 1368, 1393, 1419, 1444, 1469, 1494, 1519, 1544, 1569, 1594, 1618, 1643, 1668, + 1692, 1717, 1741, 1765, 1789, 1814, 1838, 1862, 1885, 1909, 1933, 1957, 1992, 2027, 2061, 2096, + 2130, 2165, 2199, 2233, 2267, 2300, 2334, 2367, 2400, 2434, 2467, 2499, 2532, 2575, 2618, 2661, + 2704, 2746, 2788, 2830, 2872, 2913, 2954, 2995, 3036, 3076, 3127, 3177, 3226, 3275, 3324, 3373, + 3421, 3469, 3517, 3565, 3621, 3677, 3733, 3788, 3843, 3897, 3951, 4005, 4058, 4119, 4181, 4241, + 4301, 4361, 4420, 4479, 4546, 4612, 4677, 4742, 4807, 4871, 4942, 5013, 5083, 5153, 5222, 5291, + 5367, 5442, 5517, 5591, 5665, 5745, 5825, 5905, 5984, 6063, 6149, 6234, 6319, 6404, 6495, 6587, + 6678, 6769, 6867, 6966, 7064, 7163, 7269, 7376, 7483, 7599, 7715, 7832, 7958, 8085, 8214, 8352, + 8492, 8635, 8788, 8945, 9104, 9275, 9450, 9639, 9832, 10031, 10245, 10465, 10702, 10946, 11210, + 11482, 11776, 12081, 12409, 12750, 13118, 13501, 13913, 14343, 14807, 15290, 15812, 16356, + 16943, 17575, 18237, 18949, 19718, 20521, 21387, +]; + +/// The AC Quantization lookup table for bit_depth = 8, as per "8.6.1 Dequantization functions" +pub const AC_QLOOKUP: [i16; 256] = [ + 4, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, + 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, + 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, + 140, 142, 144, 146, 148, 150, 152, 155, 158, 161, 164, 167, 170, 173, 176, 179, 182, 185, 188, + 191, 194, 197, 200, 203, 207, 211, 215, 219, 223, 227, 231, 235, 239, 243, 247, 251, 255, 260, + 265, 270, 275, 280, 285, 290, 295, 300, 305, 311, 317, 323, 329, 335, 341, 347, 353, 359, 366, + 373, 380, 387, 394, 401, 408, 416, 424, 432, 440, 448, 456, 465, 474, 483, 492, 501, 510, 520, + 530, 540, 550, 560, 571, 582, 593, 604, 615, 627, 639, 651, 663, 676, 689, 702, 715, 729, 743, + 757, 771, 786, 801, 816, 832, 848, 864, 881, 898, 915, 933, 951, 969, 988, 1007, 1026, 1046, + 1066, 1087, 1108, 1129, 1151, 1173, 1196, 1219, 1243, 1267, 1292, 1317, 1343, 1369, 1396, 1423, + 1451, 1479, 1508, 1537, 1567, 1597, 1628, 1660, 1692, 1725, 1759, 1793, 1828, +]; + +/// The AC Quantization lookup table for bit_depth = 10, as per "8.6.1 Dequantization functions" +pub const AC_QLOOKUP_10: [i16; 256] = [ + 4, 9, 11, 13, 16, 18, 21, 24, 27, 30, 33, 37, 40, 44, 48, 51, 55, 59, 63, 67, 71, 75, 79, 83, + 88, 92, 96, 100, 105, 109, 114, 118, 122, 127, 131, 136, 140, 145, 149, 154, 158, 163, 168, + 172, 177, 181, 186, 190, 195, 199, 204, 208, 213, 217, 222, 226, 231, 235, 240, 244, 249, 253, + 258, 262, 267, 271, 275, 280, 284, 289, 293, 297, 302, 306, 311, 315, 319, 324, 328, 332, 337, + 341, 345, 349, 354, 358, 362, 367, 371, 375, 379, 384, 388, 392, 396, 401, 409, 417, 425, 433, + 441, 449, 458, 466, 474, 482, 490, 498, 506, 514, 523, 531, 539, 547, 555, 563, 571, 579, 588, + 596, 604, 616, 628, 640, 652, 664, 676, 688, 700, 713, 725, 737, 749, 761, 773, 785, 797, 809, + 825, 841, 857, 873, 889, 905, 922, 938, 954, 970, 986, 1002, 1018, 1038, 1058, 1078, 1098, + 1118, 1138, 1158, 1178, 1198, 1218, 1242, 1266, 1290, 1314, 1338, 1362, 1386, 1411, 1435, 1463, + 1491, 1519, 1547, 1575, 1603, 1631, 1663, 1695, 1727, 1759, 1791, 1823, 1859, 1895, 1931, 1967, + 2003, 2039, 2079, 2119, 2159, 2199, 2239, 2283, 2327, 2371, 2415, 2459, 2507, 2555, 2603, 2651, + 2703, 2755, 2807, 2859, 2915, 2971, 3027, 3083, 3143, 3203, 3263, 3327, 3391, 3455, 3523, 3591, + 3659, 3731, 3803, 3876, 3952, 4028, 4104, 4184, 4264, 4348, 4432, 4516, 4604, 4692, 4784, 4876, + 4972, 5068, 5168, 5268, 5372, 5476, 5584, 5692, 5804, 5916, 6032, 6148, 6268, 6388, 6512, 6640, + 6768, 6900, 7036, 7172, 7312, +]; + +/// The AC Quantization lookup table for bit_depth = 12, as per "8.6.1 Dequantization functions" +pub const AC_QLOOKUP_12: [i16; 256] = [ + 4, 13, 19, 27, 35, 44, 54, 64, 75, 87, 99, 112, 126, 139, 154, 168, 183, 199, 214, 230, 247, + 263, 280, 297, 314, 331, 349, 366, 384, 402, 420, 438, 456, 475, 493, 511, 530, 548, 567, 586, + 604, 623, 642, 660, 679, 698, 716, 735, 753, 772, 791, 809, 828, 846, 865, 884, 902, 920, 939, + 957, 976, 994, 1012, 1030, 1049, 1067, 1085, 1103, 1121, 1139, 1157, 1175, 1193, 1211, 1229, + 1246, 1264, 1282, 1299, 1317, 1335, 1352, 1370, 1387, 1405, 1422, 1440, 1457, 1474, 1491, 1509, + 1526, 1543, 1560, 1577, 1595, 1627, 1660, 1693, 1725, 1758, 1791, 1824, 1856, 1889, 1922, 1954, + 1987, 2020, 2052, 2085, 2118, 2150, 2183, 2216, 2248, 2281, 2313, 2346, 2378, 2411, 2459, 2508, + 2556, 2605, 2653, 2701, 2750, 2798, 2847, 2895, 2943, 2992, 3040, 3088, 3137, 3185, 3234, 3298, + 3362, 3426, 3491, 3555, 3619, 3684, 3748, 3812, 3876, 3941, 4005, 4069, 4149, 4230, 4310, 4390, + 4470, 4550, 4631, 4711, 4791, 4871, 4967, 5064, 5160, 5256, 5352, 5448, 5544, 5641, 5737, 5849, + 5961, 6073, 6185, 6297, 6410, 6522, 6650, 6778, 6906, 7034, 7162, 7290, 7435, 7579, 7723, 7867, + 8011, 8155, 8315, 8475, 8635, 8795, 8956, 9132, 9308, 9484, 9660, 9836, 10028, 10220, 10412, + 10604, 10812, 11020, 11228, 11437, 11661, 11885, 12109, 12333, 12573, 12813, 13053, 13309, + 13565, 13821, 14093, 14365, 14637, 14925, 15213, 15502, 15806, 16110, 16414, 16734, 17054, + 17390, 17726, 18062, 18414, 18766, 19134, 19502, 19886, 20270, 20670, 21070, 21486, 21902, + 22334, 22766, 23214, 23662, 24126, 24590, 25070, 25551, 26047, 26559, 27071, 27599, 28143, + 28687, 29247, +]; diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/parser.rs b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/parser.rs new file mode 100644 index 00000000..3e91009e --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/parser.rs @@ -0,0 +1,1520 @@ +// Copyright 2022 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use crate::bitstream_utils::BitReader; +use crate::codec::vp9::lookups::AC_QLOOKUP; +use crate::codec::vp9::lookups::AC_QLOOKUP_10; +use crate::codec::vp9::lookups::AC_QLOOKUP_12; +use crate::codec::vp9::lookups::DC_QLOOKUP; +use crate::codec::vp9::lookups::DC_QLOOKUP_10; +use crate::codec::vp9::lookups::DC_QLOOKUP_12; + +pub const REFS_PER_FRAME: usize = 3; + +pub const MAX_REF_LF_DELTAS: usize = 4; +pub const MAX_MODE_LF_DELTAS: usize = 2; + +pub const INTRA_FRAME: usize = 0; +pub const LAST_FRAME: usize = 1; +pub const GOLDEN_FRAME: usize = 2; +pub const ALTREF_FRAME: usize = 3; +pub const MAX_REF_FRAMES: usize = 4; + +pub const MAX_SEGMENTS: usize = 8; +pub const SEG_TREE_PROBS: usize = MAX_SEGMENTS - 1; +pub const PREDICTION_PROBS: usize = 3; + +/// Valid segment features values. +#[repr(u8)] +pub enum SegLvl { + AltQ = 0, + AltL = 1, + RefFrame = 2, + LvlSkip = 3, +} +pub const SEG_LVL_MAX: usize = 4; + +pub const MAX_LOOP_FILTER: u32 = 63; + +pub const REF_FRAMES_LOG2: usize = 3; +pub const REF_FRAMES: usize = 1 << REF_FRAMES_LOG2; + +pub const SUPERFRAME_MARKER: u32 = 0x06; +pub const MAX_FRAMES_IN_SUPERFRAME: usize = 8; + +pub const FRAME_MARKER: u32 = 0x02; +pub const SYNC_CODE: u32 = 0x498342; + +pub const MIN_TILE_WIDTH_B64: u32 = 4; +pub const MAX_TILE_WIDTH_B64: u32 = 64; + +/// The number of pictures in the DPB +pub const NUM_REF_FRAMES: usize = 8; + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum InterpolationFilter { + #[default] + EightTap = 0, + EightTapSmooth = 1, + EightTapSharp = 2, + Bilinear = 3, + Switchable = 4, +} + +impl TryFrom for InterpolationFilter { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(InterpolationFilter::EightTap), + 1 => Ok(InterpolationFilter::EightTapSmooth), + 2 => Ok(InterpolationFilter::EightTapSharp), + 3 => Ok(InterpolationFilter::Bilinear), + 4 => Ok(InterpolationFilter::Switchable), + _ => Err(format!("Invalid InterpolationFilter {}", value)), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ReferenceFrameType { + Intra = 0, + Last = 1, + Golden = 2, + AltRef = 3, +} + +impl TryFrom for ReferenceFrameType { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(ReferenceFrameType::Intra), + 1 => Ok(ReferenceFrameType::Last), + 2 => Ok(ReferenceFrameType::Golden), + 3 => Ok(ReferenceFrameType::AltRef), + _ => Err(format!("Invalid ReferenceFrameType {}", value)), + } + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum FrameType { + #[default] + KeyFrame = 0, + InterFrame = 1, +} + +impl TryFrom for FrameType { + type Error = String; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(FrameType::KeyFrame), + 1 => Ok(FrameType::InterFrame), + _ => Err(format!("Invalid FrameType {}", value)), + } + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum Profile { + #[default] + Profile0 = 0, + Profile1 = 1, + Profile2 = 2, + Profile3 = 3, +} + +impl TryFrom for Profile { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Profile::Profile0), + 1 => Ok(Profile::Profile1), + 2 => Ok(Profile::Profile2), + 3 => Ok(Profile::Profile3), + _ => Err(format!("Invalid Profile {}", value)), + } + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum BitDepth { + #[default] + Depth8 = 8, + Depth10 = 10, + Depth12 = 12, +} + +impl TryFrom for BitDepth { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 8 => Ok(BitDepth::Depth8), + 10 => Ok(BitDepth::Depth10), + 12 => Ok(BitDepth::Depth12), + _ => Err(format!("Invalid BitDepth {}", value)), + } + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum ColorSpace { + #[default] + Unknown = 0, + Bt601 = 1, + Bt709 = 2, + Smpte170 = 3, + Smpte240 = 4, + Bt2020 = 5, + Reserved2 = 6, + CsSrgb = 7, +} + +impl TryFrom for ColorSpace { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(ColorSpace::Unknown), + 1 => Ok(ColorSpace::Bt601), + 2 => Ok(ColorSpace::Bt709), + 3 => Ok(ColorSpace::Smpte170), + 4 => Ok(ColorSpace::Smpte240), + 5 => Ok(ColorSpace::Bt2020), + 6 => Ok(ColorSpace::Reserved2), + 7 => Ok(ColorSpace::CsSrgb), + _ => Err(format!("Invalid ColorSpace {}", value)), + } + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum ColorRange { + #[default] + StudioSwing = 0, + FullSwing = 1, +} + +impl TryFrom for ColorRange { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(ColorRange::StudioSwing), + 1 => Ok(ColorRange::FullSwing), + _ => Err(format!("Invalid ColorRange {}", value)), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct LoopFilterParams { + /// Indicates the loop filter strength. + pub level: u8, + /// Indicates the sharpness level. The loop filter level and loop + /// filter_sharpness together determine when a block edge is filtered, and + /// by how much the filtering can change the sample values. + pub sharpness: u8, + /// If set, means that the filter level depends on the mode and reference + /// frame used to predict a block. If unset, means that the filter level + /// does not depend on the mode and reference frame. + pub delta_enabled: bool, + /// If set, means that the bitstream contains additional syntax elements + /// that specify which mode and reference frame deltas are to be updated. If + /// unset, means that these syntax elements are not present. + pub delta_update: bool, + /// If set, means that the bitstream contains additional syntax elements + /// that specify which mode and reference frame deltas are to be updated. If + /// unset, means that these syntax elements are not present. + pub update_ref_delta: [bool; MAX_REF_LF_DELTAS], + /// Contains the adjustment needed for the filter level based on the chosen + /// reference frame. If this syntax element is not present in the bitstream, + /// it maintains its previous value. + pub ref_deltas: [i8; MAX_REF_LF_DELTAS], + /// If set, means that the bitstream contains the syntax element + /// loop_filter_mode_deltas. If unset, means that the bitstream does not + /// contain this syntax element. + pub update_mode_delta: [bool; MAX_MODE_LF_DELTAS], + /// Contains the adjustment needed for the filter level based on the chosen + /// mode. If this syntax element is not present in the bitstream, it + /// maintains its previous value. + pub mode_deltas: [i8; MAX_MODE_LF_DELTAS], +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct QuantizationParams { + /// Indicates the base frame qindex. This is used for Y AC coefficients and + /// as the base value for the other quantizers. + pub base_q_idx: u8, + /// Indicates the Y DC quantizer relative to base_q_idx. + pub delta_q_y_dc: i8, + /// Indicates the UV DC quantizer relative to base_q_idx. + pub delta_q_uv_dc: i8, + /// Indicates the UV AC quantizer relative to base_q_idx. + pub delta_q_uv_ac: i8, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SegmentationParams { + /// If set, indicates that this frame makes use of the segmentation tool. + /// If unset, indicates that the frame does not use segmentation. + pub enabled: bool, + /// If set, indicates that the segmentation map should be updated during + /// the decoding of this frame. If unset, means that the segmentation map + /// from the previous frame is used. + pub update_map: bool, + /// Specify the probability values to be used when decoding segment_id. + pub tree_probs: [u8; SEG_TREE_PROBS], + /// Specify the probability values to be used when decoding seg_id_predicted. + pub pred_probs: [u8; PREDICTION_PROBS], + /// If set, indicates that the updates to the segmentation map are coded + /// relative to the existing segmentation map. If unset, + /// indicates that the new segmentation map is coded without + /// reference to the existing segmentation map. + pub temporal_update: bool, + /// If set, indicates that new parameters are about to be specified for each + /// segment. If unset, indicates that the segmentation parameters should + /// keep their existing values. + pub update_data: bool, + /// If unset, indicates that the segmentation parameters represent + /// adjustments relative to the standard values. If set, indicates that the + /// segmentation parameters represent the actual values to be used. + pub abs_or_delta_update: bool, + /// If unset, indicates that the corresponding feature is unused and has + /// value equal to 0. if set, indicates that the feature value is coded in + /// the bitstream. + pub feature_enabled: [[bool; SEG_LVL_MAX]; MAX_SEGMENTS], + /// Specifies the magnitude of the feature data for a segment feature. + pub feature_data: [[i16; SEG_LVL_MAX]; MAX_SEGMENTS], +} + +impl SegmentationParams { + /// Returns whether `feature` is enabled for `segment_id`. + fn is_feature_enabled(&self, segment_id: u8, feature: SegLvl) -> bool { + self.feature_enabled[segment_id as usize][feature as usize] + } + + /// An implementation of seg_feature_active as per "6.4.9 Segmentation feature active syntax" + fn is_feature_active(&self, segment_id: u8, feature: SegLvl) -> bool { + self.enabled && self.is_feature_enabled(segment_id, feature) + } + + /// Returns the data for `feature` on `segment_id`. + fn feature_data(&self, segment_id: u8, feature: SegLvl) -> i16 { + self.feature_data[segment_id as usize][feature as usize] + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Segmentation { + /// Loop filter level + pub lvl_lookup: [[u8; MAX_MODE_LF_DELTAS]; MAX_REF_FRAMES], + + /// AC quant scale for luma component + pub luma_ac_quant_scale: i16, + /// DC quant scale for luma component + pub luma_dc_quant_scale: i16, + /// AC quant scale for chroma component + pub chroma_ac_quant_scale: i16, + /// DC quant scale for chroma component + pub chroma_dc_quant_scale: i16, + + /// Whether the alternate reference frame segment feature is enabled (SEG_LVL_REF_FRAME) + pub reference_frame_enabled: bool, + /// The feature data for the reference frame featire + pub reference_frame: i16, + /// Whether the skip segment feature is enabled (SEG_LVL_SKIP) + pub reference_skip_enabled: bool, +} + +impl Segmentation { + /// Update the state of the segmentation parameters after seeing a frame + pub fn update_segmentation(segmentation: &mut [Segmentation; MAX_SEGMENTS], hdr: &Header) { + let lf = &hdr.lf; + let seg = &hdr.seg; + + let n_shift = lf.level >> 5; + + for segment_id in 0..MAX_SEGMENTS as u8 { + let luma_dc_quant_scale = hdr.get_dc_quant(segment_id, true); + let luma_ac_quant_scale = hdr.get_ac_quant(segment_id, true); + let chroma_dc_quant_scale = hdr.get_dc_quant(segment_id, false); + let chroma_ac_quant_scale = hdr.get_ac_quant(segment_id, false); + + let mut lvl_lookup: [[u8; MAX_MODE_LF_DELTAS]; MAX_REF_FRAMES]; + + if lf.level == 0 { + lvl_lookup = Default::default() + } else { + let mut lvl_seg = i32::from(lf.level); + + // 8.8.1 Loop filter frame init process + if hdr.seg.is_feature_active(segment_id, SegLvl::AltL) { + if seg.abs_or_delta_update { + lvl_seg = i32::from(seg.feature_data(segment_id, SegLvl::AltL)); + } else { + lvl_seg += i32::from(seg.feature_data(segment_id, SegLvl::AltL)); + } + } + + let lvl_seg = lvl_seg.clamp(0, MAX_LOOP_FILTER as i32) as u8; + + if !lf.delta_enabled { + lvl_lookup = [[lvl_seg; MAX_MODE_LF_DELTAS]; MAX_REF_FRAMES] + } else { + let intra_delta = lf.ref_deltas[INTRA_FRAME] as i32; + let mut intra_lvl = lvl_seg as i32 + (intra_delta << n_shift); + + lvl_lookup = segmentation[segment_id as usize].lvl_lookup; + lvl_lookup[INTRA_FRAME][0] = intra_lvl.clamp(0, MAX_LOOP_FILTER as i32) as u8; + + // Note, this array has the [0] element unspecified/unused in + // VP9. Confusing, but we do start to index from 1. + #[allow(clippy::needless_range_loop)] + for ref_ in LAST_FRAME..MAX_REF_FRAMES { + for mode in 0..MAX_MODE_LF_DELTAS { + let ref_delta = lf.ref_deltas[ref_] as i32; + let mode_delta = lf.mode_deltas[mode] as i32; + + intra_lvl = + lvl_seg as i32 + (ref_delta << n_shift) + (mode_delta << n_shift); + + lvl_lookup[ref_][mode] = + intra_lvl.clamp(0, MAX_LOOP_FILTER as i32) as u8; + } + } + } + } + + segmentation[usize::from(segment_id)] = Segmentation { + lvl_lookup, + luma_ac_quant_scale, + luma_dc_quant_scale, + chroma_ac_quant_scale, + chroma_dc_quant_scale, + reference_frame_enabled: seg.is_feature_enabled(segment_id, SegLvl::RefFrame), + reference_frame: seg.feature_data(segment_id, SegLvl::RefFrame), + reference_skip_enabled: seg.is_feature_enabled(segment_id, SegLvl::LvlSkip), + } + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct FrameSize { + width: u32, + height: u32, +} + +pub struct Frame<'a> { + /// The bitstream data for this frame. + bitstream: &'a [u8], + /// The frame header. + pub header: Header, + /// The offset into T + offset: usize, + /// The size of the data in T + size: usize, +} + +impl<'a> Frame<'a> { + pub fn new(bitstream: &'a [u8], header: Header, offset: usize, size: usize) -> Self { + Self { + bitstream, + header, + offset, + size, + } + } +} + +impl<'a> AsRef<[u8]> for Frame<'a> { + fn as_ref(&self) -> &[u8] { + let data = self.bitstream; + &data[self.offset..self.offset + self.size] + } +} + +/// A VP9 frame header. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Header { + /// A subset of syntax, semantics and algorithms defined in a part. + pub profile: Profile, + /// The bit depth of the frame. + pub bit_depth: BitDepth, + /// Specifies the chroma subsampling format. + pub subsampling_x: bool, + /// Specifies the chroma subsampling format. + pub subsampling_y: bool, + /// Specifies the color space of the stream. + pub color_space: ColorSpace, + /// Specifies the black level and range of the luma and chroma signals as + /// specified in Rec. ITU-R BT.709-6 and Rec. ITU-R BT.2020-2 + pub color_range: ColorRange, + /// Indicates the frame indexed by frame_to_show_map_idx is to be displayed. + /// If unset, indicates that further processing is required. + pub show_existing_frame: bool, + /// Specifies the frame to be displayed. It is only available if + /// show_existing_frame is set. + pub frame_to_show_map_idx: u8, + /// Indicates whether a frame is a key frame. + pub frame_type: FrameType, + /// Whether this frame should be displayed. + pub show_frame: bool, + /// Whether error resilient mode is enabled. + pub error_resilient_mode: bool, + /// The width of the frame in pixels. + pub width: u32, + /// The height of the frame in pixels. + pub height: u32, + /// If unset, means that the render width and height are inferred from the + /// frame width and height. If set, means that the render width and height + /// are explicitly coded in the bitstream. + pub render_and_frame_size_different: bool, + /// The render width of the frame in pixels. + pub render_width: u32, + /// The render height of the frame in pixels. + pub render_height: u32, + /// If set, indicates that this frame is an intra-only frame. If unset, + /// indicates that this frame is a inter frame. + pub intra_only: bool, + /// Specifies whether the frame context should be reset to default values. + pub reset_frame_context: u8, + /// Contains a bitmask that specifies which reference frame slots will be + /// updated with the current frame after it is decoded. + pub refresh_frame_flags: u8, + /// Specifies which reference frames are used by inter frames. It is a + /// requirement of bitstream conformance that the selected reference frames + /// match the current frame in bit depth, profile, chroma subsampling, and + /// color space. + pub ref_frame_idx: [u8; REFS_PER_FRAME], + /// Specifies the intended direction of the motion vector in time for each + /// reference frame. A sign bias equal to 0 indicates that the reference + /// frame is a backwards reference; a sign bias equal to 1 indicates that + /// the reference frame is a forwards reference + pub ref_frame_sign_bias: [u8; 4], + /// If unset, specifies that motion vectors are specified to quarter pel + /// precision. If set, specifies that motion vectors are specified to eighth + /// pel precision. + pub allow_high_precision_mv: bool, + /// The interpolation filter parameters. + pub interpolation_filter: InterpolationFilter, + /// If set, indicates that the probabilities computed for this frame (after + /// adapting to the observed frequencies if adaption is enabled) should be + /// stored for reference by future frames. If unset, indicates that the + /// probabilities should be discarded at the end of the frame. + pub refresh_frame_context: bool, + /// Whether parallel decoding mode is enabled. + pub frame_parallel_decoding_mode: bool, + /// Indicates the frame context to use. + pub frame_context_idx: u8, + /// The loop filter parameters + pub lf: LoopFilterParams, + /// The quantization parameters. + pub quant: QuantizationParams, + /// The segmentation parameters + pub seg: SegmentationParams, + /// Specifies the base 2 logarithm of the width of each tile (where the + /// width is measured in units of 8x8 blocks). It is a requirement of + /// bitstream conformance that tile_cols_log2 is less than or equal to 6. + pub tile_cols_log2: u8, + /// Specifies the base 2 logarithm of the height of each tile (where the + /// height is measured in units of 8x8 blocks). + pub tile_rows_log2: u8, + /// Computed from the syntax elements. If set, indicates that the frame is + /// coded using a special 4x4 transform designed for encoding frames that + /// are bit-identical with the original frames. + pub lossless: bool, + /// Indicates the size of the compressed header in bytes. + pub header_size_in_bytes: u16, + /// Indicates the size of the uncompressed header in bytes. + pub uncompressed_header_size_in_bytes: u16, +} + +impl Header { + /// An implementation of get_qindex as per "8.6.1 Dequantization functions" + fn get_qindex(&self, segment_id: u8) -> u8 { + let base_q_idx = self.quant.base_q_idx; + + if self.seg.is_feature_active(segment_id, SegLvl::AltQ) { + let mut data = self.seg.feature_data(segment_id, SegLvl::AltQ) as i32; + + if !self.seg.abs_or_delta_update { + data += base_q_idx as i32; + } + + data.clamp(0, 255) as u8 + } else { + base_q_idx + } + } + + /// An implementation of get_dc_quant as per "8.6.1 Dequantization functions" + fn get_dc_quant(&self, segment_id: u8, luma: bool) -> i16 { + let delta_q_dc = if luma { + self.quant.delta_q_y_dc + } else { + self.quant.delta_q_uv_dc + } as i32; + let qindex = self.get_qindex(segment_id); + let q_table_idx = (qindex as i32 + delta_q_dc).clamp(0, 255) as u8; + + let table = match self.bit_depth { + BitDepth::Depth8 => &DC_QLOOKUP, + BitDepth::Depth10 => &DC_QLOOKUP_10, + BitDepth::Depth12 => &DC_QLOOKUP_12, + }; + + table[q_table_idx as usize] + } + + /// An implementation of get_ac_quant as per "8.6.1 Dequantization functions" + fn get_ac_quant(&self, segment_id: u8, luma: bool) -> i16 { + let delta_q_ac = if luma { 0 } else { self.quant.delta_q_uv_ac } as i32; + let qindex = self.get_qindex(segment_id); + let q_table_idx = (qindex as i32 + delta_q_ac).clamp(0, 255) as u8; + + let table = match self.bit_depth { + BitDepth::Depth8 => &AC_QLOOKUP, + BitDepth::Depth10 => &AC_QLOOKUP_10, + BitDepth::Depth12 => &AC_QLOOKUP_12, + }; + + table[q_table_idx as usize] + } +} + +/// The VP9 superframe header as per Annex B, B.2.1, B.2.2 +struct SuperframeHeader { + /// Indicates the number of frames within this superframe. NOTE - It is + /// legal for a superframe to contain just a single frame and have NumFrames + /// equal to 1. + frames_in_superframe: u32, + /// Specifies the size in bytes of frame number i (zero indexed) within this + /// superframe. + frame_sizes: Vec, +} + +/// A VP9 bitstream parser. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Parser { + bit_depth: BitDepth, + subsampling_x: bool, + subsampling_y: bool, + color_space: ColorSpace, + color_range: ColorRange, + + mi_cols: u32, + mi_rows: u32, + sb64_cols: u32, + sb64_rows: u32, + + lf: LoopFilterParams, + seg: SegmentationParams, + + reference_frame_sz: [FrameSize; REF_FRAMES], +} + +impl Parser { + fn parse_superframe_hdr(resource: impl AsRef<[u8]>) -> Result { + let bitstream = resource.as_ref(); + + // Skip to the end of the chunk. + let mut reader = BitReader::new(&bitstream[bitstream.len() - 1..], false); + + // Try reading a superframe marker. + let marker = reader.read_bits::(3)?; + + if marker != SUPERFRAME_MARKER { + // Not a superframe + return Ok(SuperframeHeader { + frames_in_superframe: 1, + frame_sizes: vec![bitstream.len()], + }); + } + + let bytes_per_framesize = reader.read_bits::(2)? + 1; + let frames_in_superframe = reader.read_bits::(3)? + 1; + + if frames_in_superframe > MAX_FRAMES_IN_SUPERFRAME as u32 { + return Err(format!( + "Broken stream: too many frames in superframe, expected a maximum of {:?}, found {:?}", + MAX_FRAMES_IN_SUPERFRAME, + frames_in_superframe + )); + } + + let sz_index = 2 + frames_in_superframe * bytes_per_framesize; + + let data = resource.as_ref(); + let index_offset = data.len() - sz_index as usize; + let first_byte = data[index_offset]; + let last_byte = *data + .last() + .ok_or_else(|| String::from("superframe header is empty"))?; + + if first_byte != last_byte { + // Also not a superframe, we must pass both tests as per the specification. + return Ok(SuperframeHeader { + frames_in_superframe: 1, + frame_sizes: vec![bitstream.len()], + }); + } + + let mut frame_sizes = vec![]; + let mut reader = BitReader::new(&bitstream[index_offset..], false); + + // Skip the superframe header. + let _ = reader.read_bits::(8)?; + + for _ in 0..frames_in_superframe { + let mut frame_size = 0; + + for j in 0..bytes_per_framesize { + frame_size |= reader.read_bits::(8)? << (j * 8); + } + + frame_sizes.push(frame_size as usize); + } + + Ok(SuperframeHeader { + frames_in_superframe, + frame_sizes, + }) + } + + fn read_signed_8(r: &mut BitReader, nbits: u8) -> Result { + let value = r.read_bits::(nbits as usize)?; + let negative = r.read_bit()?; + + if negative { + Ok(-(value as i8)) + } else { + Ok(value as i8) + } + } + + fn parse_frame_marker(r: &mut BitReader) -> Result<(), String> { + let marker = r.read_bits::(2)?; + + if marker != FRAME_MARKER { + return Err(format!( + "Broken stream: expected frame marker, found {:?}", + marker + )); + } + + Ok(()) + } + + fn parse_profile(r: &mut BitReader) -> Result { + let low = r.read_bits::(1)?; + let high = r.read_bits::(1)?; + + let profile = (high << 1) | low; + + if profile == 3 { + // Skip the reserved bit + let _ = r.read_bit()?; + } + + Profile::try_from(profile) + } + + fn parse_frame_sync_code(r: &mut BitReader) -> Result<(), String> { + let sync_code = r.read_bits::(24)?; + + if sync_code != SYNC_CODE { + return Err(format!( + "Broken stream: expected sync code == {:?}, found {:?}", + SYNC_CODE, sync_code + )); + } + + Ok(()) + } + + fn parse_color_config(&mut self, r: &mut BitReader, hdr: &mut Header) -> Result<(), String> { + if matches!(hdr.profile, Profile::Profile2 | Profile::Profile3) { + let ten_or_twelve_bit = r.read_bit()?; + if ten_or_twelve_bit { + hdr.bit_depth = BitDepth::Depth12; + } else { + hdr.bit_depth = BitDepth::Depth10 + } + } else { + hdr.bit_depth = BitDepth::Depth8; + } + + let color_space = r.read_bits::(3)?; + hdr.color_space = ColorSpace::try_from(color_space)?; + + if !matches!(hdr.color_space, ColorSpace::CsSrgb) { + let color_range = r.read_bits::(1)?; + + hdr.color_range = ColorRange::try_from(color_range)?; + + if matches!(hdr.profile, Profile::Profile1 | Profile::Profile3) { + hdr.subsampling_x = r.read_bit()?; + hdr.subsampling_y = r.read_bit()?; + + // Skip the reserved bit + let _ = r.read_bit()?; + } else { + hdr.subsampling_x = true; + hdr.subsampling_y = true; + } + } else { + hdr.color_range = ColorRange::FullSwing; + if matches!(hdr.profile, Profile::Profile1 | Profile::Profile3) { + hdr.subsampling_x = false; + hdr.subsampling_y = false; + + // Skip the reserved bit + let _ = r.read_bit()?; + } + } + + self.bit_depth = hdr.bit_depth; + self.color_space = hdr.color_space; + self.subsampling_x = hdr.subsampling_x; + self.subsampling_y = hdr.subsampling_y; + self.color_range = hdr.color_range; + + Ok(()) + } + + fn compute_image_size(&mut self, width: u32, height: u32) { + self.mi_cols = (width + 7) >> 3; + self.mi_rows = (height + 7) >> 3; + self.sb64_cols = (self.mi_cols + 7) >> 3; + self.sb64_rows = (self.mi_rows + 7) >> 3; + } + + fn parse_frame_size(&mut self, r: &mut BitReader, hdr: &mut Header) -> Result<(), String> { + hdr.width = r.read_bits::(16)? + 1; + hdr.height = r.read_bits::(16)? + 1; + self.compute_image_size(hdr.width, hdr.height); + Ok(()) + } + + fn parse_render_size(r: &mut BitReader, hdr: &mut Header) -> Result<(), String> { + hdr.render_and_frame_size_different = r.read_bit()?; + if hdr.render_and_frame_size_different { + hdr.render_width = r.read_bits::(16)? + 1; + hdr.render_height = r.read_bits::(16)? + 1; + } else { + hdr.render_width = hdr.width; + hdr.render_height = hdr.height; + } + + Ok(()) + } + + fn parse_frame_size_with_refs( + &mut self, + r: &mut BitReader, + hdr: &mut Header, + ) -> Result<(), String> { + let mut found_ref = false; + + for i in 0..REFS_PER_FRAME { + found_ref = r.read_bit()?; + + if found_ref { + let idx = hdr.ref_frame_idx[i] as usize; + hdr.width = self.reference_frame_sz[idx].width; + hdr.height = self.reference_frame_sz[idx].height; + break; + } + } + + if !found_ref { + self.parse_frame_size(r, hdr)?; + } else { + self.compute_image_size(hdr.width, hdr.height) + } + + Self::parse_render_size(r, hdr) + } + + fn read_interpolation_filter(r: &mut BitReader) -> Result { + const LITERAL_TO_TYPE: [InterpolationFilter; 4] = [ + InterpolationFilter::EightTapSmooth, + InterpolationFilter::EightTap, + InterpolationFilter::EightTapSharp, + InterpolationFilter::Bilinear, + ]; + + let is_filter_switchable = r.read_bit()?; + + Ok(if is_filter_switchable { + InterpolationFilter::Switchable + } else { + let raw_interpolation_filter = r.read_bits::(2)?; + LITERAL_TO_TYPE[raw_interpolation_filter as usize] + }) + } + + fn setup_past_independence(&mut self, hdr: &mut Header) { + self.seg.feature_enabled = Default::default(); + self.seg.feature_data = Default::default(); + self.seg.abs_or_delta_update = false; + + self.lf.delta_enabled = true; + self.lf.ref_deltas[ReferenceFrameType::Intra as usize] = 1; + self.lf.ref_deltas[ReferenceFrameType::Last as usize] = 0; + self.lf.ref_deltas[ReferenceFrameType::Golden as usize] = -1; + self.lf.ref_deltas[ReferenceFrameType::AltRef as usize] = -1; + + self.lf.mode_deltas = Default::default(); + hdr.ref_frame_sign_bias = Default::default(); + } + + fn parse_loop_filter_params( + r: &mut BitReader, + lf: &mut LoopFilterParams, + ) -> Result<(), String> { + lf.level = r.read_bits::(6)?; + lf.sharpness = r.read_bits::(3)?; + lf.delta_enabled = r.read_bit()?; + + if lf.delta_enabled { + lf.delta_update = r.read_bit()?; + if lf.delta_update { + for i in 0..MAX_REF_LF_DELTAS { + lf.update_ref_delta[i] = r.read_bit()?; + if lf.update_ref_delta[i] { + lf.ref_deltas[i] = Self::read_signed_8(r, 6)?; + } + } + + for i in 0..MAX_MODE_LF_DELTAS { + lf.update_mode_delta[i] = r.read_bit()?; + if lf.update_mode_delta[i] { + lf.mode_deltas[i] = Self::read_signed_8(r, 6)?; + } + } + } + } + + Ok(()) + } + + fn read_delta_q(r: &mut BitReader, value: &mut i8) -> Result<(), String> { + let delta_coded = r.read_bit()?; + + if delta_coded { + *value = Self::read_signed_8(r, 4)?; + } else { + *value = 0; + } + + Ok(()) + } + + fn parse_quantization_params(r: &mut BitReader, hdr: &mut Header) -> Result<(), String> { + let quant = &mut hdr.quant; + + quant.base_q_idx = r.read_bits::(8)?; + + Self::read_delta_q(r, &mut quant.delta_q_y_dc)?; + Self::read_delta_q(r, &mut quant.delta_q_uv_dc)?; + Self::read_delta_q(r, &mut quant.delta_q_uv_ac)?; + + hdr.lossless = quant.base_q_idx == 0 + && quant.delta_q_y_dc == 0 + && quant.delta_q_uv_dc == 0 + && quant.delta_q_uv_ac == 0; + + Ok(()) + } + + fn read_prob(r: &mut BitReader) -> Result { + let prob_coded = r.read_bit()?; + + let prob = if prob_coded { + r.read_bits::(8)? + } else { + 255 + }; + + Ok(prob) + } + + fn parse_segmentation_params( + r: &mut BitReader, + seg: &mut SegmentationParams, + ) -> Result<(), String> { + const SEGMENTATION_FEATURE_BITS: [u8; SEG_LVL_MAX] = [8, 6, 2, 0]; + const SEGMENTATION_FEATURE_SIGNED: [bool; SEG_LVL_MAX] = [true, true, false, false]; + + seg.update_map = false; + seg.update_data = false; + + seg.enabled = r.read_bit()?; + + if !seg.enabled { + return Ok(()); + } + + seg.update_map = r.read_bit()?; + + if seg.update_map { + for i in 0..SEG_TREE_PROBS { + seg.tree_probs[i] = Self::read_prob(r)?; + } + + seg.temporal_update = r.read_bit()?; + + for i in 0..PREDICTION_PROBS { + seg.pred_probs[i] = if seg.temporal_update { + Self::read_prob(r)? + } else { + 255 + }; + } + } + + seg.update_data = r.read_bit()?; + + if seg.update_data { + seg.abs_or_delta_update = r.read_bit()?; + for i in 0..MAX_SEGMENTS { + for j in 0..SEG_LVL_MAX { + seg.feature_enabled[i][j] = r.read_bit()?; + if seg.feature_enabled[i][j] { + let bits_to_read = SEGMENTATION_FEATURE_BITS[j]; + let mut feature_value = r.read_bits_signed::(bits_to_read as usize)?; + + if SEGMENTATION_FEATURE_SIGNED[j] { + let feature_sign = r.read_bit()?; + + if feature_sign { + feature_value = -feature_value; + } + } + + seg.feature_data[i][j] = feature_value; + } + } + } + } + + Ok(()) + } + + fn calc_min_log2_tile_cols(sb64_cols: u32) -> u8 { + let mut min_log2 = 0; + + while (MAX_TILE_WIDTH_B64 << min_log2) < sb64_cols { + min_log2 += 1; + } + + min_log2 + } + + fn calc_max_log2_tile_cols(sb64_cols: u32) -> u8 { + let mut max_log2 = 1; + + while (sb64_cols >> max_log2) >= MIN_TILE_WIDTH_B64 { + max_log2 += 1; + } + + max_log2 - 1 + } + + fn parse_tile_info(&self, r: &mut BitReader, hdr: &mut Header) -> Result<(), String> { + let max_log2_tile_cols = Self::calc_max_log2_tile_cols(self.sb64_cols); + + hdr.tile_cols_log2 = Self::calc_min_log2_tile_cols(self.sb64_cols); + + while hdr.tile_cols_log2 < max_log2_tile_cols { + let increment_tile_cols_log2 = r.read_bit()?; + + if increment_tile_cols_log2 { + hdr.tile_cols_log2 += 1; + } else { + break; + } + } + + hdr.tile_rows_log2 = r.read_bits::(1)?; + + if hdr.tile_rows_log2 > 0 { + let increment_tile_rows_log2 = r.read_bit()?; + hdr.tile_rows_log2 += increment_tile_rows_log2 as u8; + } + + Ok(()) + } + + fn parse_frame_header( + &mut self, + resource: impl AsRef<[u8]>, + offset: usize, + ) -> Result { + let data = &resource.as_ref()[offset..]; + let mut r = BitReader::new(data, false); + let mut hdr = Header::default(); + + Self::parse_frame_marker(&mut r)?; + hdr.profile = Self::parse_profile(&mut r)?; + + hdr.show_existing_frame = r.read_bit()?; + + if hdr.show_existing_frame { + hdr.frame_to_show_map_idx = r.read_bits::(3)?; + return Ok(hdr); + } + + hdr.frame_type = FrameType::try_from(r.read_bits::(1)?)?; + + hdr.show_frame = r.read_bit()?; + hdr.error_resilient_mode = r.read_bit()?; + + let frame_is_intra; + + if matches!(hdr.frame_type, FrameType::KeyFrame) { + Self::parse_frame_sync_code(&mut r)?; + self.parse_color_config(&mut r, &mut hdr)?; + self.parse_frame_size(&mut r, &mut hdr)?; + Self::parse_render_size(&mut r, &mut hdr)?; + hdr.refresh_frame_flags = 0xff; + frame_is_intra = true; + } else { + if !hdr.show_frame { + hdr.intra_only = r.read_bit()?; + } + + frame_is_intra = hdr.intra_only; + + if !hdr.error_resilient_mode { + hdr.reset_frame_context = r.read_bits::(2)?; + } else { + hdr.reset_frame_context = 0; + } + + if hdr.intra_only { + Self::parse_frame_sync_code(&mut r)?; + + if !matches!(hdr.profile, Profile::Profile0) { + self.parse_color_config(&mut r, &mut hdr)?; + } else { + hdr.color_space = ColorSpace::Bt601; + hdr.subsampling_x = true; + hdr.subsampling_y = true; + hdr.bit_depth = BitDepth::Depth8; + + self.color_space = hdr.color_space; + self.subsampling_x = hdr.subsampling_x; + self.subsampling_y = hdr.subsampling_y; + self.bit_depth = hdr.bit_depth; + } + + hdr.refresh_frame_flags = r.read_bits::(8)?; + self.parse_frame_size(&mut r, &mut hdr)?; + Self::parse_render_size(&mut r, &mut hdr)?; + } else { + // Copy from our cached version + hdr.color_space = self.color_space; + hdr.color_range = self.color_range; + hdr.subsampling_x = self.subsampling_x; + hdr.subsampling_y = self.subsampling_y; + hdr.bit_depth = self.bit_depth; + + hdr.refresh_frame_flags = r.read_bits::(8)?; + + for i in 0..REFS_PER_FRAME { + hdr.ref_frame_idx[i] = r.read_bits::(3)?; + hdr.ref_frame_sign_bias[ReferenceFrameType::Last as usize + i] = + r.read_bits::(1)?; + } + + self.parse_frame_size_with_refs(&mut r, &mut hdr)?; + hdr.allow_high_precision_mv = r.read_bit()?; + hdr.interpolation_filter = Self::read_interpolation_filter(&mut r)?; + } + } + + if !hdr.error_resilient_mode { + hdr.refresh_frame_context = r.read_bit()?; + hdr.frame_parallel_decoding_mode = r.read_bit()?; + } else { + hdr.refresh_frame_context = false; + hdr.frame_parallel_decoding_mode = true; + } + + hdr.frame_context_idx = r.read_bits::(2)?; + + if frame_is_intra || hdr.error_resilient_mode { + self.setup_past_independence(&mut hdr); + } + + Self::parse_loop_filter_params(&mut r, &mut self.lf)?; + Self::parse_quantization_params(&mut r, &mut hdr)?; + Self::parse_segmentation_params(&mut r, &mut self.seg)?; + self.parse_tile_info(&mut r, &mut hdr)?; + + hdr.header_size_in_bytes = r.read_bits::(16)?; + + hdr.lf = self.lf.clone(); + hdr.seg = self.seg.clone(); + + for i in 0..REF_FRAMES { + let flag = 1 << i; + if hdr.refresh_frame_flags & flag != 0 { + self.reference_frame_sz[i].width = hdr.width; + self.reference_frame_sz[i].height = hdr.height; + } + } + + hdr.uncompressed_header_size_in_bytes = (r.position() as u16 + 7) / 8; + + Ok(hdr) + } + + /// Parse a single VP9 frame. + pub fn parse_frame<'a>( + &mut self, + bitstream: &'a [u8], + offset: usize, + size: usize, + ) -> Result, String> { + let header = self.parse_frame_header(bitstream, offset)?; + + Ok(Frame { + header, + bitstream, + offset, + size, + }) + } + + /// Parses VP9 frames from the data in `resource`. This can result in more than one frame if the + /// data passed in contains a VP9 superframe. + pub fn parse_chunk<'a>(&mut self, resource: &'a [u8]) -> Result>, String> { + let superframe_hdr = Parser::parse_superframe_hdr(resource)?; + let mut offset = 0; + + let mut frames = vec![]; + + for i in 0..superframe_hdr.frames_in_superframe { + let frame_sz = superframe_hdr.frame_sizes[i as usize]; + let frame = self.parse_frame(resource, offset, frame_sz)?; + offset += frame_sz; + frames.push(frame); + } + + Ok(frames) + } +} + +#[cfg(test)] +mod tests { + use crate::bitstream_utils::IvfIterator; + use crate::codec::vp9::parser::BitDepth; + use crate::codec::vp9::parser::ColorSpace; + use crate::codec::vp9::parser::FrameType; + use crate::codec::vp9::parser::InterpolationFilter; + use crate::codec::vp9::parser::Parser; + use crate::codec::vp9::parser::Profile; + use crate::codec::vp9::parser::MAX_SEGMENTS; + use crate::codec::vp9::parser::SEG_LVL_MAX; + + #[test] + fn test_parse_superframe() { + // Demuxed, raw vp9 superframe + const VP9_TEST_SUPERFRAME: &[u8] = include_bytes!("test_data/vp9-superframe.bin"); + + let mut parser = Parser::default(); + let frames = parser + .parse_chunk(VP9_TEST_SUPERFRAME) + .expect("Parsing a superframe failed"); + + assert_eq!(frames.len(), 2); + assert_eq!(frames[0].offset, 0); + assert_eq!(frames[0].size, 1333); + assert_eq!(frames[1].offset, 1333); + assert_eq!(frames[1].size, 214); + } + + #[test] + fn test_parse_test25fps() { + // Muxed as IVF + const TEST_STREAM: &[u8] = include_bytes!("test_data/test-25fps.vp9"); + + let mut parser = Parser::default(); + let ivf_iter = IvfIterator::new(TEST_STREAM); + + for (frame_n, packet) in ivf_iter.enumerate() { + let frames = parser + .parse_chunk(packet.as_ref()) + .expect("Parsing a superframe failed"); + + if frame_n == 0 { + assert_eq!(frames.len(), 1); + let h = &frames[0].header; + + assert!(matches!(h.profile, Profile::Profile0)); + assert!(matches!(h.bit_depth, BitDepth::Depth8)); + + assert!(h.subsampling_x); + assert!(h.subsampling_y); + + assert!(matches!(h.color_space, ColorSpace::Unknown)); + assert!(matches!( + h.color_range, + crate::codec::vp9::parser::ColorRange::StudioSwing + )); + + assert!(!h.show_existing_frame); + assert_eq!(h.frame_to_show_map_idx, 0); + + assert!(matches!(h.frame_type, FrameType::KeyFrame)); + assert!(h.show_frame); + assert!(!h.error_resilient_mode); + + assert_eq!(h.width, 320); + assert_eq!(h.height, 240); + + assert!(!h.render_and_frame_size_different); + + assert_eq!(h.render_width, 320); + assert_eq!(h.render_height, 240); + + assert!(!h.intra_only); + assert_eq!(h.reset_frame_context, 0); + + assert_eq!(h.refresh_frame_flags, 0xff); + assert_eq!(h.ref_frame_idx, [0, 0, 0]); + assert_eq!(h.ref_frame_sign_bias, [0, 0, 0, 0]); + + assert!(!h.allow_high_precision_mv); + assert!(matches!( + h.interpolation_filter, + InterpolationFilter::EightTap + )); + + assert!(h.refresh_frame_context); + assert!(h.frame_parallel_decoding_mode); + assert_eq!(h.frame_context_idx, 0); + + let lf = &h.lf; + assert_eq!(lf.level, 9); + assert_eq!(lf.sharpness, 0); + + assert!(lf.delta_enabled); + assert!(lf.delta_update); + + assert_eq!(lf.update_ref_delta, [true, false, true, true]); + assert_eq!(lf.ref_deltas, [1, 0, -1, -1]); + + assert_eq!(lf.update_mode_delta, [false, false]); + + let q = &h.quant; + + assert_eq!(q.base_q_idx, 65); + assert_eq!(q.delta_q_y_dc, 0); + assert_eq!(q.delta_q_uv_dc, 0); + assert_eq!(q.delta_q_uv_ac, 0); + + let s = &h.seg; + + assert!(!s.enabled); + assert!(!s.update_map); + assert_eq!(s.tree_probs, [0, 0, 0, 0, 0, 0, 0]); + assert_eq!(s.pred_probs, [0, 0, 0]); + assert!(!s.temporal_update); + assert!(!s.update_data); + assert!(!s.abs_or_delta_update); + assert_eq!(s.feature_enabled, [[false; SEG_LVL_MAX]; MAX_SEGMENTS]); + assert_eq!(s.feature_data, [[0; SEG_LVL_MAX]; MAX_SEGMENTS]); + + assert_eq!(h.tile_cols_log2, 0); + assert_eq!(h.tile_rows_log2, 0); + assert_eq!(h.header_size_in_bytes, 120); + + assert!(!h.lossless); + } else if frame_n == 1 { + assert_eq!(frames.len(), 2); + + assert_eq!(frames[0].offset, 0); + assert_eq!(frames[0].size, 2390); + assert_eq!(frames[1].offset, 2390); + assert_eq!(frames[1].size, 108); + + let h = &frames[0].header; + + assert!(matches!(h.profile, Profile::Profile0)); + assert!(matches!(h.bit_depth, BitDepth::Depth8)); + + assert!(h.subsampling_x); + assert!(h.subsampling_y); + + assert!(matches!(h.color_space, ColorSpace::Unknown)); + assert!(matches!( + h.color_range, + crate::codec::vp9::parser::ColorRange::StudioSwing + )); + + assert!(!h.show_existing_frame); + assert_eq!(h.frame_to_show_map_idx, 0); + + assert!(matches!(h.frame_type, FrameType::InterFrame)); + assert!(!h.show_frame); + assert!(!h.error_resilient_mode); + + assert_eq!(h.width, 320); + assert_eq!(h.height, 240); + + assert!(!h.render_and_frame_size_different); + + assert_eq!(h.render_width, 320); + assert_eq!(h.render_height, 240); + + assert!(!h.intra_only); + assert_eq!(h.reset_frame_context, 0); + + assert_eq!(h.refresh_frame_flags, 4); + assert_eq!(h.ref_frame_idx, [0, 1, 2]); + assert_eq!(h.ref_frame_sign_bias, [0, 0, 0, 0]); + + assert!(h.allow_high_precision_mv); + assert!(matches!( + h.interpolation_filter, + InterpolationFilter::EightTap + )); + + assert!(h.refresh_frame_context); + assert!(h.frame_parallel_decoding_mode); + assert_eq!(h.frame_context_idx, 1); + + let lf = &h.lf; + assert_eq!(lf.level, 15); + assert_eq!(lf.sharpness, 0); + + assert!(lf.delta_enabled); + assert!(!lf.delta_update); + + assert_eq!(lf.update_ref_delta, [true, false, true, true]); + assert_eq!(lf.ref_deltas, [1, 0, -1, -1]); + + assert_eq!(lf.update_mode_delta, [false, false]); + + let q = &h.quant; + + assert_eq!(q.base_q_idx, 112); + assert_eq!(q.delta_q_y_dc, 0); + assert_eq!(q.delta_q_uv_dc, 0); + assert_eq!(q.delta_q_uv_ac, 0); + + let s = &h.seg; + + assert!(!s.enabled); + assert!(!s.update_map); + assert_eq!(s.tree_probs, [0, 0, 0, 0, 0, 0, 0]); + assert_eq!(s.pred_probs, [0, 0, 0]); + assert!(!s.temporal_update); + assert!(!s.update_data); + assert!(!s.abs_or_delta_update); + assert_eq!(s.feature_enabled, [[false; SEG_LVL_MAX]; MAX_SEGMENTS]); + assert_eq!(s.feature_data, [[0; SEG_LVL_MAX]; MAX_SEGMENTS]); + + assert_eq!(h.tile_cols_log2, 0); + assert_eq!(h.tile_rows_log2, 0); + assert_eq!(h.header_size_in_bytes, 48); + + assert!(!h.lossless); + + let h = &frames[1].header; + + assert!(matches!(h.profile, Profile::Profile0)); + assert!(matches!(h.bit_depth, BitDepth::Depth8)); + + assert!(h.subsampling_x); + assert!(h.subsampling_y); + + assert!(matches!(h.color_space, ColorSpace::Unknown)); + assert!(matches!( + h.color_range, + crate::codec::vp9::parser::ColorRange::StudioSwing + )); + + assert!(!h.show_existing_frame); + assert_eq!(h.frame_to_show_map_idx, 0); + + assert!(matches!(h.frame_type, FrameType::InterFrame)); + assert!(h.show_frame); + assert!(!h.error_resilient_mode); + + assert_eq!(h.width, 320); + assert_eq!(h.height, 240); + + assert!(!h.render_and_frame_size_different); + + assert_eq!(h.render_width, 320); + assert_eq!(h.render_height, 240); + + assert!(!h.intra_only); + assert_eq!(h.reset_frame_context, 0); + + assert_eq!(h.refresh_frame_flags, 1); + assert_eq!(h.ref_frame_idx, [0, 1, 2]); + assert_eq!(h.ref_frame_sign_bias, [0, 0, 0, 1]); + + assert!(!h.allow_high_precision_mv); + assert!(matches!( + h.interpolation_filter, + InterpolationFilter::EightTap + )); + + assert!(h.refresh_frame_context); + assert!(h.frame_parallel_decoding_mode); + assert_eq!(h.frame_context_idx, 0); + + let lf = &h.lf; + assert_eq!(lf.level, 36); + assert_eq!(lf.sharpness, 0); + + assert!(lf.delta_enabled); + assert!(!lf.delta_update); + + assert_eq!(lf.update_ref_delta, [true, false, true, true]); + assert_eq!(lf.ref_deltas, [1, 0, -1, -1]); + + assert_eq!(lf.update_mode_delta, [false, false]); + + let q = &h.quant; + + assert_eq!(q.base_q_idx, 216); + assert_eq!(q.delta_q_y_dc, 0); + assert_eq!(q.delta_q_uv_dc, 0); + assert_eq!(q.delta_q_uv_ac, 0); + + let s = &h.seg; + + assert!(!s.enabled); + assert!(!s.update_map); + assert_eq!(s.tree_probs, [0, 0, 0, 0, 0, 0, 0]); + assert_eq!(s.pred_probs, [0, 0, 0]); + assert!(!s.temporal_update); + assert!(!s.update_data); + assert!(!s.abs_or_delta_update); + assert_eq!(s.feature_enabled, [[false; SEG_LVL_MAX]; MAX_SEGMENTS]); + assert_eq!(s.feature_data, [[0; SEG_LVL_MAX]; MAX_SEGMENTS]); + + assert_eq!(h.tile_cols_log2, 0); + assert_eq!(h.tile_rows_log2, 0); + assert_eq!(h.header_size_in_bytes, 9); + + assert!(!h.lossless); + } + } + } +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/README.md b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/README.md new file mode 100644 index 00000000..4302dc38 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/README.md @@ -0,0 +1,41 @@ +# VP9 Test Data + +This document lists the test data used by the VP9 decoder. + +Unless otherwise noted, the CRCs were computed using GStreamer's VA-API decoder in +`gst-plugins-bad`. + +## test-25fps.vp9 + +Same as Chromium's `test-25fps.vp9`. + +## vp90_2_10_show_existing_frame2_vp9 + +Test taken from `libvpx` official test suite. + +## vp90_2_10_show_existing_frame_vp9 + +Test taken from `libvpx` official test suite. + +## resolution_change_500frames_vp9 + +Same as Chromium's `test_resolution_change_500frames_vp9`. + +More information can be gathered from the Chromium documentation: + +``` +Dumped compressed stream of videos on +[http://crosvideo.appspot.com](http://crosvideo.appspot.com) manually +changing resolutions at random. Those contain 144p, 240p, 360p, 480p, 720p, and +1080p frames. Those frame sizes can be found by + +ffprobe -show_frames resolution_change_500frames.vp9 +``` + +## vp9-superframe.bin + +Raw dump of a VP9 superframe. Extracted from GStreamer. Available at + +``` +gst-plugins-bad/tests/check/libs/vp9parser.c +``` diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/gen_crcs.sh b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/gen_crcs.sh new file mode 100755 index 00000000..b63df007 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/gen_crcs.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Generates the CRCs for all .vp9 and .ivf files in the current directory using ffmpeg. + +for f in `ls *.vp9 *.ivf`; do + ffmpeg -i $f -pix_fmt nv12 -f framehash -hash crc32 - |grep -v '^#' |awk '{print $6}' >$f.crc + ffmpeg -i $f -pix_fmt nv12 -f framehash -hash md5 - |grep -v '^#' |awk '{print $6}' >$f.md5 +done diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/resolution_change_500frames-vp9.ivf b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/resolution_change_500frames-vp9.ivf new file mode 100644 index 00000000..e6537f64 Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/resolution_change_500frames-vp9.ivf differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/resolution_change_500frames-vp9.ivf.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/resolution_change_500frames-vp9.ivf.crc new file mode 100644 index 00000000..0ced9e17 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/resolution_change_500frames-vp9.ivf.crc @@ -0,0 +1,500 @@ +30434565 +3f479427 +1a3815b4 +003efaf8 +a326e30b +5e48c696 +4a95cdff +d094e6b9 +f88fb600 +3166c8fe +0631ba57 +0f994c4b +fcfd01d4 +0d6085ca +83f02696 +9a6281b1 +e477c455 +83fb3e76 +0dab98b1 +2704ae4a +4c735dae +665987f7 +fd3a4848 +89ab7ede +99abfab3 +c602e1de +22b58401 +3cf8420e +5a33eecf +847e256e +acdcec41 +ae5c98ac +cb7b8f57 +5f58e096 +8f5cf9eb +c2bb56a9 +72708cd9 +d60f26e0 +ac56d0e5 +56fae066 +c8f77144 +957ea038 +75a28a96 +602ba7b8 +8dfa67dc +145d0c16 +07d84b15 +d5ff745b +89b9c4fd +476ddf3b +73e3833f +eb946288 +39916706 +a62f2ffa +b62fce6f +cec06605 +d139ea59 +5250ceb5 +b75ccc92 +c79f5e51 +8bf78f86 +2f55a022 +0b4d2c33 +ee21ac93 +e5880dfb +98ebd835 +d2bd50b7 +e63409b9 +c7f17438 +b4671d62 +6e012ab2 +1d71249b +1cc7e30c +8ad0ebf3 +b3bf9734 +8f45ae14 +185495d4 +edcd3b82 +b7b7bc38 +e62517e2 +3e5767dd +a0b59888 +1517dc9b +213303bb +1c0e1d69 +48849645 +c41fe238 +3423ddb4 +e8e9bcb9 +aca22302 +85142fc3 +034b1fa8 +4fecedcb +6317d541 +b098f009 +7d13c5cf +b6b6127a +1ee289ad +3a3f70bc +a6b8e785 +185c7dea +08025f9e +a6647fdc +1916ab72 +d2906114 +2c833567 +86374614 +7dc4ad6c +aa9e4b12 +9a7e5485 +4e89084c +b0e7e971 +1c7d3e11 +6f1f48ef +d2eb406d +4952e5ce +8ad0dee8 +5581a685 +f430f28c +18adef19 +f81c45a5 +66ecef00 +6b8023cf +58d96699 +5db8ddfc +2f8d6d4c +150426dc +9d14824a +69879063 +465d1e5a +f4d03906 +3bbb4545 +74e922a6 +be20217a +dca8cce3 +7d1e9b5b +edcc5564 +5d6611d8 +0698b853 +a62b3ecb +b34c5938 +e26c121e +39cc64a8 +cc34cef7 +81cf2401 +d575fff6 +1a7f0bdc +f0c6d006 +5b106cfe +9f2888e0 +6f2a63ce +c1155ec4 +12da69bf +c1dbed84 +671c4163 +5dad13b0 +d2017cee +496bb413 +30434565 +3f479427 +1a3815b4 +003efaf8 +a326e30b +5e48c696 +4a95cdff +d094e6b9 +f88fb600 +3166c8fe +0631ba57 +0f994c4b +fcfd01d4 +0d6085ca +83f02696 +9a6281b1 +e477c455 +83fb3e76 +0dab98b1 +2704ae4a +4c735dae +665987f7 +fd3a4848 +89ab7ede +99abfab3 +c602e1de +22b58401 +3cf8420e +5a33eecf +847e256e +acdcec41 +ae5c98ac +cb7b8f57 +5f58e096 +8f5cf9eb +c2bb56a9 +72708cd9 +d60f26e0 +ac56d0e5 +56fae066 +c8f77144 +957ea038 +75a28a96 +602ba7b8 +8dfa67dc +145d0c16 +07d84b15 +d5ff745b +89b9c4fd +476ddf3b +7aebabfc +5098261a +6be66915 +2c0dac16 +46e4498a +4bb011a9 +1db45608 +51d4b563 +0c70020d +5fe5a5d3 +9fd0b920 +db24b6fd +2900f825 +ae09ecfa +7c220368 +58cb372f +b4da90fe +c62f659c +b6b990bf +665a5990 +aad5e55c +aff234d3 +c6850001 +366cb3ab +b95a04db +f34882e5 +93d19d35 +37834afb +998b8b4b +ab26528d +4f39e702 +5bd3b3fd +ecb5a096 +d434b006 +d44a715e +37f1c963 +367b4f79 +154ef078 +8a909d4e +7cd512da +2bb88121 +e2bfd733 +dba85108 +eb49fd76 +e2c9f19f +25a38992 +742619f3 +e9a279b4 +90c4de29 +4ff923d4 +e4b56000 +1a6b4200 +839fedd8 +87650ead +0345d74c +691fa48c +cedb2e03 +0b3e2577 +5f5b60b5 +95b19c13 +633d49e3 +2da01d07 +a114b659 +8080d857 +f52eab8e +b5348da2 +12453318 +a6589d2b +189c908d +eb83c448 +4e09feba +506361d6 +72de5e28 +49644af2 +ec508bb2 +efcd9e22 +947abf26 +47d491a9 +f3819b93 +19bf5692 +a1a6ff76 +ed33d700 +c4def3ad +ef10c624 +fdabd9d9 +6ad10a8e +d719e93a +593c7aae +1e3ccd7a +bf625826 +3f174d5c +222db35e +0884b2e8 +f3b5be46 +07794925 +21c9233f +35041427 +10d883a8 +765d6db0 +54dfccc9 +4801da6f +06c7e866 +69208a52 +3381d557 +c3f91b72 +1dece653 +0828a727 +6b808625 +4db6f222 +64982c8e +5782aad6 +dc1ab775 +51ab86b2 +c9f02103 +4ebe8270 +85d90d4f +62194ea7 +9c4d98db +15e04acf +955f3ec1 +6a9205c9 +6a915ccc +013d43c2 +4a7e4926 +bce87d78 +8db1db9b +f0e7eba1 +cec75219 +1cb9aa20 +37163dab +b619ad9a +c3348021 +ac44a4df +5ef80c45 +cf3238b6 +db148947 +cfebcfeb +68c2ba8d +97bd3333 +38b9a39e +88239b6e +21dada0c +3a3240ee +309c773d +782b8c8c +a18fa947 +d96adf14 +d130b2bf +13ca5914 +5d0252f8 +94482817 +f859fd3b +454ca588 +bacf1f63 +9685ff99 +12bfbea9 +758d0396 +d1dc7f1a +f332a5dd +95ed5526 +320373cf +66ae44d0 +d7f60521 +174e8778 +d8f1cb9a +505edb44 +e218879d +8ee171c8 +7f0d1f7d +00a0f4a2 +b77fcb6e +9d9e118b +3ef2a6ba +bc3ec3cb +ab1f4821 +dcb9c237 +aedf52c5 +5191f352 +e58687bd +74584d91 +42310f91 +c1276911 +c81f69ec +ffe3479f +0dcd9a22 +00c1d7cf +6308e6dc +0a7a9866 +032125a4 +68f91bf4 +e488c252 +a7534dcf +24d336a9 +98a8bbb9 +caad1bf4 +64f2157e +52a304ac +f15eb12c +c6dbedca +da11a1f8 +be321a1f +ae891efa +0ec7ace0 +ce45b0d5 +77137631 +5a0b7286 +19498745 +e1ba1f6a +9f76aa6c +c409dd1b +92536c65 +bb9ef77a +2240c34a +4b698459 +52bf0ddf +2b357e41 +b821dd12 +9c428cc4 +f58cb68b +3f1adb2c +ebbfd9c5 +f47f62d8 +7305e1be +79ef7a93 +a83ac98c +ee9fdddc +b2a237fb +22dab010 +fc03b4c2 +90cd6a71 +7cfac9af +50472042 +8d46b423 +34ca61f2 +01cc9d54 +b191f0a7 +64330c48 +25c84ef5 +e3d608b4 +1faf320a +dc42008e +a4478667 +54cc09fd +cbf1e2b5 +82c03181 +76a575cb +c88fe2a9 +92f328a2 +d792a66b +ae89fbd0 +742429d3 +1045723d +084976fc +ea999295 +2c79adc2 +ca4fb5c6 +0ae9e41d +e3d52ad3 +d453b8fc +eb24dbb1 +1220c85f +5b631b96 +eaab2c2a +c10c82b3 +01cf2040 +381ac249 +431bc361 +57df9be3 +2010ccd8 +6e5d770b +7934dea1 +37a6d295 +69b537b1 +1c5b6117 +5aef90c9 +22c21587 +f45a95f0 +46ff5fdd +a2c00527 +bf2c3a89 +ca3def6f +40e4600e +12255f84 +6439fa9e +89f0b37d +13f60bfb +6ede3385 +e2eb309f +c1dd01a5 +cecc9d99 +d1845fd9 +dd87d2b0 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/resolution_change_500frames-vp9.ivf.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/resolution_change_500frames-vp9.ivf.md5 new file mode 100644 index 00000000..5159a03c --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/resolution_change_500frames-vp9.ivf.md5 @@ -0,0 +1,2 @@ +d5a38844026bf51029f97b815fb04b19 +401c6bf17f34a1185d5381009e58ba65 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9 new file mode 100644 index 00000000..385d1b4a Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9 differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9.crc new file mode 100644 index 00000000..83a05e0c --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9.crc @@ -0,0 +1,250 @@ +1abf0870 +32ab9360 +fd2a8755 +795c3488 +05605011 +c836c6f6 +9811bd68 +2ea553c0 +2cc69486 +9784cc5d +dc95c93c +9abb41bc +fbc1180f +a4f5e378 +0ba301f7 +49af5c91 +1a33dc74 +c3841cc4 +3d22678d +9ba82c05 +acd3f53e +5e147078 +3710613c +55c0edc8 +c4a734bc +ee06bb4f +57ffbd97 +4e3b52e4 +9848ca34 +a5f6a263 +7bf13fc1 +d7cf6fcb +a49569c9 +5b9b87a3 +e1732f37 +fec42824 +1e8514df +7580ba76 +e07ae78f +065a11ed +e254d9f5 +67ca9179 +b74ad987 +8f0151b8 +c9d5220f +34788052 +c8269a62 +98108c54 +43db9efa +1d6e8a25 +790b24cf +93badd6b +43276faa +ba4561aa +78d7e0cd +aa11db2f +98c23282 +0d27f401 +4450596e +a511b3fd +45aaf3f4 +8b6afa2e +798ddc36 +c16d2441 +90096480 +32d5f72e +ba19403e +e2d0ad1b +f5306855 +184309d4 +f1134993 +49a1ae44 +e724a782 +76ecef33 +7b48a1c8 +80e678a4 +14217558 +21d6bf24 +907ca9e4 +71bdf4b9 +be9b2301 +bf953977 +a5c17a0a +b7c27ba3 +dd13bf91 +600d03eb +fd2d8874 +a2d8af0e +4c21a3db +48353f6e +e3e6b60c +ef3b9c28 +1a4df0bd +af18c549 +4e2da67b +77a51623 +86b999df +f5b45a41 +b0d5b913 +46b5ed49 +f64a266d +ff157d94 +67b1e678 +705a8536 +0a7f2695 +02c328d4 +ddbee2a6 +259fe83b +9164f5e4 +f7b740e2 +07026fdc +8887118e +ee734407 +7544e6a0 +d1cd9b46 +ed639972 +1c791217 +52ca157e +b3ac782e +3592f784 +ec3a0a53 +ba29434f +0c9dafcb +4659b0c9 +c645af06 +5b9c4202 +5a89bde5 +c7e3498e +c202031f +51b8e7d9 +bec84a33 +163fe7a8 +4e05e423 +d1e3f398 +dce754c5 +795ccbdb +5d1e3e29 +737f110a +20d97337 +f112e2db +cf7148ee +c305f875 +f18e07aa +bfdf8135 +6a216daf +8fbe0aaf +639343a0 +0e2152b1 +695c3d1f +22065e7b +98acf61d +7a3fcb35 +dcf38665 +b7a69707 +03066f3d +1ed4cf65 +193be090 +6f3ada5e +018ce957 +0eddb953 +9348cf1f +6e6d4172 +28ec3fe9 +529bfd00 +084d2315 +f3556816 +cab6043d +0262486d +7d3c4498 +61a96b90 +1cfe545a +e92f9e8a +57d3eca4 +45051fcc +c73e5a15 +6bf04540 +3e5cbfe6 +e9315867 +c59ffd57 +a7eae6b6 +ee526a3e +978aebb2 +adcf6a3c +979a7dc1 +fc3f7b6e +2e716a71 +878e3d42 +3144b8d5 +84224d07 +408a0c1d +af9a7070 +c86e81e7 +7e04a9fa +b190255a +5ce924bd +8799f259 +29e5e7da +be77bd1c +8e001635 +75f47834 +fdde4db9 +264b2528 +5586fe04 +c2a06cc1 +4a3fb4a2 +45f4f1fc +0fee683a +987dea92 +83b24331 +b4acfdcc +03679bf5 +8f9ae6f4 +5cef71dd +947f45b6 +2aac1e37 +028426d1 +2dad1dde +14a9c696 +c59d9bac +54de7644 +094a7b74 +ae7e70be +43d26c10 +6232d8c9 +cc65bf7d +59ee8a5a +807addde +e7d82793 +7fe92022 +4283f764 +d9a91c0c +a77f54b3 +02aea8de +b44fb9ce +1a116375 +46304da0 +247c2899 +a9d9cb2f +88bcfc60 +b2ba9844 +333d1188 +4c2f12ac +a4d54033 +be8e652e +1a37abc4 +2ca65b02 +95d7a371 +55840fec +ed610cf4 +c0809111 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9.json b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9.json new file mode 100644 index 00000000..6a11099b --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9.json @@ -0,0 +1,260 @@ +{ + "profile": "VP9PROFILE_PROFILE0", + "width": 320, + "height": 240, + "frame_rate": 25, + "num_frames": 250, + "num_fragments": 250, + "md5_checksums": [ + "a6ddd21f5f4e7424b6e7a1f2925fb33b", + "41c77adcfd29abfaad62a057855adeaa", + "afdb44531614034e4a4a90c805a5d3b1", + "1fb247507d6e076feefb7281846d4938", + "597957a1bb001769a675d5be58db3271", + "c023f0ad8c3051e536ac9c9bb1d5eab2", + "64fd982393c290d092e1cda39cd429c7", + "915e863eaba1a7957f554b5cce006e1e", + "ace4ad6c26023dbf12f4c5e897e996f8", + "166ba862f99f7e0145f0b3a8a46c53a4", + "bf401e2e9084c80cb3c22d2d05dcc627", + "94dfc278df94cc355b6019096133befd", + "c88a98cb020b5cb9372b528ea876e3e3", + "7c05949db143606204b3ede34fbc0472", + "ebb8798f465fbe8083bd0255a41e17da", + "59e6ad68fb6e7c78383426c2dcb768df", + "57f2f71ab176f78b815182c107649b56", + "0713b3db71e138b41e1c64d5f76292e2", + "913558ea3033285c2ef414c29cd9e36d", + "f66bcbf90aad3f2973d7210ba221c5ac", + "02371beb715f84b4cc366ac78c9597ed", + "332355fc07a4eadc59ff5b5f0514347a", + "5bc08aa98cb182e58bab48967c26c938", + "cf1a6f1bc177d45bbbd06a21417973ef", + "c69f55a8cce9b1af971f96989c2ed50e", + "51b067bd3faf4b624e265dbf3bba6e43", + "9323f02a491fa931645c5add54109890", + "2fe699e08540e1a211e259909248f91e", + "e4bb4daddbdbc8c9c1b7398adfd2664c", + "9356a0ac6701f2231c6a12d6d3eccfed", + "597e71f32915badf51be8ffc255aab74", + "08a0dcd28bcaa495e95d15d2645d9c03", + "48742951820e0b22038c17f715f55027", + "df8b6480a30359d33f0fc5ff5f22cbb2", + "25d9f7a3dfc3bd6f7e581af20daa749f", + "fcb79ce3d922d1478e34399f29fd69a4", + "6de189116760547566fa6e3885a9b2d5", + "29da43f8a80e83a54db3c83016578bf5", + "b327993a3a9782630e8f38830b99f1eb", + "6a25c1b232dff00e78565f2886bfa728", + "fad26a4f6912ca0527c5cea767eca873", + "9d05717db2bdb502179161b9be1d4604", + "47f91eb36e06b5c7fff2c538f3026c39", + "a5611dd903d184a7ad3b3c5a02027694", + "9b977c83aafc80e375655d81406fb3d1", + "6eb2923e12b36b91b1d38a50c0a477d7", + "14acfd2a71f163186da78c84b34ce0b9", + "9f5395258aaff37e8c87b6c07df978b5", + "d5a22ec4e0ef752d3877afa7ca1db26c", + "005415db7bb57768a6ba92d4a43afab1", + "40cb57177361e07b523c95a206f20be8", + "e645c544fe2c8426bbe9ff1b24c6be8b", + "94a976505f27a90649f553dc7a193fe9", + "3b435d0e9e6af6ecf3676cb7299ef6d3", + "c169aebf19b7c6f3717ee0e097036507", + "c970f90040212ca7a392ca5cb7c8708e", + "243932bc414d8de7fb3c1bdfb4fcfa91", + "3ff138cd7f150eebf63a2f16a0ddc3f7", + "03ad4bcd723e5f2a3e96de33effac6cb", + "5416a2155944c3c23167d5a00e8e12b2", + "0620389d338d9fea5c6867f261f35452", + "377b5550cccafcd5e3142da159791add", + "68b1733c716380a04d0dbf21fc1f30d4", + "c9b872ed955bb13dc0819661f2b96692", + "fcc79ce029edff15bb7c2c5130d421a7", + "933967daa7eb201accf440799113547e", + "b64df022a685c95ca39c6c860e1f5ab9", + "8cb58810932a51ab29b10c691dcd50f8", + "5dfd36a6f1d476780724792b7a33b45b", + "25580f9904ff6397ddbf57dd93623aaa", + "77d61f46a7421182f43385fb7de3ad28", + "1e7adc2c2f99538d5c629cf906e82a65", + "60336a4c5b6dffcac7f3d42cd8c8d8cf", + "7ed47daa771ef93fc2eb05cf123184ed", + "bc03e5d85743ecb2b9f408c4814ccc03", + "5096b7c2eee1d8d7bf7fc825db35fc6b", + "38c7db4fb6532a9c827a6d2c0ca15640", + "93188336537ca3b189075b83afcb4304", + "4b0a2f9d16710b28e1b5b4f2a6757101", + "c14cfb8c07018c8926d849d0d6910d1c", + "d0d67b916206b75134f4b254d41a5747", + "2390a0cfa71bced2b9fb3637cff30921", + "3ab325d11b4014ef6f70734127fd4d31", + "9964596397e69118de09fb1e44fe01ef", + "89522f41b2b45984a1a27d54c59b41c0", + "b3270c89e1278984d133f4c2b7fc0a70", + "fdd6bb9f2b4b89584294221de8291107", + "79e5d6ec50f8d136e8a01bd0f150416a", + "1066212964911081dd41a8c184716589", + "5aff5ab98073ca06b2cd02b044ed2bac", + "52fa9f744f083400fa1013a9a296783a", + "ed586b59b27f1f3147d0c33cf94618e9", + "ebe31226ad166d53db606dfd46f65c4e", + "9208a53c77d7a69273cbbd386bdaf38b", + "5cbc9121a9decf62cdf538fc3b6ac6ff", + "de33f01b71d0b84bd6641864e5e03c1d", + "68459b19450133bebfe87e8658840d84", + "f93988c89aff87855e8a46a388231ea0", + "02a92e92e273e30d11c65a9c17afbdc0", + "194ee974b89f3c7ed85efc0b3067bcf8", + "111c441b0ce29b52370d79a7c040a319", + "c5c668728b0a0def951aabb873747c64", + "9fc4ce859ad1060df8583f11650e5e69", + "ac7ea5ac33d992834e18d5a80f7865bb", + "ede582c26d225a3cb903504a2817685c", + "4d491df588bdebd988d8d89e1f3aeae0", + "3d24041ab9920d06219f02b762d1ea92", + "5bd75a66e942a73ae09bb24936335869", + "5d335e05b9b578f104306a3577acbfdf", + "866816a72e249b6cf0f9f6e33b65ac78", + "afaaa9d734c02fedb83ebf6524e9bab6", + "d7d1b2774ef4ddcb191fc9fe6bf28c29", + "482908be24d0988e8e6b40df91c1bb1c", + "1752814ee8e6d3097cca487d503edb0a", + "128c52fa88a9e3df2b928519812fd3f2", + "cb41c09b32c25ad921c87538b62f70f6", + "a1be95f5d67cf17a1540557605398b5e", + "28fcc49f81a46bfb2fa1e39b7ddbef2c", + "1e6fe0400d792d432c03a6fbb7a346da", + "fd357048fef2312acd9bd1a84f08dd0d", + "2f4925cb7f740454ca31ecad6da072fa", + "721c71f2297e2cde48d23e2d14209b70", + "eb39d3512e79c54299a2f2360d001523", + "d3b3c4661b95a8102a091dadb3f61a8f", + "abed47e70a4fcea8b5905e27bc4d91d2", + "33144b402f5b60a0d7109727d678367f", + "347710490553e286bb41635a87d51440", + "e5d90bfd73c660c136f18708cf691902", + "2ccb92441cacb6fdc449092df990f7c1", + "cd457fae93314e50d5c2b7f1f3cec91a", + "d00e8d32b7b8e03211ec61f8329944c0", + "7481f50fca5fb8758a12932b40eea3f8", + "24cda7a1abbbd3f90286e529ad0ee446", + "fe49c757004f028382165c8ea24208e7", + "d903372b98ebe31896a591a1a6cabe0e", + "28f937e819685a1b5e73cd404e998fb0", + "5bd6d6f0037891cad42864acc85b9824", + "c5c6a4e219aebda78e3e9d2c91a48564", + "0a367f3982856c5bd984c2f866b255db", + "8d62ebc501823bce65b840cfedf2e75d", + "49b0c80de766ff81ab183cd611d1d118", + "da984ebe368315c820f2a1caaafd2534", + "710b15893183f93f2497c2e98ba56e0b", + "3ec5a99fcf6f43eedff8dd9995aa4704", + "67b9d9e89fb300ae3ebb03b396fee273", + "6194ca3c338f64f144326486d118e793", + "e85a130d9149590919e72a2f8ecc5f1b", + "62fbf66d14bc819353086e442c581616", + "bbc6b58d27c1623bc87dd81793dd72a9", + "5804683f0f89bdb8e43508e93ed2d17a", + "72fb2bd71150381878acc658ea547020", + "351e4493573d81f4b8cdbf4a94fa2dd5", + "573f6ebdef825cc73a6aefa219518add", + "f671a8002c56f70623b1d36f6d1ccce9", + "bc8f0deb204095ec70effde543c4f087", + "a0e79d0c3e9c3734c90471e17cfaa402", + "1e85a120057917174101240f66fbe12c", + "bde29019b9d44035c78ae593d882c0df", + "f1036a0a0190b198558aeae5c8539100", + "7d50b367eeac4a7ea9d0f797f3f286b8", + "133a4ed3fb42fe986117df1fe34f07d9", + "1e899dad271bb94f8d768c2025d527d3", + "258bcce7005861b2a6c2e547c81d66a3", + "30723accdb0788aea00ff42610c0dd99", + "a60ab6066b2b8258194cb10d8e10c206", + "321b86641c6e978e98b36ac367982397", + "be2b37e04fde984631e06ee04c8749f8", + "af1de1b1e1b4104aa99b563527828c68", + "c10f648540156e7379ed764ad424e233", + "716f1d5d894f0caeb6e060c365b1c68e", + "4da20c2960132d909767cb328a2c70d7", + "a5d0eb676eabcb9b808c1b7c6b312c3e", + "c06cabd771e2c42a9e2c3ca7738525a0", + "e87e67d2e0ffbd9762a4e99ddeba53be", + "3eb39b20537cd8cd12f15ccd6848f672", + "f3212f83868cbf07b06774c0283b25f2", + "746d5d5355f10892237e9d0dab554485", + "1956d40b46bf714d8bec22d784c54611", + "fba00831cfc6f6d948917c0e02a22ca0", + "c744b4f54702e34f47c0f4c4f40ea615", + "7c7adfc5fa5d03b5b7e07cd6e8293b4a", + "5825b5ba1960c3b1a88aa1e10e5f0474", + "d90b42024fda5312a0d886cdc2ce20f8", + "0592eee5f90d9dbd2ac17b03ccc0ed2a", + "2ff9315ce6dd9f4c6ec501c481dceba8", + "0a3ed552d91de9a403120f2e118cfad0", + "ee32ab17ec770aba340cb68e181275d0", + "3a36fa6fb7140d2354ba1ff2f1d287ec", + "77e8b7f41eb1cd46c389c9c3fecfed44", + "7f8f710b7bbd5a033300d0e4fb47f71d", + "28eefbb77d26f698bb658ed7f58cb17c", + "811d2afc4e6f5e8efcfc03563d1374d2", + "53d51c878f1aa62fb2753d67f2decc02", + "1a20e2c1da568ac283173c60807d7bb7", + "f77541cb2afd1633cb4e5294e99de8a2", + "e859d77a2cc87972cc7df32e6c625f00", + "18d8d2ab6cf205ce70316ce7a3e7e3b8", + "fa2e35da2ad12625ed5f50be46bdd61c", + "463169e7371dc37f3d082a5f166b904e", + "5e7f7593bf77b346cbf9906741623d94", + "60089de0fd61bf8547cff25da75d79db", + "609dff1ad553656a98bd6ea178f093c1", + "34ba858360b9943770e3b2c9594dbbdd", + "02dbd49e186b241f42c59f9f77d74e13", + "0632f6c62ee2b72e3dcd3ebbaf3f8e59", + "0cf0fe04e5bda8159176da2150ae56dc", + "ea48f3c5ee4fda7b7c44ad0f9aed7ce5", + "25a1a468110f7819dfa688bf08bb08c8", + "702f29070dae81d71fd4b6ec16766967", + "bb8e273c432a96b4c8fa92fde5210a54", + "702cccf5fe0f912c4a369e0c1b0d1d6c", + "bbdef9ec58db645eef1b52129e914dcb", + "3816969dc45df4e2d39db44f460bc0cc", + "db25e3bb20d5a30bf9170897e3075732", + "43875d9f264821d179f0d861cd5c82e6", + "467945c3124b2e4a4d65ce6a55e4f889", + "47ad1273bfc4b8575515498fb3e68570", + "8cf9200d7ce629344fad9e7772c5f099", + "a2d784b07f4f64ba41309138f408bf3b", + "aa7de9e0f6765a7b5a9dd6a37de1e474", + "4e82c66108b942323c95223d729731b0", + "4d7dc2ac3395345977678913400d671f", + "817546d45e1d74c5f5211002947ce94a", + "d4b41f15f106231c194db91bba1c9350", + "a61f9dd98999e8b90093817f280046f7", + "3a6943d566df3f8d9aa342554f2a30f6", + "1738e3cb6680ce1b09c9dd25b7b51d16", + "846b40c6d1023862b5ab86c558cc75e6", + "8a6156e1d440d1694e9df3e7c0b0a2dc", + "f6be779252c9d6d9409c94a899f23090", + "11086af958413f0ae757571535d7ecc8", + "701254468010c99da0d1f98a39099bed", + "2624e584871b8fed98168aaa31c7d264", + "8fec6cc28ab6d0072a9ded2861e13f96", + "700b06457ff7f99b47e5412c2fec7324", + "b34f8ff874d0c9e4730ad94cde30722e", + "814a5d8724ac31d3af0d1076c4d3c2e1", + "342befc9829aa640b36de1085e3849c7", + "e7879857be414f0215a5716dafcce729", + "874ea752b65d583b44615f23ec00e3de", + "8b555f7686aec7c61df83504275b0491", + "7a1763fab40e8e049c8320e603be4bab", + "e50ef5703efbd0b38266057a05b4e56f", + "014bb33e138fb10157a062cd4906a032", + "94224a6b01c4b088d429a78421b777d7", + "8bb86793ac81fc54f3a6c9ff354ff9f4", + "6b90162e6ba9308ac891770528a0b2a1", + "5a63e607041536fa3f633c769f0a9e17", + "3b33f8e78890e8a41d8cd93ebee7078e", + "fbb59915d51c7f56386777e70ba2dddc" + ] +} diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9.md5 new file mode 100644 index 00000000..a68323ae --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9.md5 @@ -0,0 +1,250 @@ +c147a6c80d9209e5a1e992d4e610a95e +807f9c05d06ccfb7f6a77018f52c07d0 +df198689de44a69924d8cfe791e5dc84 +856efa94272c5b0fabec7e1179563806 +b5586069cc2a58a38d0271136949c760 +5a0a2be53ff9bc19f131469bee430262 +40374e2eabce3cb67b9f0d9992f31edc +2c0881752530fa3fec18f3aa3277f938 +c86341015022064f0070cc8b70f2edbf +ad05d275a93b63de6327436b48cf9e80 +4ed78beec93f00af0d6cf78cac200494 +9273549b482b591a9da3543c490d4847 +59b84f1b3e971e27212a8f690d8920c0 +07b78dec9c8070b40e87fdaa18b4627f +ee7e9db173349fe2ac2c199831bd5eed +ccaabbed17e58a6f77da8a5b00f9f24b +436aada1f8eeef8b11bfb454310461a3 +2bb8c17d108b4b5b9594461efe48ffe8 +04e89161b5ddd1cee2850f19c1ea9af7 +bf527e3dca6c046d5b0aba1cdcd95f28 +564fce13d1ddbaa1d587bae33130e0f1 +e7f84fd6b22935634144d85818ed0ce0 +389109c513f164483e1cdf67d1d17e20 +888e96338562ea5ac11882ff897bc45e +0734d839755f4b660e282fbdfd303da4 +86ba362ca374dea486493877bc83830b +1549542e46f6c9a412779461b3b2e1c1 +b90ec322166c72e41e9605f7a0c00e34 +2e293965ced430e72dc44daac571ae96 +8952e34cad20b1ad4460b0510c255caa +d6cfbb47f87f2c6d27bdec96fd86bc80 +57fa0881469337f201b01d025c255070 +e3c562edb81b8ed8bdda8ead100b47f7 +60faf70b449db2ce46010a71d10e9a37 +2d5fd1fa5fd07021be4be51e279496d7 +8094a24162877f21e8e2c876631d911c +cc675882c3a3998f19875e291c1f158a +4a049974b54cdf78f45c775bbe21cfc5 +6f78137fee79f8882b8bee2b5830aebc +25e75b7a102c32988bb676743d46da7f +bae608bf6d8b156669cc884ab37e6d7d +883651afc0bdaebea1a7906f51b1f07a +71047b3c17d1aeb3eb3cdad867a6cc1c +fa5b4d415cca2d417e1ee5fd2177eb6f +fe4dccf6135bb46db063b1b1b09a1b1b +4f111590afad4f98c70ef2f7ae2ab302 +c39c4467809fdf8b396f2f4643c72a39 +5eb42d6e49b71cc75748052104cdaee1 +07398ff06268387fd10a956b12dff28e +5e568ba441a984e117d5a9362267e4f1 +6bcbfebe110cc1014a325b0be3ebac52 +3ad42d2755b6e9fdbd2d92abd212d636 +a2a368e017d5cb835d0953a00a6bf2cd +c3955a9d37d474036e1457d486093aab +584fcc9af0395b6e6c3f28d64473bcc2 +511a81a66dfd9c83b15c561a81fa45d6 +62364a344fd1f9706c011a197c6b0cf2 +3f1c26938b57f0973fb73c064dfa9bf3 +dab959b019d40ca4114893d774945fa8 +1c33008422a5351bc61423912fabc669 +f20ec6445ed4d41421fb6f3fde5bbaca +ca43b98fd0e7c94c60b403f2acc2e948 +a5efd2ae15724d229993842a9c919011 +da547900d3e9eb01bf4765e2354fe88a +7259dc31f78637c9a70ef43906fa5807 +be346c4bb4516f67883802c200179e06 +8c8f6991b9124c0aa7dbdfad68bd2c9c +6049aa8416cf2a3b7df3cddd86b44e64 +363eaac4e65bad5556a3e175d643123a +b6822e13f6db695c822afd73e0b989ef +61c375d0f9dfedc67304e920c08301f1 +7a46f29451ca727ef7361420cffeabdb +80edef5ace96379c40db611885141c34 +1697174088d86b17d6b372c20aecadea +148fca7be383caf8adf8433a25e4d4cf +c2eac8e9ca853bea1e76acbcd774e2a9 +c8310d2b03328f17f9ea602df338d279 +fe641d1b7be37c948e30bde65f609fff +00c13ad8d2766ab091d96960cf6609e1 +44d4d30bdcffdab69e16d4f07aa66c9c +62fbf74f8cdca21c0c7814ef3c515a4c +9762ed61bb1c78c1dc84bd67d05c8016 +31a4ac35433525329bafd764405058ce +6bf0c3b23613d7fbcb04e8b715eb96e6 +a20bde62736096ad152544f195afc41b +0b1f0a21f64623b268ee55f8ecb0dc27 +8d77996095e9a258d3505bbf17fda0b4 +49eb262a315de652dadb7a22c1e8d601 +d466dd053e522f653be20c2d81540c03 +6e1afad04dd7f9f72a59c5a173686ab8 +9726e527b395492dcb6ff72a0d5a4796 +f272035189417682ab283889a9dc1fac +2bbd94a07488d59f8147388f0c394a40 +f7d750c1a7a88d1684f42cb535430280 +1ba751a04d93c7bcdfce9391001997e3 +68cd83e38eb422ced19838e7167765c4 +f003f95a69519492fbf6b08969a0c261 +5701ccb9205417896a193a800d32c38f +b712418e2276267553b1056782a7e96f +1be965ee1da3c8dc4e1e5bd01f850ef5 +02da1c68b982cd7a30e104320d04b36b +61e0a8d7df2d01909aa463701a3bab30 +dcad9c4c4bb4855c2fc31c47f5a0c12c +c0de7fba39fcee42577dde05f4d8b0af +6052cb7ece90c1c64861fff7421a9d69 +ef669757e94008168aff1646b5c04c0c +e03f821f36ca1b59c2054a5aa72cb82e +a32fee7e3e39f3e69f69c940a9782493 +dfbe0be3fce78c0c6bea90058b5321c3 +669b2200b96bbe5049a37ef88f86b1a2 +d95206a7ecad0f0d154d59b8b81c398f +f1939cbbc7f9803b03551e0ee8e976e4 +54a25909ecfc627c161ac9bd5dde0dd1 +be6a415aec10a459061cab5f856c1dae +398d56c536ac7b051fb8a96d7c12e147 +06b0b34464aacfb4221d4a1d17781a70 +d25f64611b9d7bebb6124c2f0c39250c +94c2b637e08ee1ee8c79eb662a199450 +7febb4a4f4b711ea70e86e7054b41277 +04b884c1af9ac9806495c65de8dcb748 +00dd0c5d65933dfa721a61e3683c54a1 +ab6974a3b1aa3a510e84ffa04b4b3f89 +297d608c848d13c71c42289326d169f2 +89c514f843b17cc07ec1c4c08feabee9 +8055e52cacff9cddfe49432d20d0bfd7 +ccefb98fa05dc5863b6deb905ff1c3ac +11406e832f5a98f031c717a30d4f3861 +f173396fc7e7bffd8d6df55551dcf51a +54d7421b70b4d182e3fb4dee07c70178 +2e43573aace01f9fdb6a6dd6537407f1 +eb6c3ca7906821794693205a27609dcd +fd686d26cde34b7d11527a9794813e3f +9b82ec0d251d5e294ccae92896b0bbb2 +509ffb563baa8b528e463dd0c1606229 +6344839db52a21e64cf93fb23a1cf8a9 +a0781ac759a9017b75a8e6ee58261adc +00cdccc8b20608e9fadba7cab2a52aa4 +d2f5a572735fb09ff0a2765366980985 +9ae66ea9466618e8d30f3dfaaf05cdc4 +8e18ba56741b48ce88c23baac7b60d83 +1530c8f53fe1edd5e290eb70969c4e0d +9fd6cf8b4992dd5a83617fa0540fdb49 +eeddde09d799b59f9676957095f7b545 +9acb5a05373f57a9dca7ba745bea066f +d85400d97728f0850a53de94eaaefa1f +88ea3f31c331b394a01f6f4ea44dbe73 +38ca4b29acc8922e012e274b2306b484 +4d3ac7cc0f7df2363eeafc1d3916904b +e7307e525f8a833de862cd1b953757be +ee6208f9f33ed8680e2bc04babad0843 +2e45bfc6975708fd3e7e0c4524755231 +3dddbaf8b60bfc79492749d5230ec23d +713eba2ea4aebf9d73c525971d6e9bd4 +396a002c7217fefd9c760f63ecfd0e44 +1ba18897ba27c7913dc78eca0b465a6c +9f80d56ce61776d19688b71fa20e66b6 +fe0fd66f26f13ab470e7696ed57440ab +7ac35e1a6f9de5a5247c55d9971ed02c +b45218ddbeaf8b1c1cc61836d49bf331 +647c6c98430ad0f6304d1f7ab54b0791 +a5b2c31a77130f10c81f9b1e8c0b36db +f85a9d6ab3fa171217d01c9eddd520fe +d294eb44a6972cd33c6c9ccd492ee4d9 +c776f43b7b879779221a2cb6fe363062 +dfbe59edcc434a5a2efe95aca98929fd +c5b7bc6f751ae1da4c4d9b596c208300 +aee6993c994fe114192cd440f2529ba2 +c16a35af9d237d8bb00bfd78d7910177 +5b17eb3d2946acc9936f54b092236e18 +35ab79392d55fe4383171693cc4f1b07 +65bb08bf3f1fd317baf8249bc39fccce +6e9db766a1563d160a646d3cee97e82e +dd4c49639c8c3d7bfb950efeec7af608 +32ba1749595850aa1f7e5430c168f1a9 +9aefbafbc6e058822f743c48cbeea6e6 +e73cbbb909c9a6e6e016b454313e00b1 +b5cdf19a48d33b8476b5b751a2f0398a +6ab09966690b0ea5c87269d4a0547597 +43ee16fe7b45c986fa99441b8c0a64d2 +1ef3e648b93a33b31084f030223dc2d3 +619b4e87d66c772be269e32443bfd768 +f30c876850197446f344597f63299fce +7c8a639e3f8175fd35516a1f03517d8e +b487f19e64626e5982f52f8de4238732 +4b41df592b2c341b9821c2c1b7385607 +f781fa740d9c3148343e1a133ee1235a +e4e7c51802d9156046e746804d2c3960 +3a63be1c6da417534b10a261604797ee +dce7b40962fa4ba57d0c2f3ffd5cd939 +dace0df358316264cdedac24fe984907 +6f6199ca0aeb0aad15eb613c37fd7db6 +cce509a1501aaa3739dadcd1351d83d9 +772877b6930a6f69b717a4d286f70259 +d8f658e3732b7ae095c89d2cfc29fe2c +a5e57567b758e1636bba4bb605598846 +7f0cc4258ce46fb86d310efa337d5dd9 +3c9586b001cc1e61d7df35c4f793f8f1 +94a0e6e24b394725a66b6c9c8ba3072b +4c01c47409d56a9ba57c421ce529b015 +f52cda4db0cd197d5a4577cc3e1d7ed9 +f2c479c08e12dc3db08cc803d0a2a74e +f5dfcc218ae104eba96007de1aceae2e +1a6e97ef60fd4dce42ce071d2e2eacbe +91e4d4e2361fcff17b342f4bccaab971 +9d47703a6842aaac5af6c5d030518e7e +eece44627dbea68991083644fc2381cd +51869793a192ffcf48cf21da662ecc9e +163be4c547f021ca8cb28a846eebf391 +344f9a7106be2be501b8e4058be63db1 +fa2fe8613470850bc170ca1f12b290cd +b8de6ae393a5942f594106575cc6ba9f +14a91b5383dc58a04629135783aa9530 +2eb55d859f402efccaedf26bdd5e7902 +590f580c70c9786da51aa8c51179e56a +74fac2eaf11337dfb26e3526fca79fae +d88e4d50c166be51882d8661fbeccf96 +88af6551d54e4ed178dce48233d2e5c0 +28fa953a8f34f100e579ca2c7009f38d +985fd0061c8219c4696c77f6bf115b5d +bd145b09d36a4fe829198e996fbb5a94 +c0fbb7395a9445007c6f48cfe663d7af +0af2f819e0d26a06f5e2fc093f9c543b +4a618df8ff7b52d9488d55640d455bb3 +83042b532a5feaa5a5330c36c70484b4 +39b3379e8a674abaf69c67fa2d9a309f +2510362eadc9668087a4464bef146a44 +afd98f5903cf76c395904e0bb634b4aa +c8a6d4f5db71bb848292eada0ce9b5c9 +4437d4d02ae857c35cafb4a86a9c819c +b5bc681646a7065caf5a5a3ec93b976a +7a7b91f1c78eba0b5da0203dabd3d972 +b4346ff7c1cb9074e51c7f0ef962e2a0 +d5958a235c658ef6af3177d59a2f980d +047e5c8dbf486d1017e335c38005711d +96a7fd8916f7b2bf5c192b2b52a5566a +5c555d4a1e27a0d72bfa06b1c655cd78 +80d6238d61e3f2281f7567c4816b20b7 +72160290ff45fe6fdb995555508fc592 +4696ab63675996dc280b290f51b8d095 +2d2440a00a59b3ec9edcd09e9899f30b +4493b8a3446daf20f34541bde3c24d41 +0b4bc2afc7e9af942c1dc79fd120862f +1758e3ad8cf7cf842955968ef22c68be +5811fbff95273c5413591fbc96b07a0c +dad847f2b0916baded9b5f219b99c213 +69d82cb277d48c60c9baf260b55c8792 +8c4d98b207141ca2fa344fcc7415d164 +a5421ea000aca629c3185e9270d34c8f +5dda27e72e1d5fff2614b14967aa89db +14e209d94a0bf732aaab43f272825d0e diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp9-superframe.bin b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp9-superframe.bin new file mode 100644 index 00000000..910b5eba Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp9-superframe.bin differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame.vp9.ivf b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame.vp9.ivf new file mode 100644 index 00000000..590bdeda Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame.vp9.ivf differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame.vp9.ivf.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame.vp9.ivf.crc new file mode 100644 index 00000000..f7997676 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame.vp9.ivf.crc @@ -0,0 +1,13 @@ +eabdf4a9 +6be7a0d4 +b36ef585 +db3e1a59 +d8d2e5cf +db3e1a59 +6eca5ed2 +6eca5ed2 +6f0c30e5 +057e11d3 +2aa4acfc +057e11d3 +2aa4acfc diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame.vp9.ivf.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame.vp9.ivf.md5 new file mode 100644 index 00000000..2b810310 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame.vp9.ivf.md5 @@ -0,0 +1,13 @@ +eaf1eea9f60fc2fb187bbef2d31af7ce +4a6b0e9f7ea530f5df470692bb217d9e +e72ff5543af78beffb82e385374d258c +34cde815edd7c2e30287879307f6277a +1945558ad45e249c5213a1724e6119e6 +34cde815edd7c2e30287879307f6277a +942648ec667110c846a4c706651fbca8 +942648ec667110c846a4c706651fbca8 +51e6d2b032ddcce42eae15c8ba83656a +d084afaff3d84fe0f9991401be11f144 +706533c04957068d77889bccc95e3037 +d084afaff3d84fe0f9991401be11f144 +706533c04957068d77889bccc95e3037 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame2.vp9.ivf b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame2.vp9.ivf new file mode 100644 index 00000000..c60984c7 Binary files /dev/null and b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame2.vp9.ivf differ diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame2.vp9.ivf.crc b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame2.vp9.ivf.crc new file mode 100644 index 00000000..96363f15 --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame2.vp9.ivf.crc @@ -0,0 +1,16 @@ +ed32c757 +b2b099a4 +02b33fae +a8428354 +44eb0023 +8b577eb0 +09bd1b35 +0c10cfa3 +a8428354 +44eb0023 +8b577eb0 +09bd1b35 +0c10cfa3 +a8428354 +44eb0023 +8b577eb0 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame2.vp9.ivf.md5 b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame2.vp9.ivf.md5 new file mode 100644 index 00000000..38a6e66d --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/codec/vp9/test_data/vp90-2-10-show-existing-frame2.vp9.ivf.md5 @@ -0,0 +1,16 @@ +9d37524d753e9fa42a0eef64a0059da2 +c64a7219c785b2bc02bdce466d66a363 +49c1deda1b22079af7a27fb79a6d99c3 +5657cc859e68b77964545d9bfe6c8379 +e69ada58184ab59345fe44ee2db4db06 +d4176f8ef863083edd6a01a8e6ed1e18 +524373eaaebc7ce4a1d02b2745a6a74c +3b6ada280133bd5543c8827dd763b055 +5657cc859e68b77964545d9bfe6c8379 +e69ada58184ab59345fe44ee2db4db06 +d4176f8ef863083edd6a01a8e6ed1e18 +524373eaaebc7ce4a1d02b2745a6a74c +3b6ada280133bd5543c8827dd763b055 +5657cc859e68b77964545d9bfe6c8379 +e69ada58184ab59345fe44ee2db4db06 +d4176f8ef863083edd6a01a8e6ed1e18 diff --git a/crates/pf-bitstream/vendor/cros-codecs/src/lib.rs b/crates/pf-bitstream/vendor/cros-codecs/src/lib.rs new file mode 100644 index 00000000..c8a816ab --- /dev/null +++ b/crates/pf-bitstream/vendor/cros-codecs/src/lib.rs @@ -0,0 +1,85 @@ +// Copyright 2022 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +//! Vendored cros-codecs **parser layer**: the `codec` module (H.264 / H.265 / AV1 / VP9 +//! parsers, DPBs, picture types) plus `bitstream_utils`. The decoder/encoder/backend +//! halves of upstream are deliberately not vendored — punktfunk's own decode layer +//! (`pf-bitstream`) sits where upstream's `decoder::stateless` would. +//! +//! This lib.rs is the one heavily-trimmed file: upstream's carries the feature-gated +//! backend modules and CLI-facing enums; only the items the `codec` module actually +//! references survive here (`Resolution` and its round mode). Everything below this +//! module doc is copied verbatim from upstream lib.rs. See PROVENANCE.md for the +//! snapshot source and the full list of deviations. + +// Vendored code is not held to the workspace lint bar: CI's `--workspace -- -D warnings` +// clippy leg would otherwise fail on upstream style (27 warnings at vendoring time). +// Held-back lints here, PROVENANCE.md records the posture. +#![allow(clippy::all)] +#![allow(mismatched_lifetime_syntaxes)] +// The one bar vendored code IS held to, and the whole point of this layer: the code that +// parses hostile bitstream bytes contains no unsafe, compiler-enforced. Upstream was one +// pointer-subtraction away from this already (PROVENANCE.md #5); a re-sync that brings +// unsafe into the codec module must fail here and be judged, not slide in. +#![forbid(unsafe_code)] + +pub mod bitstream_utils; +pub mod codec; + +/// Rounding modes for `Resolution` +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ResolutionRoundMode { + /// Rounds component-wise to the next even value. + Even, +} + +/// A frame resolution in pixels. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct Resolution { + pub width: u32, + pub height: u32, +} + +impl Resolution { + /// Whether `self` can contain `other`. + pub fn can_contain(&self, other: Self) -> bool { + self.width >= other.width && self.height >= other.height + } + + /// Rounds `self` according to `rnd_mode`. + pub fn round(mut self, rnd_mode: ResolutionRoundMode) -> Self { + match rnd_mode { + ResolutionRoundMode::Even => { + if self.width % 2 != 0 { + self.width += 1; + } + + if self.height % 2 != 0 { + self.height += 1; + } + } + } + + self + } + + pub fn get_area(&self) -> usize { + (self.width as usize) * (self.height as usize) + } +} + +impl From<(u32, u32)> for Resolution { + fn from(value: (u32, u32)) -> Self { + Self { + width: value.0, + height: value.1, + } + } +} + +impl From for (u32, u32) { + fn from(value: Resolution) -> Self { + (value.width, value.height) + } +} diff --git a/crates/pf-client-core/Cargo.toml b/crates/pf-client-core/Cargo.toml index 40212ef7..d44fe6b3 100644 --- a/crates/pf-client-core/Cargo.toml +++ b/crates/pf-client-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pf-client-core" -description = "Shared client plumbing (Linux + Windows) — session pump, FFmpeg decode, PipeWire/WASAPI audio, SDL3 gamepads, trust store, discovery — extracted from the GTK client so the shells and the Vulkan session binary build on one implementation" +description = "Shared client plumbing (Linux + Windows) — session pump, native video decode, PipeWire/WASAPI audio, SDL3 gamepads, trust store, discovery — extracted from the GTK client so the shells and the Vulkan session binary build on one implementation" version.workspace = true edition.workspace = true rust-version.workspace = true @@ -15,12 +15,78 @@ repository.workspace = true # (same public surface — see lib.rs). [target.'cfg(any(target_os = "linux", windows))'.dependencies] punktfunk-core = { path = "../punktfunk-core", features = ["quic"] } -# FFmpeg's Vulkan hwcontext surface (Vulkan Video decode on the presenter's device). -pf-ffvk = { path = "../pf-ffvk" } +# Native Vulkan Video decode (WP-C of the native-decode program, HEVC added by M3 +# WP-2, AV1 by M7): auto's TOP rung on both desktop OSes since M9 — for every codec it +# speaks, AV1 included — also pinnable via `PUNKTFUNK_DECODER=native-vulkan` — +# video_vk_native.rs, running pf-vkdecode's VkH264Decoder/VkH265Decoder/VkAv1Decoder on +# the presenter's shared device. +pf-vkdecode = { path = "../pf-vkdecode" } +# The one bitstream parser (M1): the SOFTWARE rung reads its per-picture colour +# signalling, IDR flag and recovery-point SEI from the same `AuPlan` every hardware rung +# already submits from (`video_software.rs`). That is what makes the swscale BT.601 +# default unrepresentable rather than merely fixed — there is no second colour source +# left to disagree with. +pf-bitstream = { path = "../pf-bitstream" } async-channel = "2" -# Video decode (same FFmpeg pin as the host) and Opus for the audio planes. -ffmpeg-next = "8" +# M8's software rung, the ladder's last one — no FFmpeg in either half. +# +# H.264: openh264 (BSD-2), already a workspace dependency (the HOST's GPU-less encoder, +# `pf-encode/src/enc/sw.rs`), so the licence posture and the bundled-source build are +# both already settled and already compiled by every `--workspace` leg. +# +# AV1: rav1d (BSD-2) — dav1d itself, ported to Rust by the ISRG/Prossimo memory-safety +# project. The plan of record names "dav1d"; the `dav1d` crate reaches it through +# `dav1d-sys`, which is `system-deps`-only (no vendored build): it needs `dav1d.pc` + +# headers at build time and `libdav1d.so`/`dav1d.dll` at run time on EVERY client +# package. That is a new system codec dependency added by the milestone family whose +# §6 excision checklist exists to delete exactly those. rav1d is the same decoder with +# none of that: pure Rust, no linker, nothing new in any package. +# +# ⚠ Both of these are NEW COMPILE COST on the client packaging legs, which is easy to +# miss because the workspace already built openh264: every client leg is `-p`-scoped and +# excludes pf-encode (flatpak's `cargo build -p punktfunk-client-linux -p +# punktfunk-client-session -p punktfunk-cli`, windows.yml/windows-msix.yml's +# `-p punktfunk-client-windows …`, deb.yml's client job, packaging/nix's +# `punktfunk-client`), so all of them compile the bundled OpenH264 tree for the FIRST +# time here. Only the `--workspace` CI legs and the host packages built it before. +# +# `default-features = false` drops two things deliberately: +# * `asm` — rav1d's hand-written assembly needs `nasm` at build time, and it is NOT the +# same trade openh264 makes next to it: openh264-sys2's `try_compile_nasm` returns +# quietly when nasm is missing ("Failed to compile NASM files, not using any +# assembly") and the C build still succeeds, whereas rav1d's build.rs PANICS ("NASM +# build failed. Make sure you have nasm installed or disable the \"asm\" feature"). +# So turning `asm` on makes nasm a hard build requirement of every client package, +# and `ci/rust-ci.Dockerfile` — the container the client .deb and the workspace CI +# build in — does not have it (arch, rpm, nix and the FFmpeg-building noble image +# all do; the flatpak GNOME SDK and the Windows runner are not provisioned by +# anything in this tree). Making the rung that only ever runs BECAUSE the GPU +# already failed a build-breaker for the legs that ship it is the wrong way round. +# Turn it back on the day every client leg provisions nasm — and expect a large +# speedup when you do; this is dav1d's asm, and the Rust fallbacks are much slower. +# * `bitdepth_16` — the CPU rung is 8-bit by contract (`video_software.rs` refuses +# anything else rather than mis-scaling it), so building the 10/12-bit half would be +# compiling a path the code refuses to take. +# +# One packaging risk this DOESN'T carry: rav1d exports dav1d's C ABI as `#[no_mangle]` +# symbols (`dav1d_open`, `dav1d_send_data`, …), which could in principle interpose on a +# real libdav1d loaded into the same process. It cannot here — these are Rust `staticlib` +# symbols in an executable with no `-rdynamic` and no dynamic export table entry, so the +# loader never offers them to anyone. That changes if pf-client-core ever becomes a +# `cdylib` or a leg adds `-rdynamic`/`--export-dynamic`; re-check it then. +openh264 = "0.9" +rav1d = { version = "1", default-features = false, features = ["bitdepth_8"] } +# errno names for rav1d's negated-`c_int` returns (`video_software.rs`): `ENOPROTOOPT` — +# the code a `bitdepth_8`-only build answers a 10-bit stream with — is 92 on Linux and +# 123 on Windows, and rav1d re-exports only `Dav1dResult`, so the typed enum that would +# otherwise name it is out of reach. Already in the tree (rav1d's own dependency). +libc = "0.2" + +# Opus for the audio planes. The VIDEO side has no FFmpeg at all since M10: every decode +# rung is native (pf-vkdecode / pf-dxvadec / pf-vaadec / openh264+rav1d) and the codec +# vocabulary is `punktfunk_core::quic`'s own `CODEC_*` wire bits. The HOST still encodes +# with libavcodec (`pf-encode`); nothing in this crate does. opus = "0.3" mdns-sd = "0.20" @@ -54,17 +120,33 @@ rand = "0.9" [target.'cfg(target_os = "linux")'.dependencies] pipewire = "0.9" sdl3 = { version = "0.18", features = ["hidapi"] } +# Native VAAPI decode (M6 of the native-decode program): the hand-declared libva buffer +# layouts, the profile/format/surface decisions, the AuPlan → picparams/IQ/slice +# conversion and the DRM-PRIME export descriptor that `video_vaapi_native` marshals. +# Cross-platform on purpose — everything decidable without a device is tested by the +# ordinary macOS and container gates, exactly as pf-dxvadec does for Windows. +pf-vaadec = { path = "../pf-vaadec" } +# libva itself is dlopen'd, never linked (see `video_vaapi_native`'s module docs): the +# container can then compile and clippy the whole rung without `libva-dev`, and a machine +# without a VAAPI runtime gets a clean refusal instead of a packaging dependency. +libloading = "0.8" [target.'cfg(windows)'.dependencies] wasapi = "0.23" +# Native D3D11VA decode (M5 of the native-decode program): the hand-declared DXVA buffer +# layouts and the AuPlan → picparams/qmatrix/slice-control conversion that video_d3d11_native +# submits. Windows-only because the rung is; the crate itself is cross-platform CPU code so +# its tests run on every CI leg (which is the point — `cfg(windows)` code cannot be tested by +# the Linux or macOS gates at all). +pf-dxvadec = { path = "../pf-dxvadec" } # Pad-audio correlation (pad_audio.rs): the HID devnode's ContainerID and a render endpoint's # stamped PKEY_Device_ContainerId both live in the registry — read-only, which sidesteps COM # property stores entirely (the same version the host pins). winreg = "0.56" sdl3 = { version = "0.18", features = ["hidapi", "build-from-source"] } -# D3D11VA decode (video_d3d11.rs): device/adapter selection, DXVA probes, and the shared -# NT-handle hand-off ring. Same pinned rev as clients/windows so the workspace builds ONE -# windows-rs. +# D3D11 decode-device plumbing (video_d3d11.rs): device/adapter selection, DXVA probes, and +# the shared NT-handle hand-off ring `video_d3d11_native` fills. Same pinned rev as +# clients/windows so the workspace builds ONE windows-rs. windows = { git = "https://github.com/microsoft/windows-rs", rev = "acb5a1a7441033d9312b16842af02eb0c2b403dc", features = [ # Features are header-named since windows-rs generates from the SDK headers directly # (#4689) — one feature per header, replacing the old `Win32_*` namespace features. @@ -83,6 +165,13 @@ windows = { git = "https://github.com/microsoft/windows-rs", rev = "acb5a1a74410 "winuser", ] } +[target.'cfg(windows)'.dev-dependencies] +# The native D3D11VA rung's frame-hash parity test compares decoded surfaces against the +# libavcodec goldens M5 captured — the same SHA-256 list, and the same crate, pf-vkdecode's +# Vulkan parity legs use (already in the workspace lock). The goldens are checked-in +# hashes; nothing links FFmpeg to read them. +sha2 = "0.10" + [features] # PyroWave client decode ships in every default build (flatpak included; pyrowave-sys is a # vendored in-repo tree, offline-safe, and an empty stub off Linux/Windows). The codec is diff --git a/crates/pf-client-core/src/au_dump.rs b/crates/pf-client-core/src/au_dump.rs new file mode 100644 index 00000000..7a6802d1 --- /dev/null +++ b/crates/pf-client-core/src/au_dump.rs @@ -0,0 +1,124 @@ +//! Decoder-input capture behind `PUNKTFUNK_DUMP_VIDEO` (fixture corpus for the +//! native-decode program, design/client-native-decode.md M0). +//! +//! Writes every AU exactly as the pump hands it to [`crate::video::Decoder::decode_frame`]: +//! the data file is the raw concatenation (a valid Annex-B / OBU stream for clean +//! captures), and the sidecar `.idx` keeps what a byte stream cannot carry — the exact +//! AU boundaries plus the wire `flags`/`complete` bits — one `offset len flags complete` +//! line per AU, so parser fixtures never have to re-derive framing from start codes. +//! +//! Capture is best-effort by design: any I/O error logs once and disables the dump for +//! the rest of the session; the streaming path is never failed on its account. The +//! final buffered tail flushes on drop (session end), errors swallowed — a truncated +//! last AU in a debug capture is acceptable, a stream torn down over one is not. + +use std::io::BufWriter; +use std::io::Write; +use std::path::Path; + +pub(crate) struct AuDump { + data: BufWriter, + idx: BufWriter, + offset: u64, +} + +/// Wire-codec byte → fixture file extension (also the corpus naming convention). +fn codec_ext(codec: u8) -> &'static str { + match codec { + punktfunk_core::quic::CODEC_H264 => "h264", + punktfunk_core::quic::CODEC_HEVC => "h265", + punktfunk_core::quic::CODEC_AV1 => "av1", + punktfunk_core::quic::CODEC_PYROWAVE => "pyrowave", + _ => "bin", + } +} + +impl AuDump { + /// Read `PUNKTFUNK_DUMP_VIDEO`; `None` (the overwhelmingly common case) means the + /// variable is unset or the capture files could not be created — both already logged + /// where they matter. + pub(crate) fn from_env(codec: u8) -> Option { + let dir = std::env::var_os("PUNKTFUNK_DUMP_VIDEO")?; + let stamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + Self::create(Path::new(&dir), &format!("au-{stamp}"), codec) + } + + fn create(dir: &Path, base: &str, codec: u8) -> Option { + let ext = codec_ext(codec); + let data_path = dir.join(format!("{base}.{ext}")); + let idx_path = dir.join(format!("{base}.{ext}.idx")); + let made = std::fs::create_dir_all(dir).and_then(|()| { + Ok(( + std::fs::File::create(&data_path)?, + std::fs::File::create(&idx_path)?, + )) + }); + match made { + Ok((data, idx)) => { + tracing::info!( + path = %data_path.display(), + "PUNKTFUNK_DUMP_VIDEO: capturing decoder input" + ); + Some(AuDump { + data: BufWriter::new(data), + idx: BufWriter::new(idx), + offset: 0, + }) + } + Err(e) => { + tracing::warn!( + error = %e, + dir = %dir.display(), + "PUNKTFUNK_DUMP_VIDEO set but capture files could not be created" + ); + None + } + } + } + + /// Append one AU. Returns `false` once the dump should be dropped (error logged). + pub(crate) fn write(&mut self, au: &[u8], flags: u32, complete: bool) -> bool { + let r = self.data.write_all(au).and_then(|()| { + writeln!( + self.idx, + "{} {} {:#x} {}", + self.offset, + au.len(), + flags, + u8::from(complete) + ) + }); + self.offset += au.len() as u64; + match r { + Ok(()) => true, + Err(e) => { + tracing::warn!(error = %e, "PUNKTFUNK_DUMP_VIDEO write failed — capture disabled"); + false + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_capture_is_a_byte_faithful_concatenation_with_a_boundary_index() { + let dir = std::env::temp_dir().join(format!("pf-au-dump-test-{}", std::process::id())); + let mut dump = AuDump::create(&dir, "t", punktfunk_core::quic::CODEC_HEVC) + .expect("capture files should be creatable in a temp dir"); + assert!(dump.write(&[1, 2, 3], 0x04, true)); + assert!(dump.write(&[9, 8], 0x00, false)); + drop(dump); + + let data = std::fs::read(dir.join("t.h265")).unwrap(); + let idx = std::fs::read_to_string(dir.join("t.h265.idx")).unwrap(); + assert_eq!(data, &[1, 2, 3, 9, 8]); + assert_eq!(idx, "0 3 0x4 1\n3 2 0x0 0\n"); + let _ = std::fs::remove_dir_all(&dir); + } +} diff --git a/crates/pf-client-core/src/lib.rs b/crates/pf-client-core/src/lib.rs index 40e575c2..2926c6c0 100644 --- a/crates/pf-client-core/src/lib.rs +++ b/crates/pf-client-core/src/lib.rs @@ -20,6 +20,8 @@ // instead of an argument precisely because nothing required one. #![deny(clippy::undocumented_unsafe_blocks)] +#[cfg(any(target_os = "linux", windows))] +mod au_dump; #[cfg(target_os = "linux")] pub mod audio; #[cfg(windows)] @@ -70,13 +72,21 @@ pub mod video; mod video_color; #[cfg(any(target_os = "linux", windows))] mod video_software; -// libav ownership helpers shared by the hardware decoders below (`AvBuffer`). -#[cfg(any(target_os = "linux", windows))] -mod video_libav; +// Native VAAPI decode (M6 of the native-decode program): pf-vaadec's plans driven +// straight into libva, dlopen'd at runtime, exporting DRM-PRIME dmabufs the presenter +// imports. Since M10 it is the ONLY VAAPI rung there is — the libavcodec one it +// replaced is deleted — so `auto` reaches it wherever the vendor order puts VAAPI +// first; `PUNKTFUNK_DECODER=native-vaapi` reaches it by pin regardless. See `video`'s +// evidence table for what hardware has actually run it. #[cfg(target_os = "linux")] -mod video_vaapi; +pub mod video_vaapi_native; +// Native Vulkan Video decode (WP-C of the native-decode program, HEVC added by M3 +// WP-2, AV1 by M7): pf-vkdecode's H.264/H.265/AV1 decoders on the presenter's shared +// device — auto's TOP rung on both desktop OSes since M9, for all three codecs (each +// leg has hardware parity against libavcodec; see `video`'s evidence table), also +// pinnable via `PUNKTFUNK_DECODER=native-vulkan`. #[cfg(any(target_os = "linux", windows))] -mod video_vulkan; +mod video_vk_native; // The OS-clipboard bridge for the shared clipboard (design/clipboard-and-file-transfer.md §5). // Built everywhere the session client is; the platform seam inside is Windows-real, // stub elsewhere. @@ -87,8 +97,19 @@ pub mod clipboard; // Linux's: the decoder is plain Vulkan compute on the presenter's device (no fds, no // dmabuf, no D3D11 interop), so the old "Windows present-path decision" that gated it // resolved itself — the present path is now literally the same code. +// D3D11 decode-device plumbing: the shareable-texture hand-off ring, the decode-device +// creation and `display_hdr_volume`. Field-proven, FFmpeg-free code that +// `video_d3d11_native` (and `clients/session`) build on; the libavcodec DECODER that used +// to live alongside it went with M10's excision. #[cfg(windows)] pub mod video_d3d11; +// Native D3D11VA (M5): `ID3D11VideoDecoder` driven from pf-bitstream plans, filling the +// hand-off ring `video_d3d11` owns. Since M10 it is the only DXVA rung there is. In `auto` +// for the codecs that have hardware evidence (H.264/H.265) and, with nothing proven left +// below it, for AV1 too — see `video`'s evidence table; `PUNKTFUNK_DECODER=native-d3d11va` +// reaches every leg by pin. +#[cfg(windows)] +pub mod video_d3d11_native; #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] pub mod video_pyrowave; diff --git a/crates/pf-client-core/src/session.rs b/crates/pf-client-core/src/session.rs index 11449db1..77ca0012 100644 --- a/crates/pf-client-core/src/session.rs +++ b/crates/pf-client-core/src/session.rs @@ -16,6 +16,12 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; +/// `Clone` so an embedder can keep the params a session was started with and re-dial with +/// one field changed — which is what the codec fallback ([`SessionEvent::CodecFallback`]) +/// needs and the only reason the derive exists. Every field is either plain data or an +/// `Arc` the retry deliberately SHARES: the same `force_software` flag and the same +/// presenter-written `latch_grid`, because they belong to the presenter, not the session. +#[derive(Clone)] pub struct SessionParams { pub host: String, pub port: u16, @@ -28,6 +34,15 @@ pub struct SessionParams { /// The user's preferred video codec (a `quic::CODEC_*` bit, `0` = auto). Soft — the host honors /// it when it can emit it, else falls back; the resolved codec drives the decoder. pub preferred_codec: u8, + /// `quic::CODEC_*` bits to REMOVE from this session's advertised decode caps. + /// + /// `0` for every ordinary connect. It is set by a retry after + /// [`SessionEvent::CodecFallback`]: a session whose codec exhausted the decode ladder + /// (in practice HEVC, whose CPU rung M8 removed — no permissively licensed software + /// HEVC decoder exists) comes back advertising the codec set the host must pick + /// from instead. The codec is fixed at Welcome and the control stream renegotiates + /// shard payload only, so a fresh Hello is the ONLY lever; this field is it. + pub exclude_codecs: u8, /// The advertised `quic::VIDEO_CAP_*` bits. Normally 10-bit + HDR (Main10/PQ: the /// Vulkan presenter decodes P010 everywhere and presents PQ on an HDR10 swapchain /// where the desktop offers one, tonemapping in the CSC shader where it doesn't; @@ -68,8 +83,8 @@ pub struct SessionParams { /// Library id for the host to launch this session (`"steam:570"`, from the library /// page); `None` = plain desktop session. pub launch: Option, - /// The presenter's shared Vulkan device, when its stack can run FFmpeg's Vulkan - /// Video decoder (decode lands as VkImages the presenter samples directly). + /// The presenter's shared Vulkan device, when its stack can run Vulkan Video decode + /// (decode lands as VkImages the presenter samples directly). pub vulkan: Option, /// Pinned host fingerprint; `None` = trust on first use (caller persists the observed one). pub pin: Option<[u8; 32]>, @@ -181,6 +196,48 @@ pub struct Stats { /// `chroma_444` false, the host declined — the OSD says so instead of leaving the /// switch's effect unobservable. pub asked_444: bool, + /// The decode lane can answer integrity questions AT ALL (M4). True on the native + /// hardware rungs and false on the CPU rung and PyroWave. It exists because the + /// libavcodec rungs it was written against could NOT answer — their Vulkan decoder + /// created no status queries (`nb_queries = 0`), never set `AV_FRAME_FLAG_CORRUPT`, + /// and reported trouble only as log lines, which is why the Xbox Ally X corruption + /// was undetectable rather than merely undetected. + /// + /// Everything below is meaningless without it, and a surface that renders the + /// four counters as zeros on a lane that cannot see damage is repeating the + /// exact mistake this program exists to end: "clean" and "unmeasured" are not + /// the same claim. + pub decode_integrity: bool, + /// AUs whose plan needed CONCEALMENT this window — a lost reference, a + /// `frame_num` gap, a short NALU walk. Each one cost a frame (released unshown) + /// and a re-anchor request. + pub decode_damaged: u32, + /// Frames the DRIVER reported corrupt this window through their per-op + /// `RESULT_STATUS` query — the Xbox Ally X class, and the count no libavcodec rung + /// could ever produce. Always 0 where `decode_status_queries` is false: there is no + /// verdict to read, not nothing to report. (`video::DecodeHealth::note` + /// enforces that, so the two fields can never contradict each other here.) + pub decode_failed: u32, + /// AUs the decoder REFUSED outright this window — a plan error, a + /// Vulkan/session failure. Distinct from `decode_damaged`, and the difference + /// is the whole diagnosis: concealment means the decoder coped with a damaged + /// stream, refusal means it could not run and the screen is frozen. A rung + /// refusing every AU used to report as a perfectly clean session. + pub decode_refused: u32, + /// Consecutive AUs with no showable picture as of this window's end (0 = the + /// stream is decoding clean right now). The field that separates a lossy link + /// from a stream that never came back — see `video::DecodeHealth::run`. + pub concealed_run: u32, + /// The LONGEST such run of the session so far — session-cumulative, not + /// windowed, and deliberately so: `concealed_run` is an instant sampled once a + /// second, which misses the bad moment almost every time. A window whose + /// `concealed_run` is 0 and whose `worst_concealed_run` is 40 is a session that + /// froze hard and recovered, and no other field on this struct says that. + pub worst_concealed_run: u32, + /// The device answers per-op decode-status queries (`queryResultStatusSupport`). + /// FALSE on RADV, where recording one HANGS the VCN ring, and there the integrity + /// report covers the parser's half only. + pub decode_status_queries: bool, } /// Frames the pump keeps waiting for their 0xCF host timing (pts → capture→received µs). @@ -214,9 +271,55 @@ pub enum SessionEvent { trust_rejected: bool, }, Ended(Option), + /// The session's negotiated codec ran out of decode rungs and the client can finish + /// this stream only as a DIFFERENT codec — terminal, like [`Self::Ended`], but with + /// the retry already computed. + /// + /// The one case in practice is HEVC on a box whose hardware HEVC decode failed: M8 + /// dropped software HEVC (no permissively licensed decoder exists), so the ladder's + /// last rung refuses instead of limping, and the answer is a reconnect advertising + /// [`Self::CodecFallback::retry_caps`] — which never contains the codec that just + /// failed. The other case is a picture SHAPE the CPU rung cannot decode (10-bit, + /// 4:4:4), which is a different diagnosis with the same available action; the two + /// pick different retry sets, and [`crate::video::last_rung_verdict`] is where that + /// is decided. + /// + /// An embedder that does not implement the retry MUST still show `msg` and stop — + /// treating it as an ordinary end is correct, just worse. It is a separate variant + /// rather than a flag on `Ended` so the compiler asks every embedder the question + /// once, which is how the two D3D11VA rungs' shared `stats:` tag went wrong when it + /// was not asked (`1573a987`). + CodecFallback { + /// What to pass as [`SessionParams::exclude_codecs`] on the retry — DERIVED from + /// [`Self::CodecFallback::retry_caps`], so applying it advertises exactly those + /// caps and nothing wider. + exclude_codecs: u8, + /// The caps the retry will advertise — non-empty by construction, and what + /// `exclude_codecs` above resolves to on the wire. + retry_caps: u8, + /// User-facing one-liner for the toast/status strip. + msg: String, + }, Stats(Stats), } +/// How many times THIS PROCESS has had a session's codec exhaust the decode ladder — the +/// telemetry counter the risk register asks for ("telemetry on frequency") for the +/// software-HEVC drop. +/// +/// Process-scoped and monotonic because the thing being counted is a property of the +/// machine, not of one session: a box whose hardware HEVC decode is broken produces one +/// of these per connect, and it is the RATE across a session history that says whether +/// dropping software HEVC hurt anybody. Read it with [`codec_fallbacks`]. +static CODEC_FALLBACKS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + +/// See [`CODEC_FALLBACKS`]. Surfaced on the session's Detailed stats block as an +/// additive `codec_fallbacks ` line once it is nonzero — appended last, never +/// removed, never reordered (`run.rs`'s `stats_text`). +pub fn codec_fallbacks() -> u64 { + CODEC_FALLBACKS.load(Ordering::Relaxed) +} + /// The in-stream microphone mute (B4), shared between the embedder's toggle (a keyboard chord /// in the presenter) and the capture callback that reads it every quantum. /// @@ -369,6 +472,23 @@ fn pump( // on their arrivals, so this bit alone changes nothing without a wired DualSense. let pad_speaker_on = crate::pad_audio::speaker_active(¶ms.pad_speaker); let pad_audio_on = params.pad_haptics || pad_speaker_on; + // What this session advertises it can decode, minus anything a previous attempt + // proved it cannot FINISH (see `SessionParams::exclude_codecs`). Held for the whole + // pump because the reconnect rule needs to know what was on the table, not just what + // the host picked. + let advertised_codecs = crate::video::decodable_codecs_for( + params.vulkan.as_ref(), + // The decoder pin is part of the answer: a session pinned to software has no HEVC + // rung at all, so advertising HEVC would promise what this build cannot keep. + ¶ms.decoder, + ) & !params.exclude_codecs; + if params.exclude_codecs != 0 { + tracing::info!( + excluded = params.exclude_codecs, + advertising = advertised_codecs, + "retrying with reduced decode caps" + ); + } let connector = match NativeClient::connect( ¶ms.host, params.port, @@ -378,8 +498,10 @@ fn pump( params.bitrate_kbps, params.video_caps, params.audio_channels, - // FFmpeg's codecs plus CODEC_PYROWAVE when the presenter device passed the probe. - crate::video::decodable_codecs_for(params.vulkan.as_ref()), + // The codecs OUR rungs speak (`video::decodable_codecs`), plus CODEC_PYROWAVE when + // the presenter device passed the probe, minus whatever a previous attempt proved + // undecodable end to end. + advertised_codecs, preferred, // the user's soft codec preference (0 = auto; see the pyrowave opt-in above) // This display's HDR volume → the host's virtual-display EDID. The env hatch wins so an // A/B run can pin an exact peak (PUNKTFUNK_CLIENT_PEAK_NITS=600). @@ -400,8 +522,8 @@ fn pump( } else { 0 }), - // Slice-progressive delivery: off — this presenter feeds FFmpeg whole AUs; a partial - // avcodec feed path can flip it later. + // Slice-progressive delivery: off — every rung here is fed whole AUs; a partial-feed + // path can flip it later. false, params.launch.clone(), // The host's approval-list / trust-store label for this client. Without it every no-PIN @@ -440,36 +562,29 @@ fn pump( }); // Build the decoder for the codec the host resolved (never assume HEVC), honoring the - // Settings backend preference (auto/vaapi/software). - let codec_id = crate::video::ffmpeg_codec_id(connector.codec); - // The WIRE codec is the negotiated truth; the FFmpeg id is meaningful only where - // FFmpeg decodes it. `ffmpeg_codec_id`'s fallthrough maps every unknown wire bit — - // PyroWave included — to HEVC, so logging it unconditionally claimed - // `codec_id=HEVC` for wavelet sessions that never touch FFmpeg at all. - let codec = match connector.codec { - punktfunk_core::quic::CODEC_H264 => "H264", - punktfunk_core::quic::CODEC_HEVC => "HEVC", - punktfunk_core::quic::CODEC_AV1 => "AV1", - punktfunk_core::quic::CODEC_PYROWAVE => "PyroWave", - _ => "unknown", + // Settings backend preference (auto/native-*/software). + // + // The WIRE codec bit IS the vocabulary now: M10 deleted the last libavcodec rung and + // with it `ffmpeg::codec::Id`, which this used to translate into here. That + // translation was also a small lie in the log — its fallthrough mapped every unknown + // wire bit, PyroWave included, to HEVC, so a wavelet session printed `codec_id=HEVC`. + // + // The picture shape the host RESOLVED (not what we asked for) goes with it — every + // native rung probes its device against it at construction, so a 4:4:4 or Main 10 + // session that this GPU has no decode format for refuses BEFORE the rung is chosen + // instead of error-streaking past it mid-stream. + let stream_format = crate::video::StreamFormat { + chroma_format_idc: connector.chroma_format, + bit_depth: connector.bit_depth, }; - if connector.codec == punktfunk_core::quic::CODEC_PYROWAVE { - tracing::info!( - codec, - welcome_codec = connector.codec, - "negotiated video codec" - ); - } else { - tracing::info!( - codec, - ?codec_id, - welcome_codec = connector.codec, - "negotiated video codec" - ); - } - // A negotiated PyroWave session decodes on the presenter's device, no FFmpeg — - // reachable only through the explicit preference above (resolve_codec never - // auto-picks the bit), so failing loudly here is failing an opted-in experiment. + tracing::info!( + codec = crate::video::wire_codec_name(connector.codec), + welcome_codec = connector.codec, + "negotiated video codec" + ); + // A negotiated PyroWave session decodes on the presenter's device — reachable only + // through the explicit preference above (resolve_codec never auto-picks the bit), so + // failing loudly here is failing an opted-in experiment. #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] let built = if connector.codec == punktfunk_core::quic::CODEC_PYROWAVE { let mode = connector.mode(); @@ -497,14 +612,46 @@ fn pump( )), } } else { - Decoder::new(codec_id, ¶ms.decoder, params.vulkan.as_ref()) + Decoder::new( + connector.codec, + ¶ms.decoder, + params.vulkan.as_ref(), + stream_format, + ) }; #[cfg(not(all(any(target_os = "linux", windows), feature = "pyrowave")))] - let built = Decoder::new(codec_id, ¶ms.decoder, params.vulkan.as_ref()); + let built = Decoder::new( + connector.codec, + ¶ms.decoder, + params.vulkan.as_ref(), + stream_format, + ); let mut decoder = match built { Ok(d) => d, Err(e) => { - let _ = ev_tx.send_blocking(SessionEvent::Ended(Some(format!("video decoder: {e}")))); + // The ladder had NO rung for this codec at all — on a box with no hardware + // HEVC decode (or one that pinned `PUNKTFUNK_DECODER=software` on an HEVC + // session). Same answer as the mid-stream case below, one code path. + let refusal = e.downcast_ref::().map(|nr| { + codec_fallback_event( + connector.codec, + advertised_codecs, + nr.loss(), + &e.to_string(), + ) + }); + // Nothing has been spawned yet at this point — the audio / pad / clipboard + // threads and the mic uplink are all built BELOW — so "joined its threads" is + // vacuously true here. Set the stop flag and drop the connector anyway, in + // the same order the pump's end path does, so an embedder that reconnects on + // receipt of this event finds the same world whichever refusal site produced + // it (`run.rs` starts the retry the instant it reads one). + stop.store(true, Ordering::SeqCst); + mic.set_live(false); + drop(connector); + let _ = ev_tx.send_blocking( + refusal.unwrap_or_else(|| SessionEvent::Ended(Some(format!("video decoder: {e}")))), + ); return; } }; @@ -630,6 +777,36 @@ fn pump( // ahead of `frames_dropped` (the reassembler only declares a straggler lost once it ages out of // the loss window, by which point the concealment already reached the screen). let mut next_expected_index: Option = None; + // Fixture capture for the native-decode program: every AU exactly as it reaches + // `decode_frame`, plus a boundary/flags index — see `au_dump.rs` for the format. + // + // NOTE for fault runs: this captures what the HOST sent. `PUNKTFUNK_AU_FAULT`'s + // injector lives one level down, at the native backend's decode entry, so on a + // faulted run the fixture is the CLEAN bitstream and replaying it will not + // reproduce the damage (reconstruct that from the spec — the injector is pure + // and deterministic). Deliberate: the dump's job is to preserve the host's + // output, and moving the injector above it would corrupt every backend's input + // rather than only the lane whose detectors it exists to fire. + let mut au_dump = crate::au_dump::AuDump::from_env(connector.codec); + // The decode-order watermark at the latest arm of the freeze gate (M4 review): + // a frame whose `decode_order` is at or below this was DECODED before the loss, + // whatever order it was delivered in, so its recovery point SEI describes a wave + // that completed before the loss and must not lift the freeze the loss raised. + // `gate.arms()` is the trigger to re-stamp — it moves at every arm site, + // including the two inside the gate, and not on the overdue backstop (which + // re-asks without re-arming, and where discarding an in-flight heal would be + // exactly wrong). Inert on every lane without its own parser: `decode_order` is + // 0 there and `local_recovery` is NONE anyway. + let mut gate_arms = gate.arms(); + let mut arm_decode_order: u64 = 0; + // Decode-integrity window cursor (M4), the same per-window diffing as + // `window_dropped`: the decoder's counters are session-cumulative, the OSD shows + // the delta. `None` on every lane that cannot answer — see `Stats::decode_integrity`. + let mut window_health = decoder.decode_health(); + // Set when the ladder ran out of rungs for this codec (M8): the loop breaks and this + // event replaces the plain `Ended` at the bottom. `Some` is the only way the pump + // ends with a retry attached. + let mut codec_fallback: Option = None; let end: Option = loop { if stop.load(Ordering::SeqCst) { @@ -743,40 +920,96 @@ fn pump( Some(n) if frame.frame_index.wrapping_sub(n) > u32::MAX / 2 => n, _ => frame.frame_index, }); + if let Some(d) = au_dump.as_mut() { + if !d.write(&frame.data, frame.flags, frame.complete) { + au_dump = None; + } + } + // Re-stamp the arm watermark BEFORE this AU decodes and advances the + // decoder's ordinal, so it names the newest picture that existed when the + // freeze was armed. One site covers every arm: the frame-gap arm above + // happened moments ago in this same iteration, and the four sites below + // (`on_no_output` ×2, the decoder-recovery arm, `poll`'s dropped climb) all + // run AFTER the decode, so the next iteration reaches here with the ordinal + // still exactly as they left it. + if gate.arms() != gate_arms { + gate_arms = gate.arms(); + arm_decode_order = decoder.decode_order(); + } match decoder.decode_frame(&frame.data, frame.flags, frame.complete) { Ok(Some(image)) => { - // Fold this decoded frame through the shared freeze gate: it reads the AU's - // re-anchor wire flags (FLAG_SOF IDR marker / RECOVERY_ANCHOR / RECOVERY_POINT), - // takes `image.is_keyframe()` as the ffmpeg keyframe belt, applies the two-mark + // The decoder's OWN re-anchor observation FIRST (M4): a recovery point SEI + // is the only clean point an intra-refresh session has when the host does not + // mark the wire — its wave emits no IDR to flag, and only + // one of the three encoder backends that run a wave sets + // USER_FLAG_RECOVERY_POINT — so without this such a session freezes for the + // full REANCHOR_FREEZE_MAX and then forces the very IDR the wave exists to + // avoid. The gate pairs the mark against its own arm (only a wave that + // STARTED after the loss proves anything about it) and lifts on the first + // trusted one. Before `on_decoded`, so the frame that healed the picture is + // itself presented rather than held one more round. Inert on every other + // lane: `local_recovery` reports NONE and the wire path is untouched. + // + // The gate pairs by TIME; this pairs by DECODE ORDER, and both are + // needed. A decoder that flushes its DPB after a failed AU hands back + // every picture it still held — pictures decoded BEFORE the loss, + // carrying the marks of the wave they were decoded in — and they + // arrive after the arm, so the gate cannot tell. Their ordinal can. + let local = match image.decode_order() { + Some(order) if order <= arm_decode_order => { + tracing::trace!( + order, + arm_decode_order, + "discarding the local recovery of a frame decoded before \ + the loss" + ); + punktfunk_core::reanchor::LocalRecovery::NONE + } + _ => image.local_recovery(), + }; + if gate.on_local_recovery(local) { + tracing::debug!( + "re-anchored on the stream's own recovery point SEI — no IDR needed" + ); + } + // Then the shared freeze gate: it reads the AU's re-anchor wire flags + // (FLAG_SOF IDR marker / RECOVERY_ANCHOR / RECOVERY_POINT), takes + // `image.is_keyframe()` as the decoder's own IDR belt, applies the two-mark // rule + the mark-patience backstop, clears the no-output streak, and returns // whether to present this frame or withhold it as a post-loss concealment. let present = gate.on_decoded(frame.flags, image.is_keyframe(), Instant::now()) == GateVerdict::Present; total_frames += 1; + // ⚠ The `stats:` decode-path tag is a machine interface — + // additive only. M10 removed the rungs whose tags were `vaapi`, + // `vulkan` and `d3d11va`; every surviving tag keeps its exact + // spelling. dec_path = match &image { DecodedImage::Cpu(_) => "software", #[cfg(target_os = "linux")] - DecodedImage::Dmabuf(_) => "vaapi", - DecodedImage::VkFrame(_) => "vulkan", + DecodedImage::NativeDmabuf(_) => "native-vaapi", #[cfg(windows)] - DecodedImage::D3d11(_) => "d3d11va", + DecodedImage::D3d11(_) => "native-d3d11va", #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] DecodedImage::PyroWave(_) => "pyrowave", + DecodedImage::NativeVk(_) => "native-vulkan", }; if total_frames == 1 { let (w, h, path) = match &image { DecodedImage::Cpu(c) => (c.width, c.height, "software"), #[cfg(target_os = "linux")] - DecodedImage::Dmabuf(d) => (d.width, d.height, "vaapi-dmabuf"), - DecodedImage::VkFrame(v) => (v.width, v.height, "vulkan-video"), + DecodedImage::NativeDmabuf(d) => { + (d.width, d.height, "native-vaapi-dmabuf") + } #[cfg(windows)] - DecodedImage::D3d11(d) => (d.width, d.height, "d3d11va"), + DecodedImage::D3d11(d) => (d.width, d.height, "native-d3d11va"), #[cfg(all( any(target_os = "linux", windows), feature = "pyrowave" ))] DecodedImage::PyroWave(f) => (f.width, f.height, "pyrowave"), + DecodedImage::NativeVk(f) => (f.width, f.height, "native-vulkan"), }; tracing::info!(width = w, height = h, path, "first frame decoded"); } @@ -810,8 +1043,28 @@ fn pump( // shows becomes that sample — honest, at zero pipeline cost on // every other frame. Software keeps the synchronous stamp on // every frame (its decode really is done by now). + // + // M4 re-examined this against the native rung's non-blocking + // reads (`poll_status`, `get_semaphore_counter_value`) and left + // it exactly as it is. Polling can only ever answer "complete + // by NOW", and the only place this thread polls is once per AU + // — so every sample would be quantized up by as much as a whole + // frame interval (8.3 ms at 120 Hz, against decodes that + // measure ~0.1-2 ms). That is not a cheaper measurement, it is + // a wrong one, and it would replace a true figure with a + // plausible-looking upper bound nothing downstream could tell + // apart. Sampling faster needs either a spin (the CPU burn this + // comment already warns about) or a second thread on a decoder + // that is `Send` but deliberately not `Sync`. Correctness beats + // the metric: one honest sample per window stands. let hw_fence = match &image { - DecodedImage::VkFrame(v) => Some((v.timeline_sem, v.decode_done_value)), + // The native rung's frame carries the timeline pair: the + // decode signals `semaphore_value` when the pixels are + // ready (the presenter's write-back is the `+ 1`), so + // waiting it measures received→decode-complete. Fed since + // the WP-D hardware verdict landed (bit-exact parity, both + // DPB modes). + DecodedImage::NativeVk(f) => Some((f.semaphore, f.semaphore_value)), _ => None, }; if present { @@ -868,6 +1121,25 @@ fn pump( tracing::debug!("requested keyframe (decoder produced no output)"); } } + // NOT survivable, and the only decode error that isn't: the ladder + // demoted to its last rung and there is no such rung for this codec. + // Feeding more AUs would freeze the screen forever — the exact + // "limping on software" outcome M8's HEVC drop replaces with an + // action. Break out of the pump; the terminal event below carries the + // retry the embedder reconnects with. + Err(e) if e.downcast_ref::().is_some() => { + let loss = e + .downcast_ref::() + .expect("just matched") + .loss(); + codec_fallback = Some(codec_fallback_event( + connector.codec, + advertised_codecs, + loss, + &e.to_string(), + )); + break None; + } // Survivable (loss until the next IDR/RFI recovery) — keep feeding. Err(e) => { tracing::debug!(error = %e, "decode error (recovering)"); @@ -895,9 +1167,31 @@ fn pump( // GOP has no periodic keyframe, so a rebuilt/erroring decoder would stay // gray/frozen until an unrelated packet drop happened to request one. Route it // through the same throttle as loss recovery below. + // + // The native rung's DAMAGE path arrives here too (M4): an AU whose plan needed + // concealment answers `Ok(None)` and raises this flag rather than erroring, so + // the ask happens at exactly this moment and through exactly this throttle + // while the decoder keeps its rung — stream damage is not a decoder fault (see + // `video_vk_native`'s recovery policy). That also bounds the whole thing: one + // ask per 100 ms per session however fast the damage arrives, and once the gate + // is armed further damage refreshes an existing freeze rather than compounding + // into more requests. + // + // ARM ONLY WHEN NOT ALREADY HOLDING. This flag fires per DAMAGED AU, not per + // loss, and every `arm` zeroes the gate's recovery-mark count and its + // local-SEI credit. Re-arming on each one therefore made both re-anchor paths + // — the wire's two-mark rule and M4's local SEI — impossible to complete + // during exactly the sustained damage they were written for, leaving recovery + // resting entirely on the throttled keyframe ask. A genuinely NEW loss still + // re-arms with its marks zeroed: it arrives as a frame-index gap or a + // `frames_dropped` climb, both of which arm unconditionally. The keyframe ask + // below is untouched — it still fires per damaged AU, through the same 100 ms + // throttle. if decoder.take_keyframe_request() { let now = Instant::now(); - gate.arm(now); + if !gate.is_holding() { + gate.arm(now); + } if last_kf_req .is_none_or(|t| now.duration_since(t) >= Duration::from_millis(100)) { @@ -1048,6 +1342,28 @@ fn pump( .saturating_sub(window_mic.dropped_full + window_mic.dropped_stale) as u32; window_mic = mic_now; + // Decode integrity (M4): session-cumulative counters, diffed per window + // like `frames_dropped`. `None` on a lane that cannot see damage at all — + // and that stays distinguishable from "saw none" all the way to the OSD. + let health_now = decoder.decode_health(); + let (decode_damaged, decode_failed, decode_refused) = match (health_now, window_health) + { + (Some(now), Some(prev)) => ( + now.damaged.saturating_sub(prev.damaged) as u32, + now.failed.saturating_sub(prev.failed) as u32, + now.refused.saturating_sub(prev.refused) as u32, + ), + // A lane that could not answer at the last window and can now. + // Unreachable today — the cursor is seeded from the decoder before + // the first AU and the ladder only ever demotes AWAY from the + // native rung, never back onto it — so this exists to keep the + // match total with a defensible answer (the cumulative figure) + // instead of an `unwrap` that would be a panic if that ever + // changed. + (Some(now), None) => (now.damaged as u32, now.failed as u32, now.refused as u32), + (None, _) => (0, 0, 0), + }; + window_health = health_now; tracing::debug!( fps = frames_n, hostnet_p50_us = hn_p50, @@ -1061,6 +1377,12 @@ fn pump( lost, mic_sent, mic_dropped, + decode_damaged, + decode_failed, + decode_refused, + concealed_run = health_now.map(|h| h.run).unwrap_or(0), + worst_concealed_run = health_now.map(|h| h.worst_run).unwrap_or(0), + decode_status_queries = health_now.map(|h| h.status_queries).unwrap_or(false), total_frames, "stream window" ); @@ -1090,6 +1412,13 @@ fn pump( auto_rate, chroma_444, asked_444, + decode_integrity: health_now.is_some(), + decode_damaged, + decode_failed, + decode_refused, + concealed_run: health_now.map(|h| h.run).unwrap_or(0), + worst_concealed_run: health_now.map(|h| h.worst_run).unwrap_or(0), + decode_status_queries: health_now.is_some_and(|h| h.status_queries), })); window_start = Instant::now(); frames_n = 0; @@ -1124,7 +1453,56 @@ fn pump( if let Some(t) = clipboard_thread { let _ = t.join(); // exits within its next_clip wait once `stop` is set } - let _ = ev_tx.send_blocking(SessionEvent::Ended(end)); + // The codec-exhaustion end has its own terminal event — sent HERE, after the audio / + // pad / clipboard threads have joined, so an embedder that reconnects on receipt + // never has two sessions' worth of threads on the same connector. + let _ = ev_tx.send_blocking(codec_fallback.unwrap_or(SessionEvent::Ended(end))); +} + +/// Build the terminal event for a session whose codec exhausted the decode ladder, and +/// bump the telemetry counter. +/// +/// One place, called from both refusal sites (decoder construction and the mid-stream +/// demotion), because the two must produce the SAME retry — a construction-time refusal +/// that reconnected onto a different codec set than the mid-stream one would make field +/// reports unreadable. +fn codec_fallback_event( + negotiated: u8, + advertised: u8, + loss: crate::video::RungLoss, + detail: &str, +) -> SessionEvent { + use crate::video::{last_rung_verdict, wire_codec_name, LastRungVerdict}; + CODEC_FALLBACKS.fetch_add(1, Ordering::Relaxed); + let codec = wire_codec_name(negotiated); + match last_rung_verdict(negotiated, advertised, loss) { + LastRungVerdict::Retry { caps } => { + tracing::warn!( + codec, + retry_caps = caps, + detail, + "video decode ran out of rungs — reconnecting without this codec" + ); + SessionEvent::CodecFallback { + // DERIVED from the verdict, never from the failed codec alone: the retry + // then advertises exactly `caps` (`decodable_codecs_for & !exclude` + // re-intersects to it), so the wire and the rule cannot disagree. They + // did before the M8 review — the rule dropped PyroWave and the wire + // re-offered it. + exclude_codecs: advertised & !caps, + retry_caps: caps, + msg: format!("{codec} decoding failed on this device — reconnecting"), + } + } + // Nothing left to advertise: reconnecting would negotiate the same dead end. End + // the session and say what actually happened, rather than loop. + LastRungVerdict::Dead => { + tracing::error!(codec, detail, "video decode ran out of rungs and of codecs"); + SessionEvent::Ended(Some(format!( + "{codec} can't be decoded on this device, and no other codec is available" + ))) + } + } } /// The dedicated audio thread: owns the Opus decoder, the PCM scratch, and the PipeWire @@ -1252,4 +1630,113 @@ mod tests { assert!(!mic.muted()); assert_eq!(mic.toggle(), None); } + + /// M8's HEVC reconnect, as the terminal event both refusal sites produce. + /// + /// This is the "reconnect flow tested as a first-class path" the plan's risk register + /// asks for, at the layer where it can be tested without a host: the pump's two + /// call sites (decoder construction and the mid-stream demotion) both go through + /// `codec_fallback_event`, so pinning its output pins the flow — the retry never + /// re-offers the codec that just failed, the message is user-facing, and the + /// telemetry counter moves exactly once per occurrence. + /// `CODEC_FALLBACKS` is process-global and `codec_fallback_event` bumps it, so the + /// test that asserts "counted exactly once" cannot run beside another that calls the + /// same builder. Both take this. + static FALLBACK_COUNTER: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + #[test] + fn an_exhausted_codec_produces_a_retry_event_and_moves_the_counter() { + use crate::video::RungLoss; + use punktfunk_core::quic::{CODEC_AV1, CODEC_H264, CODEC_HEVC}; + let _guard = FALLBACK_COUNTER.lock().unwrap_or_else(|e| e.into_inner()); + let before = codec_fallbacks(); + + // The shipping shape: HEVC negotiated, H.264 also advertised. + let ev = codec_fallback_event( + CODEC_HEVC, + CODEC_H264 | CODEC_HEVC, + RungLoss::Codec, + "no software HEVC", + ); + match ev { + SessionEvent::CodecFallback { + exclude_codecs, + retry_caps, + ref msg, + } => { + assert_eq!(exclude_codecs, CODEC_HEVC, "the retry must drop HEVC"); + assert_eq!(retry_caps, CODEC_H264); + // The toast is for a person: it names the codec and says what happens + // next, and does NOT read as an error the user has to act on. + assert!(msg.contains("HEVC"), "{msg}"); + assert!(msg.contains("reconnect"), "{msg}"); + } + _ => panic!("expected a CodecFallback"), + } + assert_eq!(codec_fallbacks(), before + 1, "counted exactly once"); + + // Hardware AV1 advertised too: both survivors stay on the table. + match codec_fallback_event( + CODEC_HEVC, + CODEC_H264 | CODEC_HEVC | CODEC_AV1, + RungLoss::Codec, + "x", + ) { + SessionEvent::CodecFallback { retry_caps, .. } => { + assert_eq!(retry_caps, CODEC_H264 | CODEC_AV1); + } + _ => panic!("expected a CodecFallback"), + } + + // Nothing left to offer: end honestly instead of a reconnect loop. Still counted + // — the failure happened, and its frequency is exactly what the counter is for. + let before = codec_fallbacks(); + match codec_fallback_event(CODEC_HEVC, CODEC_HEVC, RungLoss::Codec, "x") { + SessionEvent::Ended(Some(msg)) => { + assert!(msg.contains("HEVC"), "{msg}"); + assert!(msg.contains("no other codec"), "{msg}"); + } + _ => panic!("expected a plain Ended"), + } + assert_eq!(codec_fallbacks(), before + 1); + } + + /// `exclude_codecs` and `retry_caps` describe the SAME retry — the review found them + /// disagreeing, and the wire follows `exclude_codecs`, so a mismatch means the tested + /// rule is not the shipped one. + /// + /// The property is exact, not approximate: the retry advertises + /// `decodable_codecs_for(vk) & !exclude_codecs`, and this session already advertised + /// `decodable_codecs_for(vk) & !old_exclude` — so re-intersecting with the derived + /// mask must land on `retry_caps` itself. + #[test] + fn the_retrys_exclusion_resolves_to_exactly_its_advertised_caps() { + use crate::video::RungLoss; + use punktfunk_core::quic::{CODEC_AV1, CODEC_H264, CODEC_HEVC, CODEC_PYROWAVE}; + let _guard = FALLBACK_COUNTER.lock().unwrap_or_else(|e| e.into_inner()); + for advertised in 0u8..16 { + for negotiated in [CODEC_H264, CODEC_HEVC, CODEC_AV1, CODEC_PYROWAVE] { + for loss in [RungLoss::Codec, RungLoss::Shape] { + let SessionEvent::CodecFallback { + exclude_codecs, + retry_caps, + .. + } = codec_fallback_event(negotiated, advertised, loss, "x") + else { + continue; // Dead — nothing is advertised at all + }; + assert_eq!( + advertised & !exclude_codecs, + retry_caps, + "advertised {advertised:#x} negotiated {negotiated:#x} {loss:?}" + ); + assert_eq!(retry_caps & negotiated, 0, "the failed codec came back"); + } + } + } + // Excluding twice is idempotent — a second fallback in the same run widens the + // set rather than resetting it (`run.rs` ORs into the existing value). + let full = CODEC_H264 | CODEC_HEVC | CODEC_AV1; + assert_eq!((full & !CODEC_HEVC) & !CODEC_HEVC, CODEC_H264 | CODEC_AV1); + } } diff --git a/crates/pf-client-core/src/trust.rs b/crates/pf-client-core/src/trust.rs index 1aeaecd2..4af6b2df 100644 --- a/crates/pf-client-core/src/trust.rs +++ b/crates/pf-client-core/src/trust.rs @@ -1046,8 +1046,18 @@ pub struct Settings { /// preference — the host honors it when it can emit it, else falls back to the best shared codec. #[serde(default = "default_codec")] pub codec: String, - /// Video decoder preference: `"auto"` (Vulkan Video → VAAPI → software), - /// `"vulkan"`, `"vaapi"`, `"software"`. + /// Video decoder preference: `"auto"` (vendor-ordered native ladder — pf-vkdecode over + /// Vulkan Video, then the platform's own rung, then software; see `video::Decoder::new` + /// for the per-vendor order), `"native-vulkan"`, `"native-vaapi"`, `"native-d3d11va"`, + /// or `"software"`. + /// + /// ⚠ A STORED value is not a validated one — this is a plain `String` read out of a + /// user's settings file, and the pre-M10 spellings `"vulkan"`/`"vaapi"`/`"d3d11va"` + /// (which every desktop Settings UI offered) named libavcodec's rungs, deleted at + /// M10. `video::migrate_decoder_pref` maps each onto the native rung for the same + /// hardware family, at `warn`, so an upgrade does not end a session over a dropdown + /// the user picked long ago. Nothing rewrites the STORE — the value is migrated on + /// every read, so downgrading to an older client still works. /// The `PUNKTFUNK_DECODER` env var overrides this (see `video::Decoder::new`). pub decoder: String, /// Decode/present GPU (multi-GPU boxes): the adapter's marketing name, as the WinUI diff --git a/crates/pf-client-core/src/video.rs b/crates/pf-client-core/src/video.rs index 74a71965..e20237df 100644 --- a/crates/pf-client-core/src/video.rs +++ b/crates/pf-client-core/src/video.rs @@ -1,50 +1,118 @@ -//! Video decode: reassembled HEVC access units → frames for the presenter. +//! Video decode: reassembled access units → frames for the presenter. //! -//! Three backends, picked at session start (auto is vendor-ordered on BOTH desktop OSes — -//! see [`VulkanDecodeDevice::prefer_vulkan_first`]. Linux: vaapi → vulkan → software on -//! desktop Mesa, vulkan first on NVIDIA/VanGogh. Windows: d3d11va → vulkan → software on -//! Intel/unknown, vulkan first on NVIDIA/AMD. -//! Override: `PUNKTFUNK_DECODER=vulkan|vaapi|d3d11va|software`): +//! # The ladder (M10: native only) //! -//! * **Vulkan Video**: FFmpeg's Vulkan decoder running on the PRESENTER's own VkDevice -//! (its handles arrive via [`VulkanDecodeDevice`]) — the decoded VkImage feeds the -//! presenter's CSC pass directly, zero copy, every vendor with the video extensions -//! (NVIDIA's only hardware path; measured 4K@144 with 0.1 ms decode). -//! * **VAAPI** (Intel/AMD fallback): libavcodec hwaccel; each frame is mapped to a -//! DRM-PRIME dmabuf (`av_hwframe_map`, zero copy) and handed over as fds + plane -//! layout for the presenter's Vulkan import. NVIDIA has no usable VAAPI -//! (nvidia-vaapi-driver is broken for this — Moonlight blacklists it); device -//! creation fails there. A mid-session error falls back — the host's IDR/RFI -//! recovery resynchronizes. -//! * **Software**: libavcodec on the CPU + swscale to RGBA (staging upload). -//! Slice threading only — frame threading would add a frame of latency per thread. +//! Every rung is a NATIVE rung — pf-vkdecode, pf-dxvadec, pf-vaadec, openh264/rav1d — for +//! every codec and on both desktop OSes. **There is no libavcodec in this crate at all**: +//! M10 deleted the three FFmpeg-backed rungs (`video_vulkan`, `video_vaapi`, the +//! libavcodec half of `video_d3d11`), the `video_libav` helpers they shared, `pf-ffvk` and +//! the `ffmpeg-next` dependency itself. Vendor order is unchanged +//! ([`VulkanDecodeDevice::prefer_vulkan_first`]): //! -//! Both run `AV_CODEC_FLAG_LOW_DELAY`; the host encodes zero-reorder streams (no -//! B-frames, in-band parameter sets on every IDR), so decode is strictly one-in/one-out. +//! | Linux, NVIDIA/AMD | Linux, Intel/unknown | Windows, NVIDIA/AMD | Windows, Intel/unknown | +//! |---|---|---|---| +//! | native-vk → native-vaapi → sw | native-vaapi → native-vk → sw | native-vk → native-d3d11va → sw | native-d3d11va → native-vk → sw | //! -//! On Windows the VAAPI/dmabuf backend does not exist (DRM-PRIME is a Linux concept); the -//! hardware pair there is Vulkan Video and **D3D11VA** (`crate::video_d3d11` — the -//! vendor-agnostic DXVA path every Windows video player exercises), ordered per vendor: -//! Intel's driver DOES advertise Vulkan Video (Arc drivers since 2023), but FFmpeg-Vulkan -//! on it strobes and burns the frame budget (B580 field report, 2026-07) where D3D11VA -//! streams clean — so Intel/unknown take D3D11VA first and NVIDIA/AMD keep Vulkan first. -//! Everything dmabuf-shaped is `cfg(target_os = "linux")`-gated inline. +//! M9's evidence FILTER survives, narrowed to the one thing it can still protect +//! ([`native_rung_admitted`]). The filter kept a rung that had never decoded on real +//! hardware out of `auto` while its proven libavcodec twin was one step below. With the +//! twins deleted that is usually no longer the situation: below native-d3d11va's AV1 leg, +//! and below native-vaapi on NVIDIA/AMD, there is nothing proven left to fall onto, so +//! barring the rung would not move a session one rung DOWN — it would take hardware decode +//! away from it entirely, which is the worse answer. +//! +//! **One column of that table is different, and it is the one the filter still guards.** +//! On Linux, Intel and every unknown vendor id run `native-vaapi → native-vk → sw` +//! ([`VulkanDecodeDevice::prefer_vulkan_first`] is true for NVIDIA and AMD only), so the +//! rung directly below the never-run pf-vaadec is native Vulkan Video — H.264 and H.265 on +//! three drivers plus a 92-minute soak, AV1 250/250. There, barring the unproven rung moves +//! the session exactly one rung down, onto proven code, so it is barred: an unproven rung +//! yields to a rung that is BOTH verified for this codec and usable on THIS device, and to +//! nothing else. A pin still reaches it — that is how the missing evidence gets generated. +//! +//! Where a rung is admitted unproven, every session SAYS so: the "decode rung active" line +//! carries `hardware_verified` and the evidence note verbatim, and it is a **warning** when +//! no hardware has ever decoded through the rung/codec pair the session just chose +//! ([`log_rung`], [`native_evidence`]). That table below is what a field report about M10 +//! has to be read against. +//! +//! # Evidence — which rungs have actually decoded on hardware +//! +//! Recorded here because it is the fact a support engineer needs when a session log names +//! a rung. [`native_evidence`] is the same table in code; it is what every session logs at +//! decoder construction (`hardware_verified` / `evidence` on the "decode rung active" +//! line). +//! +//! | rung | module | codecs | hardware that has decoded on it | +//! |---|---|---|---| +//! | native Vulkan Video | [`crate::video_vk_native`] | H.264 | **yes** — bit-exact vs libavcodec, 250/250 AUs on three drivers + a 92-minute soak (M2 WP-D) | +//! | native Vulkan Video | | H.265 (Main / Main10 / 4:4:4) | **yes** — same parity run + HDR chain and Deck/VanGogh legs (M3) | +//! | native Vulkan Video | | AV1 | **yes** — 250/250 bit-identical to libavcodec on an RTX 5070 Ti (M7); ONE vendor, no soak | +//! | native D3D11VA | [`crate::video_d3d11_native`] | H.264, H.265 | **yes** — frame-hash parity on an RTX 4090 and an AMD iGPU + a 30-minute soak (M5) | +//! | native D3D11VA | | AV1 | **NO** — has never decoded a frame anywhere (M7 wired it; the box was unavailable) | +//! | native VAAPI | [`crate::video_vaapi_native`] | H.264, H.265, AV1 | **NO** — has never decoded a frame anywhere (M6/M7; no VAAPI hardware was reachable) | +//! | software | `video_software` | H.264, AV1 | **NO on glass** — openh264 + rav1d, CPU unit tests only (M8) | +//! +//! The software rung's evidence is recorded for the same reason but does not gate +//! anything: it is the LAST rung, so there is nothing below it to protect. +//! +//! # The rungs +//! +//! * **native Vulkan Video** (`video_vk_native`): pf-vkdecode's H.264/H.265/AV1 decoders +//! on the PRESENTER's own VkDevice — the decoded VkImage feeds its CSC pass directly, +//! zero copy. Admission is [`native_vulkan_gate`]. +//! * **native D3D11VA** (`video_d3d11_native`, Windows): pf-dxvadec plans driven into +//! `ID3D11VideoDecoder`, filling the field-proven shareable-texture hand-off ring in +//! `crate::video_d3d11`. +//! * **native VAAPI** (`video_vaapi_native`, Linux): pf-vaadec plans driven into a +//! dlopen'd libva, exporting DRM-PRIME dmabufs. NVIDIA has no usable VAAPI at all +//! (nvidia-vaapi-driver is broken for this — Moonlight blacklists it), so device +//! creation simply fails there and the ladder walks on. +//! * **Software**: the CPU rung, FFmpeg-free since M8 — openh264 for H.264, rav1d +//! (dav1d) for AV1, planes uploaded straight to the presenter's planar CSC pass. It is +//! the LAST rung, so it never demotes further; and it has no HEVC decoder at all (none +//! exists under a permissive licence), which is a REFUSAL that reconnects the session +//! onto a codec this client can decode — see [`last_rung_verdict`] and +//! [`NoSoftwareRung`]. +//! +//! The host encodes zero-reorder streams (no B-frames, in-band parameter sets on every +//! IDR), so decode is strictly one-in/one-out on every rung. +//! +//! Windows has no VAAPI (DRM-PRIME is a Linux concept) and Linux no DXVA; the vendor +//! order differs for one reason worth keeping in view: Intel's Windows driver DOES +//! advertise Vulkan Video (Arc drivers since 2023), but Vulkan decode on it strobed and +//! burned the frame budget (B580 field report, 2026-07 — measured on the FFmpeg-Vulkan +//! rung of the day) where DXVA streamed clean, so Intel/unknown take DXVA first and +//! NVIDIA/AMD keep Vulkan first. Everything dmabuf-shaped is +//! `cfg(target_os = "linux")`-gated inline. +//! +//! # Overrides +//! +//! `PUNKTFUNK_DECODER=native-vulkan|native-d3d11va|native-vaapi|software`. A pin skips +//! the vendor order, which is how a lab run reaches a rung `auto` would not pick on this +//! device; an init failure still logs and falls through to the standard ladder, so a pin +//! can never cost a session its decoder. +//! +//! The pre-M10 spellings `vulkan`/`vaapi`/`d3d11va` named libavcodec's rungs specifically. +//! They MIGRATE onto the native rung for the same hardware family, loudly +//! ([`migrate_decoder_pref`]) — they are not developer-only strings, every desktop +//! Settings UI offered them, and refusing them would end a session over a dropdown the +//! user picked long ago. -// bindgen's C-enum repr is target-dependent (u32 on Linux/clang, i32 on MSVC), so the -// pf-ffvk Vulkan flag/enum casts below are required on one platform and no-ops on the -// other — the lint would fire on whichever platform the cast is a no-op for. -#![allow(clippy::unnecessary_cast)] - -use anyhow::{anyhow, bail, Context as _, Result}; -use ffmpeg_next as ffmpeg; +// `bail!` has exactly one site left and it is Windows-only (the D3D11VA rung's win32 +// external-memory refusal), so the import is gated with it rather than allowed dead. +#[cfg(windows)] +use anyhow::bail; +use anyhow::Result; #[cfg(target_os = "linux")] use std::os::fd::RawFd; pub use crate::video_color::{csc_rows, ColorDesc}; +/// Re-exported so the SESSION layer (and its tests) can name the refusal by type — the +/// module itself stays private, like every other backend's. +pub use crate::video_software::NoSoftwareRung; use crate::video_software::SoftwareDecoder; -#[cfg(target_os = "linux")] -use crate::video_vaapi::VaapiDecoder; -use crate::video_vulkan::VulkanDecoder; +use crate::video_vk_native::{NativeCodec, NativeVulkanDecoder}; /// One decoded frame headed for the presenter, carrying the host capture timestamp so the /// UI can measure capture→displayed latency at the moment it presents. @@ -64,11 +132,29 @@ pub struct DecodedFrame { pub use crate::video_d3d11::D3d11Frame; pub enum DecodedImage { - Cpu(CpuFrame), + /// The SOFTWARE rung's output (M8): tightly-packed 8-bit I420 planes for the + /// presenter to upload and run its planar CSC pass over. + /// + /// It REPLACES the old `Cpu(CpuFrame)` RGBA variant rather than joining it — there is + /// exactly one CPU rung, and the swscale conversion it used to carry (and its BT.601 + /// default) is what M8 deleted. Adding a second CPU variant would have + /// bought the [`DecodedImage::NativeDmabuf`] property below for a distinction that + /// does not exist: no `stats:` tag, no presenter path and no consumer would ever have + /// been able to reach the old one. + Cpu(CpuPlanarFrame), + /// The NATIVE VAAPI rung's output (`pf-vaadec` + `video_vaapi_native`, M6): dmabuf + /// fds plus a plane layout, exported DRM-PRIME from libva. + /// + /// It shared this payload type with a `Dmabuf` variant — libavcodec's VAAPI hwaccel, + /// deleted at M10 — and was kept SEPARATE from it deliberately, which is what the + /// name still records. That was not fastidiousness: the two D3D11VA rungs shared one + /// variant (they shared the hand-off ring on purpose) and the consequence had to be + /// fixed in `1573a987` — the `stats:` decode-path tag is derived from the variant, so + /// a "native" soak could silently have been an FFmpeg soak with nothing in the log to + /// tell. The name is load-bearing for the same reason today: `native-vaapi` is the + /// tag downstream tooling reads, and renaming the variant would rename that. #[cfg(target_os = "linux")] - Dmabuf(DmabufFrame), - /// FFmpeg Vulkan Video output: a VkImage already on the PRESENTER's device. - VkFrame(VkVideoFrame), + NativeDmabuf(DmabufFrame), /// D3D11VA output copied into a shareable NT-handle texture the presenter imports /// (`VK_KHR_external_memory_win32`) — the DXVA path for GPUs without Vulkan Video /// (Intel's Windows driver foremost). See `crate::video_d3d11`. @@ -79,99 +165,406 @@ pub enum DecodedImage { /// samples them directly (BT.709 limited, the codec's fixed colour contract). #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] PyroWave(crate::video_pyrowave::PyroWavePlanarFrame), + /// Native Vulkan Video output (pf-vkdecode — auto's top rung for H.264, HEVC and + /// AV1; pinnable via `PUNKTFUNK_DECODER=native-vulkan`): a decoded image + + /// per-plane views already on the PRESENTER's device, zero copy — no import, no + /// staging, no interop. The picture format is the + /// stream's, carried on the frame ([`NativeVkFrame::vk_format`] — NV12 for H.264, + /// HEVC Main and AV1 Main 8-bit, P010 for Main 10, the two-plane 4:4:4 formats for + /// RExt and AV1 High), never assumed. The presenter waits the frame's timeline + /// pair, transitions the layer for sampling and BACK to + /// [`NativeVkFrame::layout`], and releases the decoder's slot by dropping the + /// frame (its guard sends the release token). + NativeVk(NativeVkFrame), } -/// One Vulkan-decoded frame. The image lives on the presenter's own VkDevice (the -/// decoder was built over its handles), so presenting is: plane views → CSC pass — no -/// import, no copy. The live synchronization state (layout / timeline value / owning -/// queue family) is deliberately NOT snapshotted here: FFmpeg updates it per submission, -/// so the presenter reads it through `vkframe` under the frames-context lock at ITS -/// submit time (the `AVVulkanFramesContext.lock_frame` contract). -pub struct VkVideoFrame { - /// `AVVkFrame*` — img[0] is the (multiplanar) image; sem/sem_value/layout/ - /// queue_family are the live sync state. Valid while `guard` lives. - pub vkframe: usize, - /// `AVHWFramesContext*` (FFmpeg's) — the first argument to the lock functions. - /// Valid while `guard` lives. - pub frames_ctx: usize, - /// `AVVulkanFramesContext.lock_frame` / `.unlock_frame` (filled in by FFmpeg's - /// init): the presenter MUST hold the lock while reading the live sync state and - /// writing back the incremented semaphore value around its submission. - pub lock_frame: usize, - pub unlock_frame: usize, - /// The frame pool's VkFormat (`AVVulkanFramesContext.format[0]`, raw i32) — the - /// multiplanar format the presenter builds its per-plane views against. - pub vk_format: i32, - /// The frame's timeline semaphore (raw VkSemaphore; creation-constant) and the - /// value FFmpeg's decode submission signals on completion — the pump waits this - /// pair AFTER shipping the frame to measure true GPU decode time (zero pipeline - /// cost: the presenter already waits the same pair on the GPU). - pub timeline_sem: u64, - pub decode_done_value: u64, - pub width: u32, - pub height: u32, - /// The decode POOL's allocated extent (`AVHWFramesContext.width`/`.height`) — the - /// CODED picture size (rounded up to the codec's macroblock alignment, then to the - /// driver's Vulkan picture-access granularity), so it is `>=` `width`/`height`. At - /// 1080p the pool is 1088 rows tall: 1080 is not a multiple of 16. +/// What the decode lane knows about this session's INTEGRITY — M4's telemetry +/// surface, and the answer to the question that started the whole native-decode +/// program: "was that stream actually clean, or could nothing here have told us?" +/// +/// Only the native rung fills it in ([`Decoder::decode_health`] answers `None` +/// everywhere else), because only the native rung has the two detectors: a +/// bitstream planner that reports lost references, and a per-op `RESULT_STATUS` +/// query that reports what the DRIVER thought of the decode. FFmpeg's Vulkan +/// decoder creates no queries at all (`nb_queries = 0`), never sets +/// `AV_FRAME_FLAG_CORRUPT`, and reports trouble only as log lines — which is why +/// the Xbox Ally X corruption was undetectable rather than merely undetected. +/// +/// Counters are session-cumulative and monotonic; the stats window diffs them the +/// way it already diffs `frames_dropped`. Nothing here allocates, and nothing here +/// is computed per frame beyond an add — the whole struct is read once per stats +/// window. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct DecodeHealth { + /// AUs whose plan needed CONCEALMENT: a reference the DPB no longer held, a + /// `frame_num` gap, a NALU walk that stopped early. The picture would have + /// been decoded from a substitute, so its output was released unshown. + pub damaged: u64, + /// Frames the DRIVER reported corrupt through their `RESULT_STATUS_ONLY` + /// query. Distinct from [`Self::damaged`] on purpose: damaged means the + /// bitstream arrived incomplete, failed means the hardware could not decode + /// what did arrive. They have different causes and different fixes, and + /// collapsing them is how "the stream is fine, it's your GPU" arguments start. /// - /// The presenter samples this image with NORMALIZED coordinates, so it needs both - /// numbers — `width`/`height` is what to display, `coded_*` is what the texture - /// actually spans. Sampling `0..1` without the ratio stretches the alignment padding - /// into view; because encoders fill those rows by replicating the picture's last - /// line, that reads as the bottom row smeared over the final few rows of the image - /// (field report 2026-07-31). Same class as the D3D11VA source-rect clamp in - /// `crate::video_d3d11`, which shows as a green bar there only because DXVA padding - /// is left uninitialized rather than replicated. - pub coded_width: u32, - pub coded_height: u32, - pub color: ColorDesc, - /// Intra keyframe (IDR/I): the stream's re-anchor point. The pump resumes display on - /// one after suppressing the concealed frames a reference loss leaves in its wake (on - /// RADV a lost reference decodes to a gray plate with the new motion painted on top). - pub keyframe: bool, - /// Keeps the cloned AVFrame (and through it the VkImage + frames context) alive - /// until the presenter's fence proves the GPU reads done — same mechanism as the - /// VAAPI path's DRM guard. - pub guard: DrmFrameGuard, + /// **Structurally 0 where [`Self::status_queries`] is false**, and + /// [`Self::note`] enforces that rather than trusting its callers: on such a + /// device `poll_status` still answers `Failed` for a lost device or an + /// unreadable timeline, and reporting THAT as a driver verdict would point a + /// support engineer at a verdict the hardware cannot produce ("driver-failed 1 + /// · no driver status" on one line). Those frames still cost a picture, so + /// they still extend [`Self::run`] — they are just not attributed to a driver + /// that never spoke. + pub failed: u64, + /// AUs the decoder REFUSED outright: a plan error (a parse failure, an AU + /// outside the punktfunk envelope, a slice against a parameter set never + /// seen), or a Vulkan/session failure. The decoder produced no picture and + /// said so with an error. + /// + /// Counted apart from [`Self::damaged`] because the two mean opposite things + /// about the RUNG: concealment says the decoder coped with a damaged stream, + /// refusal says the decoder could not run at all. A rung refusing every AU is + /// the shape of a host renegotiating outside the envelope — a frozen screen — + /// and without this counter its stats surface reads exactly like a clean + /// session, which is the founding failure mode of this whole program. + pub refused: u64, + /// Consecutive AUs that produced no showable picture, ending at the latest one + /// — 0 the moment a clean AU decodes. + /// + /// This is the field a support engineer reads first, because it separates the + /// two failure shapes a raw count cannot: `damaged 40 · run 0` is a lossy link + /// that keeps recovering, `damaged 40 · run 40` is a stream that went down and + /// never came back. Both look identical as a total. + pub run: u32, + /// The longest [`Self::run`] of the session — the worst moment, which a + /// once-per-second sample of `run` will usually miss entirely. + pub worst_run: u32, + /// Frames that decoded CORRECTLY and were then discarded without ever being + /// shown, because the backend's deliverable queue overflowed + /// (`video_vk_native::MAX_DELIVERABLE` — a decoder making more pictures + /// display-ready per access unit than the pump can take one at a time). + /// + /// Deliberately its own number and not folded into any of the three above: + /// nothing was damaged, nothing was refused and no driver failed, so counting + /// it as any of those would put a damage report on a healthy stream — and the + /// AU it happened on still showed a picture, so it must not extend + /// [`Self::run`] either. But it cannot be nothing at all: a session quietly + /// discarding a frame per AU is one running at half the frame rate it thinks + /// it is, and before this counter existed it read as perfectly clean. + /// + /// Structurally 0 on every rung but native Vulkan — it is the only one with a + /// deliverable queue — and not on the session stats line today; the + /// rate-limited `warn` at the drop site is the field signal, and this is the + /// number a stats field would read. + pub dropped: u64, + /// This device answers per-op decode-status queries + /// (`queryResultStatusSupport`). When FALSE — RADV, where recording a query + /// anyway HANGS the VCN ring — [`Self::failed`] can only ever read 0, because + /// there is no verdict to read: the status degrades to timeline completion, + /// exactly what FFmpeg knows on every driver. A report that omits this cannot + /// tell "clean" from "unmeasured", which is the precise shape of the failure + /// this program exists to end. + pub status_queries: bool, } -/// True if the decoder tagged this frame as a full IDR keyframe — a guaranteed clean re-anchor -/// after which the picture is loss-free, so the pump can lift a post-loss display freeze here. -/// -/// Keys off `AV_FRAME_FLAG_KEY` (with `pict_type == I` as a belt for decoders that fill pict_type -/// but not the flag). NOTE: FFmpeg's H.264/HEVC decode layer sets this flag **only for true IDR -/// frames**, never for an *intra-refresh recovery point*. H.264 flags key only when a picture's -/// `recovery_frame_cnt == 0` (a moving band uses `> 0`); HEVC clears the flag on every non-IRAP -/// frame regardless of the recovery-point SEI. So an intra-refresh host (NVENC/AMF/QSV) heals the -/// picture over N P-frames with no decoded frame ever flagged key — this function cannot detect -/// that clean point, and the pump would freeze until the `REANCHOR_FREEZE_MAX` backstop (in -/// `session.rs`) forces a real IDR. Detecting an intra-refresh re-anchor requires an out-of-band -/// host wire signal on the AU that completes the wave; that is not yet plumbed. -/// -/// # Safety -/// `frame` must point to a valid `AVFrame` alive for the duration of the call. -pub unsafe fn frame_is_keyframe(frame: *const ffmpeg::ffi::AVFrame) -> bool { - // SAFETY: caller guarantees a live AVFrame; plain field reads. - unsafe { - ((*frame).flags & ffmpeg::ffi::AV_FRAME_FLAG_KEY) != 0 - || (*frame).pict_type == ffmpeg::ffi::AVPictureType::AV_PICTURE_TYPE_I +impl DecodeHealth { + /// Fold one AU's verdict. `damaged` = its plan needed concealment; `refused` = + /// the decoder rejected the AU outright (an `Err` out of `decode`); `failed` = + /// how many PRIOR frames just read a `Failed` decode status. + /// + /// All three extend the run: a support engineer asking "did it ever recover?" + /// means the picture, and a refused AU or a driver-failed frame is as absent + /// from the screen as a concealed one. + /// + /// The one asymmetry is deliberate and is the whole point of + /// [`Self::status_queries`]: where the device answers no status queries, a + /// `Failed` read is NOT a driver verdict — it is the degraded timeline path + /// (the session generation is gone, the device is lost, the semaphore could + /// not be read) — so it extends the run without ever being counted as + /// [`Self::failed`]. Enforced here, at the one place every counter is written, + /// rather than at each call site, because "clean" and "unmeasured" staying + /// distinguishable is the invariant this struct exists for. + pub(crate) fn note(&mut self, damaged: bool, refused: bool, failed: u32) { + if self.status_queries { + self.failed = self.failed.saturating_add(u64::from(failed)); + } + if damaged { + self.damaged = self.damaged.saturating_add(1); + } + if refused { + self.refused = self.refused.saturating_add(1); + } + if damaged || refused || failed > 0 { + self.run = self.run.saturating_add(1); + self.worst_run = self.worst_run.max(self.run); + } else { + self.run = 0; + } + } + + /// Note one correctly-decoded frame discarded unshown — see [`Self::dropped`]. + /// + /// Separate from [`Self::note`] because it is not an AU verdict: several frames + /// can be dropped within one access unit, and the access unit itself may well + /// have shipped a picture. It touches nothing but its own counter, and in + /// particular never [`Self::run`], which answers "did the picture come back" + /// and here it did. + pub(crate) fn note_dropped(&mut self) { + self.dropped = self.dropped.saturating_add(1); } } +/// A raw `VkFormat` code point, carried across the ash-free boundary. +/// +/// A newtype rather than a bare `i32` because the hardware frame type +/// ([`NativeVkFrame`]) carries OTHER `i32`s — `poc` foremost — and the presenter's +/// colour-math lookup takes exactly one number. Handed the wrong +/// one it compiles, warns once about an unmapped format, and renders every frame of +/// the session as 8-bit: decoded correctly, displayed wrong, silently. The wrapper +/// makes that a type error instead. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct RawVkFormat(pub i32); + +/// Every picture format the NATIVE decode lane can deliver, as raw `VkFormat` code +/// points — pf-vkdecode's own [`pf_vkdecode::OUTPUT_FORMATS`] vocabulary, not a copy +/// of it. +/// +/// It is public so the PRESENTER can pin its per-format colour-math table against the +/// real producer. pf-presenter has no pf-vkdecode dependency, so without this its only +/// available check would be its own table against itself — which stays green if +/// pf-vkdecode grows a fifth output format (12-bit RExt) that the CSC pass has no +/// depth mapping for. This crate sees both, so the fact crosses here. +pub fn native_picture_formats() -> Vec { + pf_vkdecode::OUTPUT_FORMATS + .iter() + .map(|f| RawVkFormat(f.as_raw())) + .collect() +} + +/// The layout a [`NativeVkFrame`]'s image layer is in when its semaphore signals — +/// pf-client-core's ash-free mirror of the two decode layouts, so the presenter can +/// transition for sampling and back without this crate naming `vk::ImageLayout`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeVkLayout { + /// `VIDEO_DECODE_DST_KHR` — distinct-mode output; the layer holds ONLY this + /// picture and the next decode into the slot discards it (UNDEFINED-old-layout). + DecodeDst, + /// `VIDEO_DECODE_DPB_KHR` — coincide-mode output: the picture IS a DPB slot and + /// may still be a live reference, so a consumer that transitions it for sampling + /// MUST transition it back to this layout in the same submission. + DecodeDpb, +} + +/// The release token a presented/dropped [`NativeVkFrame`] hands back to the native +/// decode backend: `seq` names the shipped frame, `generation` the decoder session it +/// belongs to (a stale generation routes to the decoder's graveyard — retired pools +/// die on their last token), and `presented` reports whether the presenter SAMPLED +/// the image — i.e. whether its submission enqueued the frame's `value + 1` timeline +/// signal (the write-back the decoder must wait before reusing the image). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NativeReleaseToken { + pub seq: u64, + pub generation: u64, + /// The presenter's sampling submission (with its `value + 1` signal) was + /// enqueued for this frame. `false` for frames dropped unpresented + /// (newest-wins displacement, demotion drain, failed submit). + pub presented: bool, +} + +/// Sends the frame's [`NativeReleaseToken`] exactly once, on drop — the native path's +/// analog of the VAAPI rung's [`DrmFrameGuard`]. The presenter holds the frame (and so +/// this guard) until its sampling submission's fence has been waited, which makes +/// "guard dropped" equal "the GPU is done with the image"; a frame dropped UNPRESENTED +/// (newest-wins displacement, demotion drain) releases through the very same drop. A +/// dead channel (the backend was demoted/rebuilt) is ignored — the decoder that owned +/// the slot is gone. +pub struct NativeReleaseGuard { + tx: std::sync::mpsc::Sender, + token: Option, +} + +impl NativeReleaseGuard { + pub(crate) fn new( + tx: std::sync::mpsc::Sender, + token: NativeReleaseToken, + ) -> Self { + Self { + tx, + token: Some(token), + } + } + + /// Record that the sampling submission — including the frame's `value + 1` + /// timeline signal — was enqueued. The presenter calls this exactly when its + /// submit succeeded; the token then tells the decoder to wait that write-back + /// before the image's next use. + pub fn mark_presented(&mut self) { + if let Some(token) = &mut self.token { + token.presented = true; + } + } +} + +impl Drop for NativeReleaseGuard { + fn drop(&mut self) { + if let Some(token) = self.token.take() { + let _ = self.tx.send(token); + } + } +} + +/// One natively decoded frame (pf-vkdecode). Everything is raw `u64`/plain data — this +/// crate stays ash-free, exactly like [`VulkanDecodeDevice`]. The handles BORROW the +/// decoder's pools: valid until the frame is released (the guard's drop) AND the +/// decoder generation they carry is current — the backend keeps the decoder alive +/// until every shipped frame's token has come back (bounded), so the presenter never +/// has to validate liveness itself. +pub struct NativeVkFrame { + /// The decode image (raw `VkImage`); the picture occupies array layer [`Self::layer`]. + pub image: u64, + /// The picture's own `VkFormat`: what the image was created with and what + /// [`Self::plane_views`] alias. + /// + /// Read it, never infer it from the codec. H.264 in this program is the 8-bit + /// 4:2:0 envelope, so its frames are always NV12 — but an H.265 session's format + /// is the STREAM's (Main → NV12, Main 10 → P010, RExt 4:4:4 → the two-plane 4:4:4 + /// formats) and can change mid-stream when the host renegotiates. The presenter + /// derives the CSC pass's bit depth and MSB-packing factor from this; an assumed + /// 8 bits over a P010 surface decodes correctly and displays wrong, which is the + /// failure class this program exists to refuse. + pub vk_format: RawVkFormat, + /// Per-plane views (raw `VkImageView`s) in the formats pf-vkdecode resolves for + /// [`Self::vk_format`] — `R8`/`R8G8` for the 8-bit families, `R10X6`/`R10X6G10X6` + /// for the 10-bit ones — the presenter's planar CSC sampling contract. + pub plane_views: [u64; 2], + pub layer: u32, + /// The layout the layer is in when the semaphore signals; the presenter must + /// return it there after sampling (see [`NativeVkLayout`]). + pub layout: NativeVkLayout, + /// Timeline pair (raw `VkSemaphore` + value): pixels are ready when the semaphore + /// reaches the value — the presenter waits it on the GPU (submit wait list), never + /// on the host. + pub semaphore: u64, + pub semaphore_value: u64, + /// The decoder session generation the handles belong to (rides the release token). + pub generation: u64, + /// Display size (the conformance-window crop) — what [`DecodedImage::dimensions`] + /// reports and what the presenter shows. + pub width: u32, + pub height: u32, + /// The image's allocated/coded extent (`>=` display) — the presenter scales its + /// sampling UVs by display/coded per axis or the alignment padding smears into + /// view. That is the 1088-row lesson (field report 2026-07-31): at 1080p the pool + /// is 1088 rows tall because 1080 is not a multiple of 16, encoders fill the extra + /// rows by replicating the picture's last line, and sampling `0..1` without the + /// ratio smears that line over the bottom of the image. Same class as the D3D11VA + /// source-rect clamp in `crate::video_d3d11`, which shows as a green bar there only + /// because DXVA padding is left uninitialized rather than replicated. + pub coded_width: u32, + pub coded_height: u32, + /// Crop origin within the coded picture. Punktfunk hosts emit origin crops only; + /// the presenter's UV-scale path assumes (0,0) and a nonzero origin would show the + /// wrong window — carried so that assumption is checkable, not silent. + pub crop_x: u32, + pub crop_y: u32, + /// Colour signalling, read from the SPS active for THIS picture (the H.264/H.265 + /// VUI → H.273 code points, with E.2.1's "unspecified" inference where the VUI is + /// silent) — per frame, because the host switches HDR in-band; "unspecified" + /// resolves to the BT.709-limited SDR default (`csc_rows`' documented fallback). + pub color: ColorDesc, + /// IDR — the stream's re-anchor point (the pump's post-loss resume signal). Truly + /// IDR: on H.265 a CRA/BLA does NOT set this (pf-bitstream keys it off the NALU + /// type), which costs nothing against punktfunk hosts — they emit IDR-only + /// re-entry points — and is the conservative direction anyway, since a CRA's + /// leading pictures may be undecodable. + pub keyframe: bool, + pub poc: i32, + /// What this frame's AU said about intra-refresh RECOVERY, read out of the + /// bitstream's own recovery point SEI (pf-vkdecode's `RecoveryWatch`). + /// + /// [`Self::keyframe`] cannot answer for an intra-refresh session — the wave + /// never emits an IDR — so without this the pump has no clean point to lift a + /// post-loss freeze on and holds the last good picture until its 500 ms + /// backstop forces the very IDR the wave exists to avoid. The wire's + /// `USER_FLAG_RECOVERY_POINT` says the same thing when the host sets it, which + /// only one of the three wave-running encoder backends does (Linux + /// libav-NVENC); this is the same fact taken from the stream instead of from + /// the host, and it cannot be lost separately from the picture. Fed to + /// [`ReanchorGate::on_local_recovery`](punktfunk_core::reanchor::ReanchorGate::on_local_recovery). + pub recovery: punktfunk_core::reanchor::LocalRecovery, + /// This picture's position in DECODE order (pf-vkdecode's strictly increasing + /// per-session ordinal). Delivery order is not decode order: after a failed AU + /// the H.265 decoder flushes its DPB, handing back every buffered picture at + /// once — pictures decoded BEFORE the loss, carrying the recovery marks of the + /// wave they were decoded in. Arriving after the pump armed its freeze, those + /// marks would lift it on a heal that completed before the loss. The pump + /// stamps this ordinal at every arm and ignores [`Self::recovery`] from + /// anything older. + pub decode_order: u64, + /// Sends the release token on drop — see [`NativeReleaseGuard`]. + pub guard: NativeReleaseGuard, +} + impl DecodedImage { - /// Whether the frame is an intra keyframe — see [`frame_is_keyframe`]. The pump uses - /// this as the stream's re-anchor signal after a loss. + /// Whether the frame is an intra keyframe (IDR) — the pump's re-anchor signal after + /// a loss. + /// + /// Every rung answers this from the BITSTREAM now (pf-bitstream's NALU/OBU walk), + /// which is both earlier than a decoder's own flag and — unlike libavcodec's + /// `AV_FRAME_FLAG_KEY`, which this used to be read from — not the whole story: + /// libavcodec flags key only for a true IDR, never for an *intra-refresh recovery + /// point* (H.264 needs `recovery_frame_cnt == 0`; HEVC clears the flag on every + /// non-IRAP frame regardless of the recovery-point SEI). An intra-refresh host + /// (NVENC/AMF/QSV) heals the picture over N P-frames and flags nothing, so this + /// alone would freeze the pump until `session.rs`'s `REANCHOR_FREEZE_MAX` backstop + /// forced a real IDR — the very IDR the wave exists to avoid. That is what + /// [`Self::local_recovery`] answers, from the SEI, and it is one of the things the + /// native rungs bought. pub fn is_keyframe(&self) -> bool { match self { DecodedImage::Cpu(f) => f.keyframe, #[cfg(target_os = "linux")] - DecodedImage::Dmabuf(f) => f.keyframe, - DecodedImage::VkFrame(f) => f.keyframe, + DecodedImage::NativeDmabuf(f) => f.keyframe, #[cfg(windows)] DecodedImage::D3d11(f) => f.keyframe, #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] DecodedImage::PyroWave(f) => f.keyframe, + DecodedImage::NativeVk(f) => f.keyframe, + } + } + + /// What the decoder's OWN bitstream parser saw about an intra-refresh heal on + /// this frame's AU — the recovery point SEI, which no platform decoder exposes. + /// + /// Only the rungs with their OWN parser can answer: a platform decoder parses the + /// SEI internally and surfaces nothing of it (MediaCodec, VideoToolbox — and + /// libavcodec, whose `AV_FRAME_FLAG_KEY` is IDR-only, back when it was a rung here). + /// That is the native Vulkan rung and — since + /// M8 — the CPU rung's H.264 leg, which plans every AU with the same `H264Planner` + /// and folds the SEI with the same `RecoveryWatch`. Everyone else reports + /// [`LocalRecovery::NONE`](punktfunk_core::reanchor::LocalRecovery::NONE) and + /// the pump's re-anchor behaviour on those lanes is byte-for-byte what it was. + /// + /// ⚠ The CPU rung reports no [`Self::decode_order`], so its mark cannot be dated + /// against the pump's arm the way the native rung's is. It does not need to be: + /// openh264 is one-AU-in, at-most-one-picture-out with no DPB flush that replays + /// pictures decoded before a loss, which is the only thing that ordinal defends + /// against. + pub fn local_recovery(&self) -> punktfunk_core::reanchor::LocalRecovery { + match self { + DecodedImage::NativeVk(f) => f.recovery, + DecodedImage::Cpu(f) => f.recovery, + _ => punktfunk_core::reanchor::LocalRecovery::NONE, + } + } + + /// This frame's position in DECODE order, where the lane knows one — see + /// [`NativeVkFrame::decode_order`]. `None` everywhere else, which is what the + /// pump reads as "this lane reports no local recovery either, so there is + /// nothing to date-stamp". + pub fn decode_order(&self) -> Option { + match self { + DecodedImage::NativeVk(f) => Some(f.decode_order), + _ => None, } } @@ -182,29 +575,134 @@ impl DecodedImage { match self { DecodedImage::Cpu(f) => (f.width, f.height), #[cfg(target_os = "linux")] - DecodedImage::Dmabuf(f) => (f.width, f.height), - DecodedImage::VkFrame(f) => (f.width, f.height), + DecodedImage::NativeDmabuf(f) => (f.width, f.height), #[cfg(windows)] DecodedImage::D3d11(f) => (f.width, f.height), #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] DecodedImage::PyroWave(f) => (f.width, f.height), + DecodedImage::NativeVk(f) => (f.width, f.height), } } } -/// RGBA pixels for `GdkMemoryTexture` (which takes a stride). -pub struct CpuFrame { +/// One software-decoded picture as 8-bit 4:2:0 PLANES (M8) — Y, Cb and Cr back to back +/// in one allocation, every plane tightly packed at its own width. +/// +/// "Tightly packed" is a load-bearing invariant, not a convenience: the presenter uploads +/// the buffer with a single `copy_nonoverlapping` and three `vkCmdCopyBufferToImage` +/// regions with `bufferRowLength = 0`, so a padded row here would shear the picture. The +/// decoders' own strides (openh264 pads for SIMD, dav1d aligns) are undone once, in +/// [`Self::from_i420`], which is also the only copy this rung makes per frame — where the +/// old RGBA path made a full swscale conversion pass and then handed over 4 bytes per +/// pixel instead of 1.5. +/// +/// Colour is NOT applied here. The planes carry the stream's own Y′CbCr and `color` +/// carries what the bitstream said about it; the presenter's planar CSC shader converts +/// with [`csc_rows`], the same coefficients every hardware rung's frames go through. That +/// is the whole point of the milestone: there is no second CSC implementation on this +/// lane to get the matrix or the range wrong. +pub struct CpuPlanarFrame { pub width: u32, pub height: u32, - /// RGBA row stride in bytes (≥ width*4 — swscale pads rows for SIMD). - pub stride: usize, - pub rgba: Vec, - /// Signaling of the source frame. swscale already undid the YUV matrix + range (the - /// pixels are full-range RGB), but a PQ/BT.2020 stream keeps its transfer + primaries - /// baked in — the presenter tags the texture so GTK tone-maps it. + /// Y, then Cb, then Cr — see [`Self::plane`]. + data: Vec, + /// Byte offset of each plane's first row in [`Self::data`]. + offsets: [usize; 3], + /// Signalling of the source frame, read from the bitstream (not from the decoder — + /// see `video_software`'s module docs). Drives the CSC matrix/range AND, for a PQ + /// stream, the presenter's tone-map mode. pub color: ColorDesc, - /// Intra keyframe (IDR/I) — the pump's post-loss re-anchor signal. See [`VkVideoFrame`]. + /// Intra keyframe (IDR) — the pump's post-loss re-anchor signal. See + /// [`DecodedImage::is_keyframe`]. pub keyframe: bool, + /// What this frame's AU said about intra-refresh RECOVERY — the same + /// `pf-vkdecode` [`RecoveryWatch`](pf_vkdecode::RecoveryWatch) fold the native rung + /// runs, over the same `AuPlan`. [`Self::keyframe`] cannot answer for an + /// intra-refresh session (the wave emits no IDR), so without this the pump freezes + /// until its 500 ms backstop forces the very IDR the wave exists to avoid. + /// + /// H.264 only: AV1 carries no equivalent SEI (see `video_software`'s AV1 leg), so + /// that half reports [`LocalRecovery::NONE`](punktfunk_core::reanchor::LocalRecovery) + /// and behaves exactly as it did. + pub recovery: punktfunk_core::reanchor::LocalRecovery, +} + +impl CpuPlanarFrame { + /// Chroma plane size for 4:2:0, rounding UP — an odd luma dimension still has a + /// chroma sample covering its last row/column, and rounding down would drop it. + pub fn chroma_dims(width: u32, height: u32) -> (u32, u32) { + (width.div_ceil(2), height.div_ceil(2)) + } + + /// Plane `i` (0 = Y, 1 = Cb, 2 = Cr), tightly packed. + pub fn plane(&self, i: usize) -> &[u8] { + let (w, h) = self.plane_dims(i); + let start = self.offsets[i]; + &self.data[start..start + (w * h) as usize] + } + + /// Plane `i`'s size in samples — `(width, height)` for luma, the 4:2:0 halves for + /// chroma. The presenter sizes its plane images from this. + pub fn plane_dims(&self, i: usize) -> (u32, u32) { + if i == 0 { + (self.width, self.height) + } else { + Self::chroma_dims(self.width, self.height) + } + } + + /// Copy a decoder's strided I420 output into one tightly-packed allocation. + /// + /// Refuses rather than truncates: a plane the decoder reported shorter than its own + /// geometry means the decoder and we disagree about the picture, and reading the rows + /// that ARE there would produce a plausible-looking picture over uninitialized + /// memory. + pub(crate) fn from_i420( + width: u32, + height: u32, + planes: [&[u8]; 3], + strides: [usize; 3], + color: ColorDesc, + keyframe: bool, + recovery: punktfunk_core::reanchor::LocalRecovery, + ) -> Result { + anyhow::ensure!(width > 0 && height > 0, "empty picture {width}x{height}"); + let (cw, ch) = Self::chroma_dims(width, height); + let dims = [(width, height), (cw, ch), (cw, ch)]; + let total: usize = dims.iter().map(|(w, h)| *w as usize * *h as usize).sum(); + let mut data = vec![0u8; total]; + let mut offsets = [0usize; 3]; + let mut at = 0usize; + for i in 0..3 { + let (w, h) = (dims[i].0 as usize, dims[i].1 as usize); + anyhow::ensure!( + strides[i] >= w, + "plane {i}: stride {} is narrower than {w} samples", + strides[i] + ); + anyhow::ensure!( + planes[i].len() >= (h - 1) * strides[i] + w, + "plane {i}: decoder reported {} bytes for {w}x{h} at stride {}", + planes[i].len(), + strides[i] + ); + offsets[i] = at; + for row in 0..h { + let src = row * strides[i]; + data[at..at + w].copy_from_slice(&planes[i][src..src + w]); + at += w; + } + } + Ok(CpuPlanarFrame { + width, + height, + data, + offsets, + color, + keyframe, + recovery, + }) + } } /// A decoded frame still on the GPU: dmabuf fds + plane layout for @@ -222,7 +720,8 @@ pub struct DmabufFrame { /// Signaling of the source frame — drives the `GdkDmabufTexture` color state (BT.709 /// narrow for SDR, BT.2020 PQ for an HDR stream). pub color: ColorDesc, - /// Intra keyframe (IDR/I) — the pump's post-loss re-anchor signal. See [`VkVideoFrame`]. + /// Intra keyframe (IDR/I) — the pump's post-loss re-anchor signal. See + /// [`DecodedImage::is_keyframe`]. pub keyframe: bool, pub guard: DrmFrameGuard, } @@ -234,44 +733,120 @@ pub struct DmabufPlane { pub stride: u32, } -/// Owns the mapped DRM-PRIME `AVFrame` (which in turn references the VAAPI surface). -/// Dropping it releases the surface back to the decoder pool and closes the fds. -pub struct DrmFrameGuard(pub(crate) *mut ffmpeg::ffi::AVFrame); -// SAFETY: the guard owns one `AVFrame` and frees it exactly once in `Drop`. libav's buffer -// refcounts are atomic and its hwframe pool is internally locked, so releasing the frame — and with -// it the VAAPI surface, back to the decoder's pool — from a different thread than the one that -// mapped it is sound. That is the whole point here: the guard is handed to GTK and dropped on the -// main thread while the pump thread keeps decoding. Moved, never shared; deliberately NOT `Sync`. -unsafe impl Send for DrmFrameGuard {} - -impl Drop for DrmFrameGuard { - fn drop(&mut self) { - // SAFETY: `self.0` is the one `AVFrame` this guard owns; `av_frame_free` releases it - // exactly once (this `Drop` runs once) and nulls the pointer through the `&mut`. - unsafe { ffmpeg::ffi::av_frame_free(&mut self.0) }; - } -} +/// Keeps a decoded surface alive until the consumer's GPU reads are done: dropping +/// it releases the surface back to its decoder's pool and closes the fds. +/// +/// The consumer treats this as opaque — the presenter dups every dmabuf fd it +/// imports and simply holds the guard until its fence has been waited. +/// +/// It was an ENUM until M10, because libavcodec's VAAPI hwaccel handed over a mapped +/// `AVFrame` while the native rung (`video_vaapi_native`, M6) owns a `VASurface` from its +/// own pool — widening this seam is what let the native rung exist alongside it. With the +/// FFmpeg rung deleted there is one owner left, so the enum collapses to the newtype it +/// started as, and the `unsafe impl Send` the `AVFrame` pointer needed goes with it: +/// [`VaFrameGuard`](crate::video_vaapi_native::VaFrameGuard) is owned fds plus an +/// `mpsc::Sender`, i.e. `Send` on its own terms. +#[cfg(target_os = "linux")] +pub struct DrmFrameGuard( + /// Nothing ever READS this — the whole type is its `Drop`, which closes the exported + /// PRIME fds and returns the surface to the decoder's pool. `dead_code` is answered + /// here rather than by removing the field (that would release the surface at + /// construction) or by making the type an alias (that would hand the presenter a + /// pf-vaadec-shaped name for something it must treat as opaque). + #[allow(dead_code)] + pub(crate) crate::video_vaapi_native::VaFrameGuard, +); enum Backend { - Vulkan(VulkanDecoder), + /// Native Vulkan Video H.264/HEVC/AV1 (pf-vkdecode) on the presenter's device — + /// auto's TOP rung on both desktop OSes, for all three codecs — every leg + /// has hardware parity against libavcodec (see this module's evidence table) — also + /// pinnable by name (`PUNKTFUNK_DECODER=native-vulkan`); see [`native_vulkan_gate`]. + /// The negotiated codec picks the decoder once, at construction; everything else + /// about this backend is codec-agnostic. + /// Boxed: the decoder (planner + shipped-frame ledger) dwarfs the other variants, + /// same as PyroWave below. + NativeVulkan(Box), + /// Native VAAPI (`pf-vaadec` + `video_vaapi_native`) — M6's replacement for + /// libavcodec's VAAPI hwaccel, and since M10 the only VAAPI rung: libva driven + /// straight from pf-bitstream plans, dlopen'd, exporting the same DRM-PRIME dmabufs. + /// Reachable by pin (`PUNKTFUNK_DECODER=native-vaapi`) and by `auto` in the vendor + /// order. ⚠ This rung has decoded NOTHING on hardware ([`native_evidence`]) — `auto` + /// runs it where the alternative below it is the CPU, and yields to native Vulkan + /// Video where that rung is proven for the codec and usable on the device + /// ([`native_rung_admitted`], which is the Intel/unknown arm). Every session that + /// lands on it says so at `warn`. Errors ride the SAME streak/demotion machinery as + /// every other hardware rung. + /// Boxed: the decoder (two planners, a display and a surface pool) dwarfs the + /// other variants. #[cfg(target_os = "linux")] - Vaapi(VaapiDecoder), + NativeVaapi(Box), + /// Native D3D11VA (`pf-dxvadec` + `video_d3d11_native`) — M5's replacement for + /// libavcodec's D3D11VA hwaccel, and since M10 the only DXVA rung: + /// `ID3D11VideoDecoder` driven from pf-bitstream plans, filling the shareable-RGBA + /// hand-off ring in `crate::video_d3d11`. + /// Reachable by pin (`PUNKTFUNK_DECODER=native-d3d11va`) and by `auto` in the vendor + /// order: its H.264/H.265 legs have hardware parity + a soak (M5); its AV1 leg has + /// decoded nothing anywhere and runs with the warning [`log_rung`] emits. Errors + /// ride the SAME streak/demotion machinery as every other hardware rung. + /// Boxed: the decoder (two planners plus a session) dwarfs the other variants. #[cfg(windows)] - D3d11va(crate::video_d3d11::D3d11vaDecoder), - /// PyroWave (wired-LAN wavelet codec): pyrowave compute on the presenter's device, - /// no FFmpeg involvement (Linux + Windows — same Vulkan presenter on both). No demotion + NativeD3d11va(Box), + /// PyroWave (wired-LAN wavelet codec): pyrowave compute on the presenter's device + /// (Linux + Windows — same Vulkan presenter on both). No demotion /// rung — there is no other decoder for it. /// Boxed: the decoder (pinned create-info hold + plane ring) dwarfs the other variants. #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] PyroWave(Box), + /// The CPU rung (M8: openh264 / rav1d). Last in every ladder, so it + /// never demotes — and the only rung that can fail to EXIST for a codec, which is a + /// different answer from failing to decode: see [`last_rung_verdict`]. Software(SoftwareDecoder), } +/// The picture shape the host resolved in its Welcome, before a single AU arrives. +/// +/// The in-band SPS stays authoritative — this is the NEGOTIATED answer, which is what +/// makes it available at decoder-construction time. It exists so a backend whose +/// support for a shape is device-dependent can refuse BEFORE it is chosen, where the +/// ladder's fall-through to the next rung is a plain construction failure, instead of +/// discovering it at the first decode where the only exit is an error-streak demotion +/// PAST that rung. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct StreamFormat { + /// `chroma_format_idc` — [`punktfunk_core::quic::CHROMA_IDC_420`] (1) or + /// [`punktfunk_core::quic::CHROMA_IDC_444`] (3). An older host that omitted it + /// reads as 4:2:0, never 0. + pub chroma_format_idc: u8, + /// Bits per component: 8, or 10 for a Main10/HDR session (an older host reads 8). + pub bit_depth: u8, +} + +impl StreamFormat { + /// The 8-bit 4:2:0 envelope — what every H.264 session is, and what an older + /// host's Welcome decodes to. + pub const SDR_420_8: StreamFormat = StreamFormat { + chroma_format_idc: punktfunk_core::quic::CHROMA_IDC_420, + bit_depth: 8, + }; + + /// `bit_depth` as the `bit_depth_luma_minus8` the H.265 SPS (and pf-vkdecode's + /// profile key) speaks, or `None` for a depth outside the 8/10 envelope — which + /// is itself a refusal, not a "probe skipped". + pub(crate) fn bit_depth_minus8(self) -> Option { + self.bit_depth.checked_sub(8) + } +} + pub struct Decoder { backend: Backend, - /// The negotiated codec (from the host's Welcome), so a mid-session VAAPI→software demotion - /// rebuilds the software decoder for the SAME codec. - codec_id: ffmpeg::codec::Id, + /// The negotiated codec as the WIRE states it ([`punktfunk_core::quic`]'s `CODEC_*` + /// bit, from the host's Welcome) — the ONE codec vocabulary this crate speaks since + /// M10 deleted `ffmpeg::codec::Id` along with the rungs that needed it. Every rung + /// map ([`native_codec`], [`native_d3d11_codec`], [`native_vaapi_codec`]), the + /// evidence table and the software rung's refusal are keyed on it, so a mid-session + /// demotion rebuilds for the SAME codec by construction rather than by translation. + wire_codec: u8, /// Consecutive hardware decode errors (Vulkan or VAAPI) — a single transient failure /// (e.g. a reference-missing frame after packet loss) shouldn't cost the whole /// session its hardware decoder. @@ -286,6 +861,34 @@ pub struct Decoder { /// The pump drains it and asks the host — under the infinite GOP there is no periodic /// keyframe, so a rebuilt/erroring decoder would otherwise stay gray/frozen forever. want_keyframe: bool, + /// The CURRENT backend has delivered at least one frame. A backend that never did + /// is one the session never actually had, so its error streak must not cost the + /// session the rung BELOW it. Reset on every backend swap. + /// + /// ⚠ Read by nothing but [`Decoder::decode_frame`]'s streak accounting since M10: + /// the arm that consumed it — "a native Vulkan rung that never delivered demotes to + /// FFmpeg-Vulkan rather than past it" — is gone with FFmpeg-Vulkan itself, and the + /// property it protected now holds structurally, because the rung directly below + /// native Vulkan IS the next candidate the walk tries. + delivered: bool, + /// The presenter's device, kept so the demotion walk can build a native Vulkan + /// decoder mid-stream. Cloned once per session; its handles outlive every pump + /// (see [`VulkanDecodeDevice`]). + vk: Option, + /// The negotiated picture shape, kept for the same reason `vk` is: the + /// demotion walk can build a NATIVE rung mid-stream, and every native constructor + /// takes it as its device probe ([`StreamFormat`]). + stream: StreamFormat, + /// Which hardware rungs this session has actually RUN — [`RUNG_BIT_NATIVE_VULKAN`] + /// and [`RUNG_BIT_NATIVE_PLATFORM`], set the moment a rung is installed. + /// + /// It is what makes the demotion walk TERMINATE now that two native rungs can + /// demote into each other (the ladder is `native → other native → software`, and + /// the two orders are opposite per vendor — so + /// without this a Vulkan⇄platform pair could hand the session back and forth + /// forever, one error streak at a time, and never reach the CPU rung that would at + /// least show a picture). A rung already entered is never re-entered. + entered_rungs: u8, /// The presenter has the win32 external-memory import path, so D3D11VA frames can reach /// the screen — kept for the mid-session Vulkan→D3D11VA demotion rung (the Windows /// analog of Linux's Vulkan→VAAPI rung). @@ -300,6 +903,25 @@ pub struct Decoder { d3d11_hdr10: bool, } +/// The native VULKAN rung ran this session — see [`Decoder::entered_rungs`]. +const RUNG_BIT_NATIVE_VULKAN: u8 = 1 << 0; +/// The native PLATFORM rung (VAAPI on Linux, D3D11VA on Windows) ran this session. +const RUNG_BIT_NATIVE_PLATFORM: u8 = 1 << 1; + +/// Which [`Decoder::entered_rungs`] bit a backend claims — 0 for the rungs that cannot +/// be a demotion TARGET twice (software is terminal; PyroWave never demotes at all), so +/// they need no bookkeeping. +fn rung_bit(backend: &Backend) -> u8 { + match backend { + Backend::NativeVulkan(_) => RUNG_BIT_NATIVE_VULKAN, + #[cfg(target_os = "linux")] + Backend::NativeVaapi(_) => RUNG_BIT_NATIVE_PLATFORM, + #[cfg(windows)] + Backend::NativeD3d11va(_) => RUNG_BIT_NATIVE_PLATFORM, + _ => 0, + } +} + /// Demote a hardware backend (Vulkan→VAAPI/D3D11VA, VAAPI/D3D11VA→software) only after /// this many consecutive decode errors; a lone transient error just re-requests an IDR /// and keeps the hardware decoder. @@ -312,120 +934,556 @@ const VAAPI_DEMOTE_AFTER: u32 = 3; /// software before the first requested IDR could even arrive. const HW_DEMOTE_MIN_STREAK: std::time::Duration = std::time::Duration::from_millis(1000); -/// Map a negotiated `quic` codec bit to the FFmpeg decoder id the client opens. -pub fn ffmpeg_codec_id(wire: u8) -> ffmpeg::codec::Id { +/// May a successful `decode` answer CLEAR the demotion error streak? +/// +/// The streak is the hardware rungs' only escape hatch, and clearing it is a +/// claim: *this decoder is working*. A delivered frame proves that outright. So +/// does a clean `Ok(None)` — the decoder ran and had nothing to object to (it +/// buffered, or skipped an H.265 RASL picture after an open-GOP join). +/// +/// What proves nothing is the third `Ok(None)`: the native rung's CONCEALMENT +/// answer, where the plan needed a substitute for something lost and the picture +/// was released unshown. That is deliberately not an `Err` — stream damage is not +/// a decoder fault, and three of them in a second must not demote the rung on +/// exactly the lossy links it exists to diagnose — but "not an error" was silently +/// read as "a success", and clearing on it is the dangerous half of that: +/// +/// * a driver failing every OTHER AU on a lossy link has its `Err`s zeroed by the +/// concealment between them and never reaches [`VAAPI_DEMOTE_AFTER`]; +/// * and a rung answering concealment FOREVER — a host framing regression putting +/// two pictures in one AU makes every AU conceal, and unlike a reference gap it +/// does not self-heal at an IDR — holds a frozen last-good frame with no escape +/// at all, where before this milestone the same stream demoted to a rung that +/// ignores AU boundaries and showed a picture. +/// +/// Leaving the streak untouched costs nothing on a healthy link: one damaged AU +/// between good frames is cleared by the next good frame. +fn clears_demotion_streak(delivered: bool, concealed: bool) -> bool { + delivered || !concealed +} + +/// `VK_VIDEO_CODEC_OPERATION_DECODE_H264_BIT_KHR` — the raw flag bit within +/// [`VulkanDecodeDevice::decode_video_caps`] (this crate stays ash-free). +const VIDEO_CODEC_OP_DECODE_H264: u32 = 0x0000_0001; +/// `VK_VIDEO_CODEC_OPERATION_DECODE_H265_BIT_KHR` — its H.265 sibling. +const VIDEO_CODEC_OP_DECODE_H265: u32 = 0x0000_0002; + +/// `VK_VIDEO_CODEC_OPERATION_DECODE_AV1_BIT_KHR`. The Deck's VanGogh advertises +/// it alongside H.264/H.265/VP9; it is what [`av1_hardware_decodable`] reads and, +/// since M7, the caps bit [`native_codec`] demands for an AV1 session. +const VIDEO_CODEC_OP_DECODE_AV1: u32 = 0x0000_0004; + +/// The native decoder for a negotiated wire codec, plus the +/// `VkVideoCodecOperationFlagBitsKHR` the presenter's decode family must advertise +/// for it — or `None` for a codec pf-vkdecode cannot decode natively. +/// +/// The two are returned together on purpose: "which decoder" and "which caps bit" +/// are one fact, and splitting them is how a gate ends up admitting HEVC on an +/// H.264-only decode family (`vkCreateVideoSessionKHR` for a codec operation the +/// family cannot run is undefined behaviour, not an error). +/// +/// ⚠ Being here is "pf-vkdecode has a decoder", NOT "the automatic ladder may pick +/// it": [`native_vulkan_gate`] holds that decision, and it reads this map for the +/// codec/caps pair only. +/// +/// The key is the WIRE bit ([`punktfunk_core::quic`]'s `CODEC_*`), which since M10 is +/// the only codec vocabulary in this crate — the `ffmpeg::codec::Id` it used to be +/// died with the last libavcodec rung. +fn native_codec(wire: u8) -> Option<(NativeCodec, u32)> { match wire { - punktfunk_core::quic::CODEC_H264 => ffmpeg::codec::Id::H264, - punktfunk_core::quic::CODEC_AV1 => ffmpeg::codec::Id::AV1, - _ => ffmpeg::codec::Id::HEVC, + punktfunk_core::quic::CODEC_H264 => Some((NativeCodec::H264, VIDEO_CODEC_OP_DECODE_H264)), + punktfunk_core::quic::CODEC_HEVC => Some((NativeCodec::H265, VIDEO_CODEC_OP_DECODE_H265)), + punktfunk_core::quic::CODEC_AV1 => Some((NativeCodec::Av1, VIDEO_CODEC_OP_DECODE_AV1)), + _ => None, } } -/// Select a decoder for `codec_id` that can actually drive `hw_pix_fmt` through -/// `hw_device_ctx` — the open-time capability check every hardware backend needs. +/// The native DXVA decoder for a negotiated wire codec, or `None` for one pf-dxvadec +/// cannot decode. No caps bit accompanies it (unlike [`native_codec`]): DXVA advertises +/// support as a profile GUID on the adapter, which +/// [`crate::video_d3d11_native::NativeD3d11Decoder::new`] checks directly against the +/// device it is about to build on — there is no device-level "which codecs" flag to +/// consult first. +#[cfg(windows)] +fn native_d3d11_codec(wire: u8) -> Option { + match wire { + punktfunk_core::quic::CODEC_H264 => Some(pf_dxvadec::Codec::H264), + punktfunk_core::quic::CODEC_HEVC => Some(pf_dxvadec::Codec::H265), + // AV1 (M7). It was not a widening of what this client can decode — the FFmpeg + // D3D11VA rung of the day already decoded AV1 Profile 0 through the same profile + // GUID — but the native rung had to cover it, or dropping FFmpeg (M10, this + // milestone) would have dropped a codec. + punktfunk_core::quic::CODEC_AV1 => Some(pf_dxvadec::Codec::Av1), + _ => None, + } +} + +/// The native VAAPI decoder for a negotiated wire codec, or `None` for one pf-vaadec +/// cannot decode. Like its DXVA twin there is no caps bit to consult first: VAAPI +/// advertises support as a profile/entrypoint pair on the DISPLAY, which +/// [`crate::video_vaapi_native::NativeVaapiDecoder::new`] queries on the device it is +/// about to build on. +#[cfg(target_os = "linux")] +fn native_vaapi_codec(wire: u8) -> Option { + match wire { + punktfunk_core::quic::CODEC_H264 => Some(pf_vaadec::Codec::H264), + punktfunk_core::quic::CODEC_HEVC => Some(pf_vaadec::Codec::H265), + // AV1 (M7) — same reasoning as the DXVA map above. + punktfunk_core::quic::CODEC_AV1 => Some(pf_vaadec::Codec::Av1), + _ => None, + } +} + +/// One NATIVE decode rung, named so the evidence table and the admission rule can talk +/// about rungs without naming a [`Backend`] variant (whose set is per-platform). /// -/// `avcodec_find_decoder(id)` is NOT that: it returns the registry's FIRST decoder for -/// the id, and upstream orders the native `av1` decoder LAST on purpose ("hwaccel hooks -/// only, so prefer external decoders" — allcodecs.c), behind libdav1d/libaom. The ID -/// lookup therefore hands every AV1 session a pure software decoder that silently -/// ignores `hw_device_ctx` and never calls `get_format`; each frame then fails the -/// backend's hw-format guard and the session burns the demotion ladder MID-STREAM -/// (~1 s per rung — field-logged as 68 Vulkan fails → D3D11VA → 102 fails → software, -/// ~3 s of black) instead of failing here at open in milliseconds. H.264/HEVC never hit -/// this only because their native decoders happen to be registered first. -/// -/// The walk mirrors what `avcodec_find_decoder` would do, restricted to decoders whose -/// `avcodec_get_hw_config` advertises the wanted surface via -/// `AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX` — registry order still wins among those, -/// so H.264/HEVC keep selecting exactly the decoder they always did. The error names -/// the decoders that WERE found, so a log reader can tell "this build has no AV1 -/// hwaccel at all" from "no AV1 decoder exists, period". -pub(crate) fn find_hw_decoder( - codec_id: ffmpeg::codec::Id, - hw_pix_fmt: ffmpeg::ffi::AVPixelFormat, -) -> Result<*const ffmpeg::ffi::AVCodec> { - use ffmpeg::ffi; - let want: ffi::AVCodecID = codec_id.into(); - let mut found: Vec = Vec::new(); - // SAFETY: `av_codec_iterate` walks libav's static codec registry (`opaque` is its - // cursor) and returns static `AVCodec`s; `avcodec_get_hw_config` only reads the - // codec's own static hw-config table, NULL-terminated by returning null past the end. - unsafe { - let mut opaque = std::ptr::null_mut(); - loop { - let codec = ffi::av_codec_iterate(&mut opaque); - if codec.is_null() { - break; - } - if (*codec).id != want || ffi::av_codec_is_decoder(codec) == 0 { - continue; - } - for i in 0.. { - let cfg = ffi::avcodec_get_hw_config(codec, i); - if cfg.is_null() { - break; - } - if (*cfg).methods & ffi::AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX as i32 != 0 - && (*cfg).pix_fmt == hw_pix_fmt - { - return Ok(codec); - } - } - found.push( - std::ffi::CStr::from_ptr((*codec).name) - .to_string_lossy() - .into_owned(), - ); +/// The CPU rung is here for completeness of the table only — see [`native_evidence`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeRung { + /// pf-vkdecode on the presenter's device (`video_vk_native`). + Vulkan, + /// pf-dxvadec driving `ID3D11VideoDecoder` (`video_d3d11_native`, Windows). + D3d11va, + /// pf-vaadec driving a dlopen'd libva (`video_vaapi_native`, Linux). + Vaapi, + /// openh264 + rav1d (`video_software`). + Software, +} + +impl NativeRung { + /// The name this rung goes by in logs and in `PUNKTFUNK_DECODER` — the same strings + /// the `stats:` decode-path tag uses, so a log line and a stats line name one thing. + pub fn name(self) -> &'static str { + match self { + NativeRung::Vulkan => "native-vulkan", + NativeRung::D3d11va => "native-d3d11va", + NativeRung::Vaapi => "native-vaapi", + NativeRung::Software => "software", } } - if found.is_empty() { - bail!("no {codec_id:?} decoder in this FFmpeg build"); - } - bail!( - "no {codec_id:?} decoder in this FFmpeg build can drive {hw_pix_fmt:?} via \ - hw_device_ctx (found: {})", - found.join(", ") - ); } -/// The name of a registry `AVCodec` (`(*codec).name`), owned — the field every decode -/// log carries so `decoder="av1"` vs `decoder="libdav1d"` is one glance, not a debugger. +/// What HARDWARE has actually decoded a frame on a given rung/codec pair — the fact M9's +/// default flip turns on, written down where it cannot rot. /// -/// # Safety -/// `codec` must point to a registered `AVCodec` (their `name` is a static NUL-terminated -/// string, valid for the process). -pub(crate) unsafe fn codec_name(codec: *const ffmpeg::ffi::AVCodec) -> String { - // SAFETY: caller guarantees a registered AVCodec; `name` is its static C string. - unsafe { - std::ffi::CStr::from_ptr((*codec).name) - .to_string_lossy() - .into_owned() +/// This is deliberately not a confidence score or a tier. It answers exactly one +/// question — *has a real GPU ever produced a picture through this code path* — because +/// that is the question the admission rule needs and the one a support engineer reading +/// `hardware_verified=false` on a session's "decode rung active" line needs. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RungEvidence { + /// A real device has decoded frames through this rung/codec pair and the result was + /// checked (frame-hash parity, a soak, or both). + pub verified: bool, + /// WHAT hardware, in one line — or, when `verified` is false, why there is none. Goes + /// verbatim into the session log so a report carries its own provenance. + pub note: &'static str, +} + +/// The evidence table (this module's docs hold the readable copy), keyed by rung and WIRE +/// codec. +/// +/// Wire bits rather than `ffmpeg::codec::Id` — deliberately, back when there still was such +/// a vocabulary here: this is a fact about punktfunk's own decode lanes, and keying it on +/// FFmpeg's ids would have meant re-keying it at M10. It did not have to be re-keyed. +/// +/// An unknown codec for a rung answers `verified: false` — the safe direction: a rung +/// grows a codec leg before anyone runs it on hardware, and the default must be "no +/// evidence", not "inherits its neighbour's". +pub fn native_evidence(rung: NativeRung, wire: u8) -> RungEvidence { + use punktfunk_core::quic::{CODEC_AV1, CODEC_H264, CODEC_HEVC}; + let (verified, note) = match (rung, wire) { + (NativeRung::Vulkan, CODEC_H264) => ( + true, + "bit-exact vs libavcodec, 250/250 AUs on three drivers + 92-min soak (M2 WP-D)", + ), + (NativeRung::Vulkan, CODEC_HEVC) => ( + true, + "bit-exact vs libavcodec incl. Main10/4:4:4, three drivers + HDR and Deck legs (M3)", + ), + (NativeRung::Vulkan, CODEC_AV1) => ( + true, + "250/250 bit-identical to libavcodec on an RTX 5070 Ti (M7) - one vendor, no soak", + ), + (NativeRung::D3d11va, CODEC_H264 | CODEC_HEVC) => ( + true, + "frame-hash parity on an RTX 4090 and an AMD iGPU + 30-min soak (M5)", + ), + (NativeRung::D3d11va, CODEC_AV1) => ( + false, + "NEVER decoded a frame on any hardware - wired in M7, the box was unavailable", + ), + (NativeRung::Vaapi, _) => ( + false, + "NEVER decoded a frame on any hardware - no VAAPI device was reachable (M6/M7)", + ), + (NativeRung::Software, CODEC_H264 | CODEC_AV1) => ( + false, + "never run on glass - openh264/rav1d have CPU unit tests only (M8)", + ), + _ => (false, "no hardware run recorded for this rung and codec"), + }; + RungEvidence { verified, note } +} + +/// Can THIS device run the native Vulkan rung for this wire codec at all? +/// +/// [`native_vulkan_gate`] without its `choice` half — the same device facts, asked by the +/// callers that need to know what is BELOW them rather than what they are about to build: +/// [`native_rung_admitted`]'s callers, and (spelled inline, with `"auto"`) the mid-session +/// demotion walk. One function so that "the Vulkan rung is available on this box" cannot +/// come to mean two different things inside one file. +pub fn native_vulkan_usable(wire: u8, video_decode: bool, decode_video_caps: u32) -> bool { + native_vulkan_gate("auto", wire, video_decode, decode_video_caps) +} + +/// May `auto` pick this native rung for this wire codec, given what sits directly BELOW it +/// on THIS device? +/// +/// One sentence: **an unproven rung yields to a proven one, and to nothing else.** +/// +/// * a rung/codec pair WITH hardware evidence is admitted, always — that is what the +/// evidence was collected for; +/// * a pair without it is admitted unless the rung the ladder would fall onto instead is +/// itself verified for this codec AND usable on this device. +/// +/// `below` is that rung, or `None` when nothing usable is left below it (the CPU). It is +/// the CALLER's to compute, because "what is below me" is the vendor order's answer, not +/// this function's: it differs per platform and, on Linux, per vendor id. +/// +/// Where the rule bites, and where it deliberately does not: +/// +/// * **Linux, Intel and every unknown vendor id.** The order is `native-vaapi → +/// native-vk → sw`, so the rung under the never-run pf-vaadec is native Vulkan Video, +/// proven for all three codecs. Barring VAAPI there moves the session ONE rung down onto +/// proven code, so it is barred — and it stays reachable below Vulkan (the same ladder +/// reaches it again if Vulkan can't be built) and by pin. +/// * **Everything else.** Below the unproven rung is the CPU. Trading hardware decode for +/// software decode to avoid an unproven decoder is the worse answer, so those rungs run, +/// with the warning [`log_rung`] emits. That includes Windows Intel/unknown, where the +/// rung below native-d3d11va IS native Vulkan Video on paper: that vendor family is the +/// one thing in this program with a MEASURED wrong-pixel report against Vulkan decode +/// (the B580, see [`Decoder::new`]), and "has never run" is not a reason to move a +/// session onto "known to strobe here". Callers say so where they pass `None`. +/// +/// ⚠ This governs `auto` ONLY. An explicit `PUNKTFUNK_DECODER=` pin bypasses it exactly as +/// it bypasses the vendor order — a pin is how a lab run reaches a rung `auto` will not +/// pick, and taking that away would leave no way to GENERATE the missing evidence. The +/// mid-session demotion walk bypasses it too: there the rung above has already failed +/// repeatedly, so unproven hardware is the only hardware left to try. +pub fn native_rung_admitted(rung: NativeRung, wire: u8, below: Option) -> bool { + native_evidence(rung, wire).verified + || !below.is_some_and(|b| native_evidence(b, wire).verified) +} + +/// The native Vulkan Video admission gate (WP-C of the native-decode program, widened by +/// the 2026-08-05 ladder decision, by M3 WP-2's HEVC wiring and by M7's AV1 wiring): the +/// pf-vkdecode backend engages when `choice` asks for it, by name +/// (`PUNKTFUNK_DECODER=native-vulkan` — `choice` is env-first, so that's what carries it) +/// or as the auto family (`auto`/``/`hardware`), where native is auto's TOP rung for +/// every codec it speaks. +/// +/// A native INIT failure falls through to the platform's own native rung, so admission +/// can never cost a session its decoder at start. A runtime error streak demotes like +/// every hardware rung's streaks do. Every explicit OTHER-backend pin refuses here; the +/// pre-M10 `vulkan` spelling reaches this gate already rewritten to `native-vulkan` +/// ([`migrate_decoder_pref`]), so it admits, which is the point of the migration. +/// +/// Beyond the choice: the negotiated wire codec must be one pf-vkdecode speaks — +/// H.264, H.265 or AV1 ([`native_codec`]) — and the presenter's decode family must +/// advertise THAT codec's decode operation. `video_decode` alone proves the extension +/// stack, never the codec: an AV1-only decode family exists on real hardware, and +/// H.264-only ones are the common case on older silicon. +/// +/// What the gate deliberately does NOT check is the stream's picture SHAPE — that is +/// [`NativeVulkanDecoder::new`]'s construction-time probe, which has the negotiated +/// chroma format and bit depth and can ask the device directly. Keeping it there keeps +/// this decision pure (and CPU-testable) while still refusing before a decoder exists. +fn native_vulkan_gate(choice: &str, wire: u8, video_decode: bool, decode_video_caps: u32) -> bool { + let Some((_, codec_op)) = native_codec(wire) else { + return false; + }; + let chosen = matches!(choice, "native-vulkan" | "auto" | "" | "hardware"); + chosen && video_decode && decode_video_caps & codec_op != 0 +} + +/// The `quic::CODEC_*` bit's human name — for logs, errors and the user-visible +/// reconnect toast. `?` for a bit this build does not know, which is honest: an unknown +/// codec must not print as one of the known ones. +pub fn wire_codec_name(wire: u8) -> &'static str { + match wire { + punktfunk_core::quic::CODEC_H264 => "H.264", + punktfunk_core::quic::CODEC_HEVC => "HEVC", + punktfunk_core::quic::CODEC_AV1 => "AV1", + punktfunk_core::quic::CODEC_PYROWAVE => "PyroWave", + _ => "?", } } -/// The `quic` codec bitfield this client can decode — whatever FFmpeg has a decoder for (HEVC/H.264 -/// always; AV1 when built in). Advertised to the host so it never emits a codec we can't decode. +/// The `quic` codec bits this build can decode ON THE CPU — the ladder's last rung, and +/// therefore the set a session is guaranteed to survive to the end of. +/// +/// One function so the answer cannot drift between the rung that refuses (the software +/// backend's own codec map) and the rule that decides what to reconnect as +/// ([`last_rung_verdict`]). +pub fn software_decodable_codecs() -> u8 { + punktfunk_core::quic::CODEC_H264 | punktfunk_core::quic::CODEC_AV1 +} + +/// What to do when the last rung has no decoder for the session's codec. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LastRungVerdict { + /// Reconnect advertising these caps instead — they are non-empty, and they exclude + /// the codec that just ran out of rungs, so the host must pick something else. + Retry { caps: u8 }, + /// Nothing is left to advertise: every codec this client offered has now exhausted + /// its rungs. Reconnecting would negotiate the same dead end, so the session ends + /// and says why. + Dead, +} + +/// WHY the last rung had no answer — the two diagnoses behind a [`NoSoftwareRung`], and +/// the reason [`last_rung_verdict`] needs more than "a codec failed". +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RungLoss { + /// The CODEC has no CPU rung in this build at all (HEVC). Every hardware rung for it + /// has already failed, so the retry may only offer codecs that DO have a CPU rung — + /// anything else is the same bet that just lost, one session later. + Codec, + /// The codec has a CPU rung; this stream's picture SHAPE is outside it (10-bit, + /// 4:4:4). The hardware rungs are not implicated at all — nothing failed, the CPU + /// decoder simply is not built for this picture — so every other advertised codec is + /// a genuine candidate and filtering by [`software_decodable_codecs`] here would end + /// sessions a plain HEVC retry would have finished. + Shape, +} + +/// The reconnect rule, in one pure function: an HEVC session whose hardware rungs are +/// exhausted must come back as a session this client can finish. +/// +/// The codec is fixed at Welcome and the control stream renegotiates shard payload only, +/// so there is no in-session move available — the only lever is what the NEXT Hello +/// advertises. `advertised` is what this session offered; the answer removes `negotiated` +/// from it, plus — for a [`RungLoss::Codec`] — any other codec that would land in the +/// same hole (one whose only remaining rung is a software one that does not exist). Two +/// sessions of the same failure is the shape this rules out. +/// +/// `caps` is what the retry ACTUALLY advertises: the pump derives its `exclude_codecs` +/// from this set rather than from the failed codec alone, so the wire and this verdict +/// cannot disagree (they did until the M8 review — the wire re-offered PyroWave the rule +/// had removed). +/// +/// Pure and total on purpose — this is the piece that gets tested as a first-class path, +/// because the on-glass version of it costs a real host with a real GPU failure. +pub fn last_rung_verdict(negotiated: u8, advertised: u8, loss: RungLoss) -> LastRungVerdict { + let survivors = advertised & !negotiated; + let caps = match loss { + // Everything still on the table that ALSO has a CPU rung underneath it. + RungLoss::Codec => survivors & software_decodable_codecs(), + RungLoss::Shape => survivors, + }; + // A retry the host's precedence ladder cannot PICK is not a retry: `resolve_codec` + // deliberately keeps PyroWave out of that ladder (it is opt-in only), so a Hello + // whose survivors are PyroWave alone resolves to nothing and the host refuses the + // session. Judge liveness on the pickable ones and carry the rest along. + const PICKABLE: u8 = punktfunk_core::quic::CODEC_H264 + | punktfunk_core::quic::CODEC_HEVC + | punktfunk_core::quic::CODEC_AV1; + if caps & PICKABLE == 0 { + LastRungVerdict::Dead + } else { + LastRungVerdict::Retry { caps } + } +} + +/// The pre-M10 decoder-preference spellings, mapped onto the rung that replaced them. +/// +/// `vulkan`, `vaapi` and `d3d11va` named **libavcodec's** rungs specifically — the whole +/// point of the `native-*` names was that they were the OTHER ones — and M10 deleted them. +/// But those three are not developer-only env values: all three desktop Settings UIs +/// offered them (`clients/linux`'s "VAAPI", the WinUI shell's "Hardware (Direct3D 11 / +/// DXVA)", the console UI's list), so they sit in shipped users' settings files right now. +/// Refusing them would turn an upgrade into a dead session for anyone who ever touched +/// that dropdown, and the message would tell them to edit something they never edited. +/// +/// So they MIGRATE rather than refuse, and the mapping is exact in the sense that +/// matters: the user asked for a hardware FAMILY (Vulkan Video / VAAPI / DXVA) and gets +/// that family, on the implementation that still exists. The UI labels stay true +/// word-for-word — native Vulkan Video is still Vulkan Video. What does change is the +/// failure mode: a libavcodec pin that failed to open was a hard session error, while +/// every `native-*` pin logs and falls through to the standard ladder. For a value read +/// out of a settings file that is the right direction; a pin that cannot open must not be +/// the reason a user's client stops working. +/// +/// ⚠ It is `pub` and PURE — no log line — for the Settings dialogs, not for the pump. +/// Their decoder combos look the stored string up in their own preset list, and a value +/// that matches nothing shows as "Automatic"; save the dialog without touching that row +/// and the user's Vulkan/VAAPI preference is silently rewritten to `auto`. So they +/// migrate on the WAY IN too, and a function that logged would then warn once per dialog +/// open. [`Decoder::new`] does the logging, where "a session started on a migrated +/// preference" is the fact worth recording. +/// +/// Nothing rewrites the STORE. The value is migrated on every read, so a user who +/// downgrades to an older client still finds the preference they set. +pub fn migrate_decoder_pref(pref: &str) -> String { + match pref { + "vulkan" => "native-vulkan".to_string(), + "vaapi" => "native-vaapi".to_string(), + "d3d11va" => "native-d3d11va".to_string(), + _ => pref.to_string(), + } +} + +/// Is video decode PINNED to the CPU rung — the Settings "Video decoder" value, or the +/// `PUNKTFUNK_DECODER` override that wins over it? +/// +/// Same precedence as [`Decoder::new`] resolves (env first, then the setting), because a +/// second reading of the same two inputs is a second place for them to drift. +pub fn decode_pinned_to_software(pref: &str) -> bool { + resolve_decoder_pref(std::env::var("PUNKTFUNK_DECODER").ok().as_deref(), pref) == "software" +} + +/// Resolve the decoder preference: the `PUNKTFUNK_DECODER` override if it carries a +/// value, else the stored setting. Pure, so the rule is testable without touching the +/// process environment — and shared, because [`Decoder::new`] and +/// [`decode_pinned_to_software`] read the same two inputs and a second reading is a +/// second place for them to drift. +/// +/// **Trimmed**, which is the part that had to be fixed rather than merely factored out. +/// `PUNKTFUNK_VK_ADAPTER` already trimmed; this did not, so `"native-vulkan "` — one +/// trailing space, which a Windows `.cmd` produces for free because `echo x>> file` +/// keeps the space before the redirect — matched no arm of [`native_vulkan_gate`] and +/// fell through to `auto` SILENTLY. An operator's pin was ignored and nothing said so, +/// which is the exact failure the rest of this module's logging exists to prevent. It +/// cost a full on-glass session to find. +/// +/// Whitespace-only is treated as absent, not as a pin to `""`: someone who exported the +/// variable empty means "no override", and `""` is a value `native_vulkan_gate` happens +/// to accept. +pub(crate) fn resolve_decoder_pref(env: Option<&str>, pref: &str) -> String { + env.map(str::trim) + .filter(|v| !v.is_empty()) + .map_or_else(|| pref.to_string(), str::to_string) +} + +/// The `quic` codec bitfield this client can decode — the union of the codecs the RUNGS +/// THIS BUILD COMPILED speak. Advertised to the host so it never emits a codec we can't +/// decode. +/// +/// It used to be a libavcodec registry walk (`ffmpeg::decoder::find` per id), and M9 is +/// where that stopped being an answer to the question asked: the registry described +/// decoders that were not in the ladder. It is now what §3.6 of the plan asked for — a +/// statement about our own rungs — and it is the reason M10 could delete every FFmpeg rung +/// without renegotiating a single field session: the answer did not move, because the +/// FFmpeg rungs never covered a codec the native ones don't: +/// +/// * native Vulkan Video (`video_vk_native`, both desktop OSes) decodes H.264, H.265 and +/// AV1, and it is compiled unconditionally; +/// * the platform native rungs (`video_d3d11_native` / `video_vaapi_native`) cover the +/// same three; +/// * the CPU rung ([`software_decodable_codecs`]) covers H.264 and AV1. +/// +/// The three flags are constants rather than probes for the same reason they always were: +/// this is asked before a device exists (it feeds the very first Hello), so it can only +/// speak about what was BUILT. Everything device-shaped is [`decodable_codecs_for`]. +/// +/// ⚠ **AV1 here is a decoder EXISTING, not a decoder that can keep up.** Use +/// [`decodable_codecs_for`], which gates it on hardware — see [`av1_hardware_decodable`]. +/// +/// ⚠ **HEVC here is a HARDWARE decoder existing**, and since M8 that is the only kind +/// there is: the CPU rung has no HEVC ([`software_decodable_codecs`]). Advertising it +/// anyway is deliberate and is the plan's — hardware HEVC is the path most hosts and most +/// clients actually take, and refusing it up front would cost every one of them the codec +/// to protect the few whose hardware later fails. The exhaustion case is handled where it +/// happens, by [`last_rung_verdict`], and it is the ONE codec whose advertisement is a +/// promise this client cannot keep unconditionally. Where the client can KNOW in advance +/// that it cannot keep it — decode pinned to software — [`decodable_codecs_for`] drops +/// the bit before the first Hello instead. pub fn decodable_codecs() -> u8 { - let _ = ffmpeg::init(); - let mut bits = 0u8; - for (id, bit) in [ - (ffmpeg::codec::Id::HEVC, punktfunk_core::quic::CODEC_HEVC), - (ffmpeg::codec::Id::H264, punktfunk_core::quic::CODEC_H264), - (ffmpeg::codec::Id::AV1, punktfunk_core::quic::CODEC_AV1), - ] { - if ffmpeg::decoder::find(id).is_some() { - bits |= bit; - } + // The native Vulkan rung's three codecs (`native_codec`'s map is the same set), plus + // the CPU rung's — written as a union so that removing a rung's codec leg shows up + // here rather than silently keeping the advertisement alive. + punktfunk_core::quic::CODEC_H264 + | punktfunk_core::quic::CODEC_HEVC + | punktfunk_core::quic::CODEC_AV1 + | software_decodable_codecs() +} + +/// Can this machine decode AV1 in HARDWARE? +/// +/// The question exists because "a decoder for AV1 exists" was never the same claim: back +/// when this crate linked libavcodec, `ffmpeg::decoder::find(AV1)` answered yes on every +/// build that carried libdav1d — a SOFTWARE decoder — and advertising off that answer told +/// the host "send me AV1" on machines that would then decode a 4K stream on the CPU. The +/// dependency is gone and the trap is not: this crate still HAS a CPU AV1 rung (rav1d), so +/// `decodable_codecs` still says AV1, and the wire's codec negotiation is still a promise +/// about capability made once, with nothing to fall back to once the session runs. +/// +/// Answered from device facts only, never from a decoder existing: +/// +/// * the presenter's Vulkan device advertises `DECODE_AV1` in its decode queue +/// family's codec operations, or +/// * (Windows) the presenter can import D3D11 textures — the native DXVA rung then decodes +/// AV1 Profile 0 through the adapter's profile GUID, and `auto` reaches it. ⚠ That leg +/// has decoded nothing on hardware ([`native_evidence`]); the session says so at `warn`. +/// Before M10 this arm was conditional, because the leg was kept out of `auto` while +/// libavcodec's DXVA rung was still below it — with that rung deleted there is no +/// condition left to write. +/// +/// ⚠ Deliberately NOT consulted: VAAPI. Asking libva costs opening a display, which +/// this function is called too early and too often to do; the Vulkan bit covers the +/// Mesa devices where VAAPI AV1 exists in practice, and a machine with VAAPI AV1 but +/// no Vulkan AV1 loses only the ADVERTISEMENT, not a working path. +pub fn av1_hardware_decodable(vk: Option<&VulkanDecodeDevice>) -> bool { + if vk.is_some_and(|v| v.video_decode && v.decode_video_caps & VIDEO_CODEC_OP_DECODE_AV1 != 0) { + return true; } - bits + // The second answer is per-platform, so it is bound to a name rather than + // written as a cfg'd `return`: on Windows clippy calls that `needless_return` + // and fails `-D warnings`, which NO ci leg would have caught (nothing runs + // clippy on Windows — this surfaced only from a manual check on a box). + #[cfg(windows)] + let d3d11 = vk.is_some_and(|v| v.d3d11_import); + #[cfg(not(windows))] + let d3d11 = false; + d3d11 } /// [`decodable_codecs`] plus the PyroWave bit when the presenter's device passed the -/// compute-feature probe. Advertisement-only: `resolve_codec` never auto-picks PyroWave — -/// the session must also name it `preferred_codec` (plan §3), which the client does only -/// under its explicit opt-in. -pub fn decodable_codecs_for(vk: Option<&VulkanDecodeDevice>) -> u8 { - let bits = decodable_codecs(); +/// compute-feature probe, minus the codecs `decoder_pref` makes unreachable. +/// Advertisement-only: `resolve_codec` never auto-picks PyroWave — the session must also +/// name it `preferred_codec` (plan §3), which the client does only under its explicit +/// opt-in. +pub fn decodable_codecs_for(vk: Option<&VulkanDecodeDevice>, decoder_pref: &str) -> u8 { + let mut bits = decodable_codecs(); + // AV1 is hardware-gated (M7). Without this the bit rides on the CPU rung's mere + // existence and the host is told to send AV1 to a machine that would decode it on + // the CPU — and once the session is negotiated there is nothing to fall back to. + if bits & punktfunk_core::quic::CODEC_AV1 != 0 && !av1_hardware_decodable(vk) { + tracing::info!( + "AV1 not advertised: no hardware AV1 decode on this device (a software \ + decoder exists, but a 4K AV1 stream is not survivable on it)" + ); + bits &= !punktfunk_core::quic::CODEC_AV1; + } + // The one HEVC case the client can answer BEFORE the Hello (M8 review): decode is + // pinned to the CPU rung, and the CPU rung has no HEVC — so the advertisement would + // be a promise this build cannot keep for the whole session, exactly what + // `av1_hardware_decodable` exists to stop for AV1. Every other HEVC failure is a + // per-device fact only the session can learn, and `last_rung_verdict` answers it + // there. Guarded on something remaining: a Hello advertising ZERO codecs reads as + // "HEVC-only" to a host (`resolve_codec`'s pre-negotiation default), which would be + // the precise opposite of this. + if bits & punktfunk_core::quic::CODEC_HEVC != 0 + && bits & !punktfunk_core::quic::CODEC_HEVC != 0 + && decode_pinned_to_software(decoder_pref) + { + tracing::info!( + "HEVC not advertised: decode is pinned to software and there is no software \ + HEVC decoder in this build" + ); + bits &= !punktfunk_core::quic::CODEC_HEVC; + } #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] if vk.map(|v| v.pyrowave_decode).unwrap_or(false) { return bits | punktfunk_core::quic::CODEC_PYROWAVE; @@ -435,63 +1493,187 @@ pub fn decodable_codecs_for(vk: Option<&VulkanDecodeDevice>) -> u8 { bits } -/// libavcodec logs reference-frame recovery to the process stderr very verbosely -/// (`First slice in a frame missing`, `Could not find ref with POC …`, `Error -/// constructing the frame RPS`) — normal chatter while the decoder waits for a keyframe -/// after loss, but a raw flood in the user's terminal (it bypasses our tracing). Default -/// it to fatal-only; `PUNKTFUNK_FFMPEG_LOG=` restores it -/// for decode debugging. Process-global; set once per decoder build (idempotent). -fn quiet_ffmpeg_log() { - use ffmpeg::util::log::Level; - let level = match std::env::var("PUNKTFUNK_FFMPEG_LOG").ok().as_deref() { - Some("quiet") => Level::Quiet, - Some("error") => Level::Error, - Some("warning") => Level::Warning, - Some("info") => Level::Info, - Some("debug" | "trace") => Level::Debug, - _ => Level::Fatal, +/// Say what `PUNKTFUNK_AU_FAULT` will do to THIS session, once, at decoder +/// construction — including the two cases where the answer is "nothing". +/// +/// The knob only bites on the native VULKAN rung (its injector sits at that backend's +/// decode entry), so a lab run that armed it and landed anywhere else — a shape that +/// rung refused, a session that demoted, a PyroWave session — must be told +/// so. Silence there is indistinguishable from "the fault was injected and +/// nothing detected it", which is precisely the conclusion a fault run exists to +/// make trustworthy. Unset is the normal state and says nothing at all. +fn report_au_fault_env(native_rung: bool) { + let Ok(spec) = std::env::var("PUNKTFUNK_AU_FAULT") else { + return; }; - ffmpeg::util::log::set_level(level); + if spec.is_empty() { + return; + } + match pf_vkdecode::AuFault::from_spec(&spec) { + // The native backend logs the arming itself (mode + period), with the + // decoder it is about to corrupt in hand — no need to say it twice. + Some(_) if native_rung => {} + Some(_) => tracing::warn!( + value = %spec, + "PUNKTFUNK_AU_FAULT is armed, but this session is NOT on the native \ + Vulkan rung — no AU will be corrupted and no detector will fire" + ), + None => tracing::warn!( + value = %spec, + "PUNKTFUNK_AU_FAULT not understood (want drop|truncate|flip[:period]) \ + — ignored" + ), + } +} + +/// Name the rung a session just landed on, and say whether any hardware has ever decoded +/// a frame through it for this codec. +/// +/// This is the program's honesty surface, and M10 is where it earns its keep: every rung +/// is now native, two of them have never decoded anything anywhere, and there is no +/// libavcodec twin left underneath to catch a session that lands wrong. A field report of +/// the form "M10 broke my stream" is only actionable if the log distinguishes *the rung +/// with three drivers and a 92-minute soak behind it* from *the rung nothing has ever +/// run*, and the `stats:` decode-path tag — which is a machine interface and stays +/// additive-only — names the rung but says nothing about its provenance. +/// +/// So: `info` when the pair is hardware-verified, **`warn` when it is not**, with the +/// evidence string from [`native_evidence`] carried verbatim so the log explains itself +/// without a reader having to find this file. +fn log_rung(backend: &Backend, wire: u8) { + let (rung, evidence) = match backend { + Backend::NativeVulkan(_) => ( + NativeRung::Vulkan.name(), + Some(native_evidence(NativeRung::Vulkan, wire)), + ), + #[cfg(windows)] + Backend::NativeD3d11va(_) => ( + NativeRung::D3d11va.name(), + Some(native_evidence(NativeRung::D3d11va, wire)), + ), + #[cfg(target_os = "linux")] + Backend::NativeVaapi(_) => ( + NativeRung::Vaapi.name(), + Some(native_evidence(NativeRung::Vaapi, wire)), + ), + Backend::Software(_) => ( + NativeRung::Software.name(), + Some(native_evidence(NativeRung::Software, wire)), + ), + // PyroWave is not in the evidence table and never will be: it is not a rung of + // the H.264/H.265/AV1 ladder at all, it is its own codec with its own decoder and + // no rung above or below it. + #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] + Backend::PyroWave(_) => ("pyrowave", None), + }; + let codec = wire_codec_name(wire); + match evidence { + Some(e) if e.verified => tracing::info!( + rung, + codec, + hardware_verified = true, + evidence = e.note, + "decode rung active" + ), + Some(e) => tracing::warn!( + rung, + codec, + hardware_verified = false, + evidence = e.note, + "decode rung active — NO hardware has ever decoded a frame through this \ + rung/codec pair (evidence table, video.rs)" + ), + None => tracing::info!(rung, codec, "decode rung active"), + } } impl Decoder { - /// `codec_id` is the codec the host resolved in the Welcome (never assume HEVC). - /// `pref` is the Settings "Video decoder" value (`auto`/`vulkan`/`vaapi`/`d3d11va`/ - /// `software`; `hardware` — the WinUI shell's stored value — reads as auto). - /// `vk` is the presenter's shared Vulkan device when its stack can run FFmpeg's - /// Vulkan Video decoder — decode lands as VkImages the presenter samples directly. + /// `wire` is the codec the host resolved in the Welcome, as the WIRE states it + /// ([`punktfunk_core::quic`]'s `CODEC_*` bit — never assume HEVC). It is the only + /// codec vocabulary the ladder speaks since M10 dropped `ffmpeg::codec::Id` with the + /// last libavcodec rung. + /// `pref` is the Settings "Video decoder" value (`auto`/`native-vulkan`/ + /// `native-vaapi`/`native-d3d11va`/`software`; `hardware` — the WinUI shell's stored + /// value — reads as auto). + /// `vk` is the presenter's shared Vulkan device — decode lands as VkImages the + /// presenter samples directly. /// Precedence: the `PUNKTFUNK_DECODER` env override wins (support/debug escape /// hatch, and the documented knob), then the setting; both default to auto. /// Auto's hardware order depends on the device on BOTH desktop OSes - /// ([`VulkanDecodeDevice::prefer_vulkan_first`]). Linux: VAAPI → Vulkan → software on - /// desktop Mesa (AMD/Intel), Vulkan → VAAPI → software on NVIDIA and the Deck's - /// VanGogh. Windows (no VAAPI there): Vulkan → D3D11VA → software on NVIDIA/AMD, - /// D3D11VA → Vulkan → software on Intel/unknown (Intel's driver advertises Vulkan - /// Video, but FFmpeg-Vulkan on it strobes/overruns the budget — B580 field report). + /// ([`VulkanDecodeDevice::prefer_vulkan_first`]). Linux: native-vk → native-vaapi → + /// software on NVIDIA and ALL AMD (`prefer_vulkan_first` is vendor-wide — + /// desktop RADV included, on-glass verdict — not just the Deck's VanGogh); the two + /// swap on Intel/unknown. Windows (no VAAPI there): native-vk → + /// native-d3d11va → software on NVIDIA/AMD, swapped on + /// Intel/unknown (Intel's driver advertises Vulkan Video, but Vulkan decode on it + /// strobed/overran the budget — B580 field report). + /// + /// On top of that order sits the evidence filter ([`native_rung_admitted`]): a rung + /// that has never decoded a frame does not go FIRST when the rung directly below it is + /// proven for this codec and usable on this device. That is the Linux Intel/unknown + /// arm and only that arm — everywhere else what is below is the CPU. + /// + /// Whatever it lands on, the session logs `decode rung active` with the rung's name + /// and its evidence state, and that line is a WARNING when no hardware has ever + /// decoded a frame through the rung/codec pair the session just chose. + /// + /// `stream` is the picture shape the host resolved ([`StreamFormat`]). Every native + /// rung reads it as its construction-time device probe, so a shape this GPU cannot + /// decode refuses BEFORE the rung is chosen — where the fall-through to the next rung + /// is a plain construction failure — instead of at the first AU, where the only exit + /// is an error-streak demotion past it. pub fn new( - codec_id: ffmpeg::codec::Id, + wire: u8, pref: &str, vk: Option<&VulkanDecodeDevice>, + stream: StreamFormat, ) -> Result { - ffmpeg::init().context("ffmpeg init")?; - quiet_ffmpeg_log(); - let choice = std::env::var("PUNKTFUNK_DECODER") - .ok() - .filter(|v| !v.is_empty()) - .unwrap_or_else(|| pref.to_string()); + let stored = resolve_decoder_pref(std::env::var("PUNKTFUNK_DECODER").ok().as_deref(), pref); + let choice = migrate_decoder_pref(&stored); + if choice != stored { + // Said once per session, at `warn`, because a developer who set the env var to + // bisect libavcodec against native needs to know that distinction is gone — + // and a support engineer reading a log needs to know the rung below was not + // the one the settings file names. See [`migrate_decoder_pref`]. + tracing::warn!( + stored, + using = choice, + "the decoder preference named libavcodec's rung, which no longer exists \ + (M10 removed FFmpeg from the client) — using the native rung for the \ + same hardware path" + ); + } #[cfg(windows)] let (d3d11_import, adapter_luid, d3d11_hdr10) = ( vk.is_some_and(|v| v.d3d11_import), vk.and_then(|v| v.adapter_luid), vk.is_some_and(|v| v.d3d11_hdr10), ); - let done = |backend| { + let done = |backend: Backend| { + // Whatever rung this session landed on, say what `PUNKTFUNK_AU_FAULT` + // is going to do about it — see [`report_au_fault_env`]. Here, at the + // one exit every backend leaves through, rather than in the native + // Vulkan backend's constructor: a lab run whose session never REACHES that + // constructor (a refused shape, a demotion, another rung entirely) would + // otherwise sit silently un-faulted and read as a fault run that + // detected nothing. + report_au_fault_env(matches!(backend, Backend::NativeVulkan(_))); + // ...and say WHICH rung it is and whether any hardware has ever decoded a + // frame through it. Every rung is native now and two of them have no + // hardware evidence at all — a session log that does not distinguish + // those from the proven ones would make every field report about M10 + // unfalsifiable. See [`log_rung`]. + log_rung(&backend, wire); Ok(Decoder { + entered_rungs: rung_bit(&backend), backend, - codec_id, + wire_codec: wire, vaapi_fails: 0, first_fail: None, want_keyframe: false, + delivered: false, + vk: vk.cloned(), + stream, #[cfg(windows)] d3d11_import, #[cfg(windows)] @@ -500,11 +1682,166 @@ impl Decoder { d3d11_hdr10, }) }; - // Linux `auto`: try VAAPI FIRST unless this device is one where Vulkan Video is - // the established right answer (NVIDIA — no usable VAAPI; VanGogh — VAAPI + // The codec's human name, for every log line below — the ladder used to print + // `?codec_id` (an `ffmpeg::codec::Id` Debug), and this is the same fact stated in + // the vocabulary that survived M10. + let codec_name = wire_codec_name(wire); + // The PINS, ahead of everything, because a pin is a pin: it skips the vendor + // order, which is how a lab run reaches a rung `auto` would not have picked on + // this device. Any refusal or init failure logs and DEMOTES to the standard + // ladder below exactly as if the rung had errored (choice reads as `auto` from + // here on) — a pinned rung's failure must never be quieter, or land somewhere + // other, than the same rung's failure inside `auto`. + let mut choice = choice; + // Native D3D11VA (M5, pf-dxvadec). + #[cfg(windows)] + if choice == crate::video_d3d11_native::DECODER_PIN { + match (native_d3d11_codec(wire), vk.filter(|v| v.d3d11_import)) { + (Some(codec), Some(v)) => { + match crate::video_d3d11_native::NativeD3d11Decoder::new( + codec, + stream, + v.adapter_luid, + v.d3d11_hdr10, + ) { + Ok(d) => { + tracing::info!( + codec = codec_name, + decoder = d.name(), + "native D3D11VA hardware decode active \ + (pf-dxvadec, shared-texture hand-off)" + ); + return done(Backend::NativeD3d11va(Box::new(d))); + } + Err(e) => tracing::warn!(reason = %format!("{e:#}"), + "native D3D11VA init failed — demoting to the standard ladder"), + } + } + (None, _) => tracing::warn!( + codec = codec_name, + "PUNKTFUNK_DECODER=native-d3d11va refused (needs an H.264, HEVC or \ + AV1 session) — standard ladder" + ), + (_, None) => tracing::warn!( + "PUNKTFUNK_DECODER=native-d3d11va refused (the presenter's device lacks \ + the win32 external-memory import extensions) — standard ladder" + ), + } + choice = "auto".to_string(); + } + // Native VAAPI (M6, pf-vaadec). The pin matters most here: this rung has decoded + // nothing on any hardware, and the pin is what a lab run uses to GENERATE the + // evidence that would change that — on a box whose vendor order puts Vulkan first + // and where `auto` would therefore never reach it. + #[cfg(target_os = "linux")] + if choice == crate::video_vaapi_native::DECODER_PIN { + match native_vaapi_codec(wire) { + Some(codec) => { + match crate::video_vaapi_native::NativeVaapiDecoder::new(codec, stream) { + Ok(d) => { + tracing::info!( + codec = codec_name, + decoder = d.name(), + "native VAAPI hardware decode active (pf-vaadec, zero-copy dmabuf)" + ); + return done(Backend::NativeVaapi(Box::new(d))); + } + Err(e) => tracing::warn!(reason = %format!("{e:#}"), + "native VAAPI init failed — demoting to the standard ladder"), + } + } + None => tracing::warn!( + codec = codec_name, + "PUNKTFUNK_DECODER=native-vaapi refused (needs an H.264, HEVC or \ + AV1 session) — standard ladder" + ), + } + choice = "auto".to_string(); + } + let mut native_tried = false; + if choice == "native-vulkan" { + if native_vulkan_gate( + &choice, + wire, + vk.is_some_and(|v| v.video_decode), + vk.map_or(0, |v| v.decode_video_caps), + ) { + native_tried = true; + let vk = vk.expect("gate demands video_decode, so vk is Some"); + let (codec, _) = native_codec(wire).expect("the gate admitted this codec"); + match NativeVulkanDecoder::new(vk, codec, stream) { + Ok(n) => { + tracing::info!( + codec = codec_name, + "native Vulkan Video hardware decode active \ + (pf-vkdecode, presenter-shared device)" + ); + return done(Backend::NativeVulkan(Box::new(n))); + } + Err(e) => tracing::warn!(reason = %format!("{e:#}"), + "native Vulkan decode init failed — demoting to the standard ladder"), + } + } else { + // The gate is an AND of three, so name all three. `video_decode=true` + // beside "refused" is otherwise unreadable: it says the device decodes + // SOMETHING while refusing THIS codec, and the bit that would explain it + // — the decode family's advertised operations — went unprinted. That is + // the difference between "your GPU can't do this" and "we asked for the + // wrong thing", and only the second is our bug. + tracing::warn!( + codec = codec_name, + video_decode = vk.is_some_and(|v| v.video_decode), + decode_video_caps = + format_args!("0x{:X}", vk.map_or(0, |v| v.decode_video_caps)), + codec_op_needed = + format_args!("0x{:X}", native_codec(wire).map_or(0, |(_, op)| op)), + device = vk.map_or("", |v| v.device_name.as_str()), + "PUNKTFUNK_DECODER=native-vulkan refused (needs an H.264, HEVC or AV1 \ + session and a presenter device whose decode family advertises that \ + codec) — standard ladder" + ); + } + choice = "auto".to_string(); + } + // Linux's VAAPI RUNG: native VAAPI (pf-vaadec). `auto` reaches it from two places + // — Intel/unknown take it before Vulkan, everyone else after — so it lives here + // once instead of twice. + // + // ⚠ It has decoded nothing on any hardware ([`native_evidence`]). Until M10 that + // kept it out of `auto` while libavcodec's VAAPI hwaccel sat directly below it; + // with that rung deleted this is the only VAAPI there is. It runs with the warning + // `done` logs — except where the evidence filter sends the Intel/unknown arm to + // native Vulkan Video first ([`native_rung_admitted`], at the call site below), + // after which this closure is reached from the SECOND arm, genuinely below Vulkan. + #[cfg(target_os = "linux")] + let vaapi_rung = |choice: &str| -> Result> { + if let Some(codec) = native_vaapi_codec(wire) { + match crate::video_vaapi_native::NativeVaapiDecoder::new(codec, stream) { + Ok(d) => { + tracing::info!( + codec = codec_name, + decoder = d.name(), + "native VAAPI hardware decode active (pf-vaadec, zero-copy dmabuf)" + ); + return Ok(Some(Backend::NativeVaapi(Box::new(d)))); + } + Err(e) => tracing::info!(reason = %format!("{e:#}"), + "native VAAPI unavailable — continuing down the ladder"), + } + } + // ⚠ `choice` is unread here now, and that is the M10 change worth naming: the + // pre-M10 `vaapi` pin meant libavcodec's rung SPECIFICALLY, so this closure + // ended in a hard error for it. It is migrated to `native-vaapi` before the + // ladder ever runs ([`migrate_decoder_pref`]), so by here it is either a + // native pin (handled above, ahead of the vendor order) or the auto family. + let _ = choice; + Ok(None) + }; + // Linux `auto`: try VAAPI FIRST unless this device is one where Vulkan Video + // is the established right answer (NVIDIA — no usable VAAPI; VanGogh — VAAPI // chroma-fringes). Mesa now exposes decode queues by default (and the session // binary opts RADV in for the Deck's sake), which silently moved every desktop - // AMD/Intel box onto FFmpeg-Vulkan-on-Mesa — user-reported to judder/error-streak + // AMD/Intel box onto Vulkan-on-Mesa — user-reported to judder/error-streak // (then demote to software) where explicit VAAPI streams perfectly. #[cfg(target_os = "linux")] let mut vaapi_tried = false; @@ -514,30 +1851,95 @@ impl Decoder { .filter(|v| v.video_decode) .is_some_and(|v| v.prefer_vulkan_first()) { - vaapi_tried = true; - match VaapiDecoder::new(codec_id) { - Ok(v) => { - tracing::info!( - ?codec_id, - decoder = v.name(), - "VAAPI hardware decode active (zero-copy dmabuf)" - ); - return done(Backend::Vaapi(v)); - } - Err(e) => { - tracing::info!(reason = %e, "VAAPI unavailable — trying Vulkan Video"); + // ⚠ The evidence filter, and the ONE arm of the whole ladder where it still + // has somewhere to yield to ([`native_rung_admitted`]). This is the + // Intel/unknown order, so the rung directly below VAAPI is native Vulkan + // Video — proven for all three codecs, where pf-vaadec is proven for none. If + // this device can actually run that rung for this codec, VAAPI does not go + // first; the ladder falls through to Vulkan below and VAAPI keeps its place + // UNDER it (`vaapi_tried` stays false, so the second arm still tries it when + // Vulkan can't be built). + let below = native_vulkan_usable( + wire, + vk.is_some_and(|v| v.video_decode), + vk.map_or(0, |v| v.decode_video_caps), + ) + .then_some(NativeRung::Vulkan); + if native_rung_admitted(NativeRung::Vaapi, wire, below) { + vaapi_tried = true; + if let Some(b) = vaapi_rung(&choice)? { + return done(b); } + } else { + tracing::info!( + codec = codec_name, + evidence = native_evidence(NativeRung::Vaapi, wire).note, + "native VAAPI is this device's first hardware rung, but it has decoded \ + nothing on any hardware and the rung below it — native Vulkan Video — \ + has, for this codec, on this device: taking Vulkan first \ + (PUNKTFUNK_DECODER=native-vaapi runs it anyway)" + ); } } // Windows `auto`: D3D11VA FIRST unless this device is one where Vulkan Video is // the established right answer (NVIDIA/AMD). Intel's Windows driver advertises - // Vulkan Video (Arc drivers since 2023) so the capability gate alone no longer - // keeps Intel off FFmpeg-Vulkan — and that combination is field-broken (B580, + // Vulkan Video (Arc drivers since 2023) so the capability gate alone does not + // keep Intel off the Vulkan rung — and that combination is field-broken (B580, // 2026-07: strobing between clean anchors and corrupt inter frames that never // trips the error-streak demotion, 7 ms p50 decodes blowing the 120 Hz budget) // where D3D11VA — the DXVA path every Windows video player exercises, and what // this backend was built for — streams clean. Vulkan stays reachable below by // explicit preference and as auto's fallback when D3D11VA can't be built. + // + // ⚠ The B580 measurement was taken on the FFmpeg-Vulkan rung of the day, not on + // pf-vkdecode, and no Intel box has run the native one. The vendor order is kept + // as it was for exactly that reason: nothing has been measured that would justify + // changing it, and "the old evidence no longer applies" is not evidence. + // + // Windows' D3D11VA RUNG: native D3D11VA (pf-dxvadec). Its H.264/H.265 legs HAVE + // hardware evidence (parity on an RTX 4090 and an AMD iGPU plus a 30-minute soak, + // M5); its AV1 leg has none and runs with the warning `done` logs — until M10 that + // leg was skipped in `auto` in favour of libavcodec's DXVA rung, which no longer + // exists. The rung needs the presenter's win32 import path or its frames could + // never reach the screen — that check is first, once. + #[cfg(windows)] + let d3d11_rung = |choice: &str| -> Result> { + let Some(v) = vk.filter(|v| v.d3d11_import) else { + // A PIN that cannot possibly work is worth saying out loud — a DXVA frame + // reaches the screen through the presenter's win32 import and nothing else, + // so without it this rung would decode into a texture no one can display. + // (`native-d3d11va` here covers the migrated `d3d11va` too — see + // [`migrate_decoder_pref`].) + if choice == crate::video_d3d11_native::DECODER_PIN { + bail!( + "PUNKTFUNK_DECODER=native-d3d11va but the presenter's device lacks the \ + win32 external-memory import extensions — see the presenter log" + ); + } + return Ok(None); + }; + if let Some(codec) = native_d3d11_codec(wire) { + match crate::video_d3d11_native::NativeD3d11Decoder::new( + codec, + stream, + v.adapter_luid, + v.d3d11_hdr10, + ) { + Ok(d) => { + tracing::info!( + codec = codec_name, + decoder = d.name(), + "native D3D11VA hardware decode active \ + (pf-dxvadec, shared-texture hand-off)" + ); + return Ok(Some(Backend::NativeD3d11va(Box::new(d)))); + } + Err(e) => tracing::info!(reason = %format!("{e:#}"), + "native D3D11VA unavailable — continuing down the ladder"), + } + } + Ok(None) + }; #[cfg(windows)] let mut d3d11_tried = false; #[cfg(windows)] @@ -545,148 +1947,140 @@ impl Decoder { && !vk .filter(|v| v.video_decode) .is_some_and(|v| v.prefer_vulkan_first()) + // The evidence filter applies here too, and it admits — the `None` is the + // load-bearing part, so it is stated rather than assumed. On paper the rung + // below D3D11VA on this arm is native Vulkan Video; in fact this arm IS the + // Intel/unknown vendor family, the one family in this program with a MEASURED + // wrong-pixel report against Vulkan decode (the B580 note below). Falling from + // "has never run" onto "known to strobe here" is not a fall onto proven code, + // so what is really below the DXVA AV1 leg is the CPU — and its H.264/H.265 + // legs are verified anyway, which is what the first clause of + // [`native_rung_admitted`] answers. + && native_rung_admitted(NativeRung::D3d11va, wire, None) { - if let Some(v) = vk.filter(|v| v.d3d11_import) { - d3d11_tried = true; - match crate::video_d3d11::D3d11vaDecoder::new( - codec_id, - v.adapter_luid, - v.d3d11_hdr10, - ) { - Ok(d) => { - tracing::info!( - ?codec_id, - decoder = d.name(), - "D3D11VA hardware decode active (shared-texture hand-off)" - ); - return done(Backend::D3d11va(d)); - } - Err(e) => { - tracing::info!(reason = %format!("{e:#}"), - "D3D11VA unavailable — trying Vulkan Video"); - } - } + d3d11_tried = true; + if let Some(b) = d3d11_rung(&choice)? { + return done(b); } } - if matches!(choice.as_str(), "auto" | "" | "vulkan" | "hardware") { - // `video_decode` gates the Vulkan Video attempt: the presenter now exports its - // handle bundle even when the device has no decode queue (Windows D3D11 interop - // rides the same struct), so presence alone no longer implies a usable decoder. - match vk.filter(|v| v.video_decode) { - Some(vk) => match VulkanDecoder::new(codec_id, vk) { - Ok(v) => { - tracing::info!( - ?codec_id, - decoder = v.name(), - "Vulkan Video hardware decode active (presenter-shared device)" - ); - return done(Backend::Vulkan(v)); - } - Err(e) => { - if choice == "vulkan" { - return Err(e.context("PUNKTFUNK_DECODER=vulkan but it failed")); - } - tracing::info!(reason = %format!("{e:#}"), - "Vulkan Video unavailable — falling back"); - } - }, - None if choice == "vulkan" => { - bail!( - "PUNKTFUNK_DECODER=vulkan but the presenter's device can't (missing \ - video extensions/queue) — see the presenter log" - ) - } - None => {} - } - } - // Deck/NVIDIA note: `auto` reaches VAAPI here when Vulkan Video isn't available - // (on desktop Mesa it was already tried above — `vaapi_tried` skips the repeat). - // A presenter that can't display the dmabufs demotes this decoder to software - // mid-session via [`Decoder::force_software`]. Windows has no VAAPI — auto falls - // straight through to software there. - #[cfg(target_os = "linux")] - if choice != "software" && choice != "vulkan" && !vaapi_tried { - match VaapiDecoder::new(codec_id) { - Ok(v) => { + // The VULKAN RUNG: native Vulkan Video (pf-vkdecode). Unlike the two platform + // rungs above it needs no closure — `auto` reaches it from exactly one place. + // [`native_vulkan_gate`] carries the whole admission decision, including the + // choice. Every codec leg has hardware parity against libavcodec (M2/M3 for + // H.264/H.265, M7 for AV1 — this module's evidence table); an init failure logs + // and falls through to the rung below. + // (`native_tried` skips the repeat when the pin above already attempted — and + // failed — the same construction.) + if !native_tried + && native_vulkan_gate( + &choice, + wire, + vk.is_some_and(|v| v.video_decode), + vk.map_or(0, |v| v.decode_video_caps), + ) + { + let vk = vk.expect("gate demands video_decode, so vk is Some"); + let (codec, _) = native_codec(wire).expect("the gate admitted this codec"); + match NativeVulkanDecoder::new(vk, codec, stream) { + Ok(n) => { tracing::info!( - ?codec_id, - decoder = v.name(), - "VAAPI hardware decode active (zero-copy dmabuf)" + codec = codec_name, + "native Vulkan Video hardware decode active \ + (pf-vkdecode auto rung, presenter-shared device)" ); - return done(Backend::Vaapi(v)); - } - Err(e) => { - if choice == "vaapi" { - return Err(e.context("PUNKTFUNK_DECODER=vaapi but VAAPI failed")); - } - tracing::warn!(error = %e, "VAAPI unavailable — falling back to software decode"); + return done(Backend::NativeVulkan(Box::new(n))); } + Err(e) => tracing::info!(reason = %format!("{e:#}"), + "native Vulkan decode unavailable — continuing down the ladder"), } } - // Windows: D3D11VA as the fallback rung for NVIDIA/AMD auto (Vulkan Video missing - // or failed to open) and the explicit `d3d11va` preference — gated on the presenter - // having the win32 external-memory import path, else its frames could never reach - // the screen. (On Intel/unknown auto it was already tried above — `d3d11_tried` - // skips the repeat.) + // Deck/NVIDIA note: `auto` reaches the VAAPI rung here when Vulkan Video isn't + // available (on desktop Mesa it was already tried above — `vaapi_tried` skips the + // repeat). A presenter that can't display the dmabufs demotes this decoder to + // software mid-session via [`Decoder::force_software`]. Windows has no VAAPI — auto + // falls straight through to software there. + #[cfg(target_os = "linux")] + if choice != "software" && !vaapi_tried { + if let Some(b) = vaapi_rung(&choice)? { + return done(b); + } + } + // Windows: the D3D11VA rung as the fallback for NVIDIA/AMD auto (Vulkan Video + // missing or failed to open). (On Intel/unknown auto it was already tried above — + // `d3d11_tried` skips the repeat.) #[cfg(windows)] - if choice != "software" && choice != "vulkan" && !d3d11_tried { - match vk.filter(|v| v.d3d11_import) { - Some(v) => { - match crate::video_d3d11::D3d11vaDecoder::new( - codec_id, - v.adapter_luid, - v.d3d11_hdr10, - ) { - Ok(d) => { - tracing::info!( - ?codec_id, - decoder = d.name(), - "D3D11VA hardware decode active (shared-texture hand-off)" - ); - return done(Backend::D3d11va(d)); - } - Err(e) => { - if choice == "d3d11va" { - return Err(e.context("PUNKTFUNK_DECODER=d3d11va but it failed")); - } - tracing::info!(reason = %format!("{e:#}"), - "D3D11VA unavailable — software decode"); - } - } - } - None if choice == "d3d11va" => bail!( - "PUNKTFUNK_DECODER=d3d11va but the presenter's device lacks the win32 \ - external-memory import extensions — see the presenter log" - ), - None => {} + if choice != "software" && !d3d11_tried { + if let Some(b) = d3d11_rung(&choice)? { + return done(b); } } if choice == "software" { // Say WHY hardware wasn't even attempted — a stored "software" preference - // (or the env override) silently skipping vulkan/vaapi has burned real + // (or the env override) silently skipping the hardware rungs has burned real // debugging time on boxes that could do better. tracing::info!( "software decode by preference (Settings decoder / PUNKTFUNK_DECODER) — \ hardware decode not attempted" ); } - done(Backend::Software(SoftwareDecoder::new(codec_id)?)) + // `?` here can carry a `NoSoftwareRung` (an HEVC session that pinned software, or + // one whose device offered no hardware rung at all). It stays typed all the way + // to the pump, which turns it into the reconnect rather than a dead session — + // see [`last_rung_verdict`]. + done(Backend::Software(SoftwareDecoder::new(wire)?)) } /// Wait for a Vulkan-Video frame's GPU decode to complete (timeline semaphore) — - /// the pump's decode-stat measurement. `false` = not the Vulkan backend, or timeout. + /// the pump's decode-stat measurement. `false` = not a Vulkan backend, timeout, or + /// a pair no longer in the shipped ledger / a stale session + /// generation — every false just declines the sample. pub fn wait_hw_decoded(&self, timeline_sem: u64, value: u64, timeout_ns: u64) -> bool { match &self.backend { - Backend::Vulkan(v) => v.wait_timeline(timeline_sem, value, timeout_ns), + Backend::NativeVulkan(d) => d.wait_timeline(timeline_sem, value, timeout_ns), _ => false, } } - /// Drain the "please ask the host for an IDR" flag — the pump calls this each iteration - /// (throttled) so a demoted/erroring decoder can resynchronize under the infinite GOP. + /// This session's decode-integrity counters, or `None` on a backend that has + /// no way to answer (the CPU rung and PyroWave — see [`DecodeHealth`]). + /// + /// `None` and `Some(DecodeHealth::default())` are deliberately different + /// answers, and the stats surface must keep them different: the first is "this + /// decoder cannot see corruption", the second is "this decoder looked and saw + /// none". Reporting the first as the second is exactly the mistake that let a + /// field corruption run undetected for a release. + pub fn decode_health(&self) -> Option { + match &self.backend { + Backend::NativeVulkan(d) => Some(d.health()), + // The native DXVA rung has the bitstream planner, so it sees concealment and + // refusals — but D3D11VA exposes no per-picture status query at all, so its + // `status_queries` is false and `failed` stays structurally 0. That is the + // honest report: "this decoder looked at the STREAM and saw none" without + // claiming a driver verdict nothing can produce. + #[cfg(windows)] + Backend::NativeD3d11va(d) => Some(d.health()), + // Same shape as the DXVA rung above, for the same reason: libva has no + // per-picture decode-status query either. + #[cfg(target_os = "linux")] + Backend::NativeVaapi(d) => Some(d.health()), + _ => None, + } + } + + /// The DECODE-order ordinal of the newest picture this lane has planned — the + /// watermark a caller stamps when it arms a post-loss freeze, so it can tell a + /// frame decoded before the loss from one decoded after it (see + /// [`NativeVkFrame::decode_order`]). 0 on every lane that has no bitstream + /// parser of its own, which is also every lane that reports no local recovery. + pub fn decode_order(&self) -> u64 { + match &self.backend { + Backend::NativeVulkan(d) => d.decode_order(), + _ => 0, + } + } + /// Open a PyroWave decoder for a `CODEC_PYROWAVE` session (plan §4.5): pyrowave - /// compute on the presenter's device, no FFmpeg. `codec_id` is irrelevant (kept as - /// HEVC so an — impossible — demotion path stays well-formed). + /// compute on the presenter's device. #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] pub fn new_pyrowave( vk: &VulkanDecodeDevice, @@ -697,6 +2091,8 @@ impl Decoder { color: ColorDesc, hdr16: bool, ) -> Result { + // Never the native rung — see [`report_au_fault_env`]. + report_au_fault_env(false); Ok(Decoder { backend: Backend::PyroWave(Box::new(crate::video_pyrowave::PyroWaveDecoder::new( vk, @@ -707,13 +2103,19 @@ impl Decoder { color, hdr16, )?)), - codec_id: ffmpeg::codec::Id::HEVC, + wire_codec: punktfunk_core::quic::CODEC_PYROWAVE, vaapi_fails: 0, first_fail: None, want_keyframe: false, + delivered: false, // A PyroWave session never demotes (nothing else decodes it — a failure - // renegotiates the codec instead), so the D3D11VA rebuild facts are unused - // here; keep them well-formed rather than plumbing them in for nothing. + // renegotiates the codec instead), so the demotion-rebuild facts (the + // device here, the D3D11VA ones below) are unused; keep them well-formed + // rather than plumbing them in for nothing. + vk: None, + stream: StreamFormat::SDR_420_8, + // A PyroWave session never demotes, so nothing ever reads this. + entered_rungs: 0, #[cfg(windows)] d3d11_import: false, #[cfg(windows)] @@ -723,10 +2125,41 @@ impl Decoder { }) } + /// Drain the "please ask the host for an IDR" flag — the pump calls this each iteration + /// (throttled) so a demoted/erroring decoder can resynchronize under the infinite GOP. pub fn take_keyframe_request(&mut self) -> bool { std::mem::take(&mut self.want_keyframe) } + /// Install a rung: swap the backend in and reset everything that describes the OLD + /// one's health, in one place. + /// + /// It exists because M9 doubled the number of demotion targets, and every one of + /// them has to clear the same four things — the error streak, its start stamp, the + /// delivered flag, and (new) the [`Self::entered_rungs`] bookkeeping the walk's + /// termination depends on. Four call sites each doing it by hand is how one of them + /// eventually forgets the bit and the ladder starts looping. + fn install(&mut self, backend: Backend) { + self.entered_rungs |= rung_bit(&backend); + self.backend = backend; + self.vaapi_fails = 0; + self.first_fail = None; + self.delivered = false; + } + + /// Is the running rung the platform's NATIVE hardware rung (native VAAPI on Linux, + /// native D3D11VA on Windows)? The one demotion candidate that goes "sideways" — + /// into native Vulkan — fires only from here. + fn is_native_platform_rung(&self) -> bool { + #[cfg(target_os = "linux")] + let it = matches!(self.backend, Backend::NativeVaapi(_)); + #[cfg(windows)] + let it = matches!(self.backend, Backend::NativeD3d11va(_)); + #[cfg(not(any(target_os = "linux", windows)))] + let it = false; + it + } + /// Demote to software decode on the PRESENTER's verdict (dmabuf presentation impossible: /// GL converter init failed, texture import rejected). Decode itself succeeds in that /// state, so the error-streak demotion never fires — without this the stream would stay @@ -736,9 +2169,9 @@ impl Decoder { return Ok(()); } tracing::warn!("presenter can't display hardware frames — demoting to software decode"); - self.backend = Backend::Software(SoftwareDecoder::new(self.codec_id)?); - self.vaapi_fails = 0; - self.first_fail = None; + // Same typed refusal as every other software-rung construction: on an HEVC + // session there is nothing below this and the pump reconnects. + self.install(Backend::Software(SoftwareDecoder::new(self.wire_codec)?)); self.want_keyframe = true; Ok(()) } @@ -769,15 +2202,62 @@ impl Decoder { user_flags: u32, complete: bool, ) -> Result> { + // Did THIS AU come back as a concealment — an `Ok(None)` the native rung + // produced because the picture was damaged, not because the decoder was + // buffering? Only the native rung can answer, and the answer decides + // whether the `Ok` below is allowed to clear the demotion streak. + let mut concealed = false; let result = match &mut self.backend { - Backend::Vulkan(v) => { + Backend::NativeVulkan(n) => { debug_assert!(complete, "partial AUs are pyrowave-only"); - v.decode(au).map(|f| f.map(DecodedImage::VkFrame)) + let r = n.decode(au).map(|f| f.map(DecodedImage::NativeVk)); + // STREAM damage is not a decoder fault, and must not ride the + // demotion streak. + // + // The distinction exists only because these rungs can SEE damage — + // and that is precisely what makes it dangerous. libavcodec's rungs + // concealed a lost reference silently and kept their job; if a + // native rung turned the same event into an error, three of them + // over a second would demote the program's own headline decoder + // exactly on the lossy links it was built to diagnose. So + // concealment comes back as `Ok(None)` plus this flag: the pump + // still asks for a re-anchor at the same moment and through the + // same throttle it always did, and the hardware rung survives the + // loss that caused it. + // + // A driver `RESULT_STATUS` verdict of Failed is NOT routed here — + // it stays an `Err` below. That one really is a statement about + // the decoder ("I could not decode what I was given"), and a + // driver making it repeatedly is the exact case demotion exists + // for; it is also the Xbox Ally X shape. + if n.take_recovery_request() { + self.want_keyframe = true; + concealed = true; + } + r } #[cfg(target_os = "linux")] - Backend::Vaapi(v) => v.decode(au).map(|f| f.map(DecodedImage::Dmabuf)), + Backend::NativeVaapi(v) => { + debug_assert!(complete, "partial AUs are pyrowave-only"); + let r = v.decode(au).map(|f| f.map(DecodedImage::NativeDmabuf)); + // Same concealment split as the Vulkan rung above, for the same reason. + if v.take_recovery_request() { + self.want_keyframe = true; + concealed = true; + } + r + } #[cfg(windows)] - Backend::D3d11va(d) => d.decode(au).map(|f| f.map(DecodedImage::D3d11)), + Backend::NativeD3d11va(d) => { + debug_assert!(complete, "partial AUs are pyrowave-only"); + let r = d.decode(au).map(|f| f.map(DecodedImage::D3d11)); + // Same concealment split as the Vulkan rung above, for the same reason. + if d.take_recovery_request() { + self.want_keyframe = true; + concealed = true; + } + r + } // No demote ladder below PyroWave (nothing else decodes it): propagate the // error; the pump surfaces it and the session falls back to HEVC by // renegotiation (plan §4.6), not by decoder swap. @@ -792,69 +2272,140 @@ impl Decoder { }; match result { Ok(f) => { - self.vaapi_fails = 0; - self.first_fail = None; + // Only an answer that PROVES the rung works may clear the streak — + // see [`clears_demotion_streak`] for the whole argument. + if clears_demotion_streak(f.is_some(), concealed) { + self.vaapi_fails = 0; + self.first_fail = None; + } + self.delivered |= f.is_some(); Ok(f) } Err(e) => { let which = match self.backend { - Backend::Vulkan(_) => "Vulkan Video", + Backend::NativeVulkan(_) => "native Vulkan Video", #[cfg(windows)] - Backend::D3d11va(_) => "D3D11VA", - _ => "VAAPI", + Backend::NativeD3d11va(_) => "native D3D11VA", + #[cfg(target_os = "linux")] + Backend::NativeVaapi(_) => "native VAAPI", + // PyroWave returns above and software never reaches here. + _ => "hardware", }; self.vaapi_fails += 1; self.want_keyframe = true; let first = *self.first_fail.get_or_insert_with(std::time::Instant::now); if self.vaapi_fails >= VAAPI_DEMOTE_AFTER && first.elapsed() >= HW_DEMOTE_MIN_STREAK { - // A failing Vulkan backend still has a hardware rung below it on - // Linux — demote to VAAPI first (user-reported: FFmpeg-Vulkan-on-Mesa - // error-streaking where VAAPI streams perfectly); only when that - // can't be built either does the session land on software. + // ⚠ A native rung that never delivered a single frame is not a + // failing decoder — it is a decoder the session never had (usually a + // stream shape THIS DEVICE cannot host: `NativeVulkanDecoder::new`'s + // probe catches what the negotiation can see, but a level above the + // device's `maxLevelIdc`, or an SPS that disagrees with the Welcome, + // only surfaces here). Such a rung must not cost the session the rung + // BELOW it as well. + // + // Until M10 that needed an explicit arm: native Vulkan fell through + // to FFmpeg-Vulkan first, because demoting past it would have taken a + // 4K HEVC session on NVIDIA/Linux — where VAAPI is unusable — straight + // to the CPU. The arm is gone with FFmpeg-Vulkan, and the property now + // holds structurally: the rung directly below native Vulkan is the + // platform's own native rung, and that is exactly the next candidate + // this walk tries. `self.delivered` is kept for the streak accounting + // and for the record, not for a branch. + // + // The platform's NATIVE hardware rung, first, exactly as in + // `Decoder::new`'s ladder. `entered_rungs` is what keeps the walk + // monotone: the two native rungs sit in opposite orders per vendor, so + // without it a demotion could climb back into a rung that already failed. #[cfg(target_os = "linux")] - if matches!(self.backend, Backend::Vulkan(_)) { - match VaapiDecoder::new(self.codec_id) { - Ok(v) => { - tracing::warn!(error = %e, fails = self.vaapi_fails, - decoder = v.name(), - "Vulkan Video decode failing repeatedly — demoting to VAAPI"); - self.backend = Backend::Vaapi(v); - self.vaapi_fails = 0; - self.first_fail = None; - return Ok(None); + if self.entered_rungs & RUNG_BIT_NATIVE_PLATFORM == 0 { + if let Some(codec) = native_vaapi_codec(self.wire_codec) { + match crate::video_vaapi_native::NativeVaapiDecoder::new( + codec, + self.stream, + ) { + Ok(d) => { + tracing::warn!(error = %e, fails = self.vaapi_fails, + from = which, decoder = d.name(), + "hardware decode failing repeatedly — demoting to \ + native VAAPI"); + self.install(Backend::NativeVaapi(Box::new(d))); + return Ok(None); + } + Err(va) => tracing::info!(reason = %format!("{va:#}"), + "native VAAPI unavailable for demotion — continuing down \ + the ladder"), } - Err(va) => tracing::info!(reason = %va, - "VAAPI unavailable for demotion — software decode"), } } - // Windows' hardware rung below Vulkan is D3D11VA (a 4K120 stream is - // not survivable on software) — same-GPU rebuild via the stashed LUID. #[cfg(windows)] - if matches!(self.backend, Backend::Vulkan(_)) && self.d3d11_import { - match crate::video_d3d11::D3d11vaDecoder::new( - self.codec_id, - self.adapter_luid, - self.d3d11_hdr10, - ) { - Ok(d) => { - tracing::warn!(error = %e, fails = self.vaapi_fails, - decoder = d.name(), - "Vulkan Video decode failing repeatedly — demoting to D3D11VA"); - self.backend = Backend::D3d11va(d); - self.vaapi_fails = 0; - self.first_fail = None; - return Ok(None); + if self.entered_rungs & RUNG_BIT_NATIVE_PLATFORM == 0 && self.d3d11_import { + if let Some(codec) = native_d3d11_codec(self.wire_codec) { + match crate::video_d3d11_native::NativeD3d11Decoder::new( + codec, + self.stream, + self.adapter_luid, + self.d3d11_hdr10, + ) { + Ok(d) => { + tracing::warn!(error = %e, fails = self.vaapi_fails, + from = which, decoder = d.name(), + "hardware decode failing repeatedly — demoting to \ + native D3D11VA"); + self.install(Backend::NativeD3d11va(Box::new(d))); + return Ok(None); + } + Err(dx) => tracing::info!(reason = %format!("{dx:#}"), + "native D3D11VA unavailable for demotion — continuing down \ + the ladder"), + } + } + } + // The last hardware candidate, and the one that only exists because + // M9 stacked two native rungs: a failing native PLATFORM rung on an + // Intel/unknown box has native Vulkan BELOW it (that vendor order + // puts the platform rung first), and there is nothing between them. + // Without this the rung with the weakest evidence in the whole + // program — native VAAPI, which has decoded nothing anywhere — would + // take a 4K session straight to the CPU rung the moment it + // error-streaked. Only fires FROM a native platform rung. + if self.entered_rungs & RUNG_BIT_NATIVE_VULKAN == 0 + && self.is_native_platform_rung() + { + if let Some(v) = self.vk.clone().filter(|v| v.video_decode) { + if native_vulkan_gate( + "auto", + self.wire_codec, + true, + v.decode_video_caps, + ) { + let (codec, _) = + native_codec(self.wire_codec).expect("the gate admitted it"); + match NativeVulkanDecoder::new(&v, codec, self.stream) { + Ok(n) => { + tracing::warn!(error = %e, fails = self.vaapi_fails, + from = which, + "hardware decode failing repeatedly — demoting to \ + native Vulkan Video"); + self.install(Backend::NativeVulkan(Box::new(n))); + return Ok(None); + } + Err(nv) => tracing::info!(reason = %format!("{nv:#}"), + "native Vulkan Video unavailable for demotion — \ + software decode"), + } } - Err(dx) => tracing::info!(reason = %dx, - "D3D11VA unavailable for demotion — software decode"), } } tracing::warn!(error = %e, fails = self.vaapi_fails, "{which} decode failing repeatedly — demoting to software"); - self.backend = Backend::Software(SoftwareDecoder::new(self.codec_id)?); - self.vaapi_fails = 0; - self.first_fail = None; + // The ladder's bottom. On H.264/AV1 this always builds; on HEVC it + // NEVER does, and the `?` carries the typed `NoSoftwareRung` up to + // the pump, which reconnects with HEVC-less caps instead of leaving + // the session on a rung that cannot decode a single AU. That + // substitution — a refusal where a silently useless decoder used to + // sit — is the whole reason the drop of software HEVC is safe. + self.install(Backend::Software(SoftwareDecoder::new(self.wire_codec)?)); } else { tracing::debug!(backend = which, error = %e, "decode error — requesting keyframe, keeping hardware decode"); @@ -865,30 +2416,24 @@ impl Decoder { } } -// -EAGAIN. FFmpeg uses POSIX errno values on both our targets (MinGW's EAGAIN is 11 too). -pub(crate) const AVERROR_EAGAIN: i32 = -11; - -pub(crate) fn averr(what: &str, code: i32) -> anyhow::Error { - anyhow!("{what}: {}", ffmpeg::Error::from(code)) -} - /// Guard-less mutex serializing every `vkQueueSubmit`/`vkQueuePresentKHR`/ -/// `vkQueueWaitIdle` on the device the presenter shares with FFmpeg. +/// `vkQueueWaitIdle` on the device the presenter shares with the decode lane. /// -/// Why it exists: the presenter created the device with ONE graphics-family queue and -/// told FFmpeg's `AVVulkanDeviceContext` to use that same family (`nb_graphics_queues -/// = 1` ⇒ queue index 0) for its transfer/compute prep work — so the presenter thread -/// and the session pump thread were submitting to the SAME `VkQueue` with no shared -/// lock. `vkQueueSubmit` requires external synchronization on the queue; the race -/// surfaced as intermittent `VK_ERROR_DEVICE_LOST` at exactly the moments FFmpeg puts -/// work on the graphics queue (decoder open / frames-context rebuild — i.e. stream -/// start and every adaptive-bitrate encoder rebuild; live-diagnosed 2026-07-09). +/// Why it exists: the presenter creates the device with ONE graphics-family queue, and +/// the session pump thread submits decode/CSC prep work to that SAME `VkQueue` from a +/// different thread. `vkQueueSubmit` requires external synchronization on the queue; the +/// race surfaced as intermittent `VK_ERROR_DEVICE_LOST` at exactly the moments the decode +/// lane put work on the graphics queue — decoder open and frames-context rebuild, i.e. +/// stream start and every adaptive-bitrate encoder rebuild (live-diagnosed 2026-07-09, +/// on the FFmpeg-Vulkan rung, whose `AVVulkanDeviceContext` was configured with +/// `nb_graphics_queues = 1` ⇒ queue index 0). /// -/// FFmpeg's hook for this is the `lock_queue`/`unlock_queue` callback pair on -/// `AVVulkanDeviceContext` — a raw lock/unlock shape with no RAII scope, hence this -/// guard-less primitive (`std::sync::Mutex`'s guard can't cross the C callbacks). -/// Contention is a handful of µs-scale critical sections per frame; a plain -/// Mutex+Condvar is more than enough. +/// It is guard-less because FFmpeg's hook for this was a raw `lock_queue`/`unlock_queue` +/// callback PAIR with no RAII scope (`std::sync::Mutex`'s guard can't cross a C callback). +/// That consumer is gone; the shape stays because the presenter, the Skia overlay and the +/// native decode lane all still share the queue, and [`QueueLock::guard`] gives Rust +/// callers the RAII form. Contention is a handful of µs-scale critical sections per +/// frame; a plain Mutex+Condvar is more than enough. pub struct QueueLock { locked: std::sync::Mutex, cv: std::sync::Condvar, @@ -945,17 +2490,19 @@ impl Drop for QueueLockGuard<'_> { } } -/// The presenter's Vulkan device handles, exported so FFmpeg's Vulkan Video decoder -/// runs on the SAME device the presenter samples from — the whole point: the decoded -/// VkImage is composited directly, no interop, no copy (plan: Vulkan Video phase). +/// The presenter's Vulkan device handles, exported so the DECODE lane runs on the SAME +/// device the presenter samples from — the whole point: the decoded VkImage is composited +/// directly, no interop, no copy (plan: Vulkan Video phase). /// -/// Plain integers/strings on purpose: pf-client-core has no ash dependency; pf-ffvk -/// casts these into vulkan.h handle types when filling `AVVulkanDeviceContext`. All -/// handles stay valid for the presenter's lifetime, which outlives every session pump -/// (the run loop tears the pump down before the presenter). +/// Plain integers/strings on purpose: pf-client-core has no ash dependency, so the +/// consumers (`video_vk_native` → pf-vkdecode, `video_pyrowave` → pyrowave-sys) cast +/// these back into handle types themselves. All handles stay valid for the presenter's +/// lifetime, which outlives every session pump (the run loop tears the pump down before +/// the presenter). #[derive(Clone)] pub struct VulkanDecodeDevice { - /// `PFN_vkGetInstanceProcAddr` from the loader — FFmpeg resolves everything else. + /// `PFN_vkGetInstanceProcAddr` from the loader — the decode lanes resolve everything + /// else through it. pub get_instance_proc_addr: usize, pub instance: usize, pub physical_device: usize, @@ -966,16 +2513,16 @@ pub struct VulkanDecodeDevice { /// The driver's device-name string (e.g. "AMD RADV VANGOGH") — the VanGogh/Deck /// detection for [`Self::prefer_vulkan_first`]. pub device_name: String, - /// The presenter's graphics+present family (FFmpeg's "required" tx/comp family too). + /// The presenter's graphics+present family. pub graphics_qf: u32, - /// Raw `VkQueueFlags` of that family (the qf[] entry wants the real capabilities). - pub graphics_queue_flags: u32, - /// The video-decode family (may equal `graphics_qf` on some hardware). + /// The video-decode family (may equal `graphics_qf` on some hardware — which is a + /// case the native rung must detect; see `video_vk_native::submit_queues_collide`). pub decode_qf: u32, /// Raw `VkVideoCodecOperationFlagsKHR` the decode family advertises. pub decode_video_caps: u32, - /// Everything enabled at instance/device creation — FFmpeg keys code paths off the - /// extension STRINGS, so the lists must match reality exactly. + /// Everything enabled at instance/device creation. The pyrowave decoder replays these + /// lists verbatim into its pinned create-info reconstruction, so they must match + /// reality exactly. pub instance_extensions: Vec, pub device_extensions: Vec, /// Features enabled at device creation (reported via `device_features`). @@ -983,8 +2530,8 @@ pub struct VulkanDecodeDevice { pub f_timeline_semaphore: bool, pub f_synchronization2: bool, /// Vulkan Video decode is actually usable on this device (decode queue + extensions + - /// features). The bundle now exists even without it — Windows D3D11 interop rides the - /// same struct — so consumers gate the FFmpeg-Vulkan decoder on THIS, not on `Some`. + /// features). The bundle exists even without it — Windows D3D11 interop rides the + /// same struct — so consumers gate the Vulkan decode rung on THIS, not on `Some`. pub video_decode: bool, /// The presenter has REAL on-glass present timing (`VK_KHR_present_wait` — its /// `PresentTimer` runs). Gates the `CLIENT_CAP_PHASE_LOCK` advertisement: without a @@ -1020,8 +2567,8 @@ pub struct VulkanDecodeDevice { /// GPUs. `None` when not reported (or off Windows, where it's unused). pub adapter_luid: Option<[u8; 8]>, /// The device's shared queue lock (see [`QueueLock`]). The presenter holds it around - /// its own submits/presents; the decoder wires it into FFmpeg's - /// `lock_queue`/`unlock_queue` callbacks so both sides serialize on the same queues. + /// its own submits/presents and every decode lane takes it around its own, so both + /// sides serialize on the same queues. pub queue_lock: std::sync::Arc, } @@ -1040,8 +2587,11 @@ impl VulkanDecodeDevice { /// /// Intel and unknown vendors take the battle-tested path first: VAAPI on Linux (ANV's /// Vulkan Video is the least-proven Mesa path), D3D11VA on Windows — Intel's Windows - /// driver advertises Vulkan Video (Arc drivers since 2023), but FFmpeg-Vulkan on it is - /// field-broken (B580, 2026-07: strobing + ~7 ms decodes) where DXVA streams clean. + /// driver advertises Vulkan Video (Arc drivers since 2023), but Vulkan decode on it + /// was field-broken (B580, 2026-07: strobing + ~7 ms decodes) where DXVA streamed + /// clean. ⚠ That measurement was taken on the FFmpeg-Vulkan rung, which M10 deleted; + /// no Intel box has run pf-vkdecode. The order stands until something is measured, + /// because "the old evidence no longer applies" is not evidence. pub fn prefer_vulkan_first(&self) -> bool { const VENDOR_NVIDIA: u32 = 0x10DE; const VENDOR_AMD: u32 = 0x1002; @@ -1049,31 +2599,236 @@ impl VulkanDecodeDevice { } } -/// `fourcc(a,b,c,d)` — the DRM FourCC packing (little-endian, `a | b<<8 | c<<16 | d<<24`). -const fn fourcc(a: u8, b: u8, c: u8, d: u8) -> u32 { - (a as u32) | ((b as u32) << 8) | ((c as u32) << 16) | ((d as u32) << 24) -} - -/// The combined DRM FourCC for a decoder software pixel format. The host streams 8-bit -/// 4:2:0 (NV12); P010 is here for the eventual 10-bit/HDR path. -// Only the (Linux-gated) VAAPI path calls this outside tests; the constants are worth -// locking on every platform, so it stays compiled rather than cfg-gated with its caller. -#[cfg_attr(windows, allow(dead_code))] -pub(crate) fn drm_fourcc_for(sw: ffmpeg_next::ffi::AVPixelFormat) -> Option { - use ffmpeg_next::ffi::AVPixelFormat::*; - Some(match sw { - AV_PIX_FMT_NV12 => fourcc(b'N', b'V', b'1', b'2'), - AV_PIX_FMT_P010LE => fourcc(b'P', b'0', b'1', b'0'), - // Full-chroma 4:4:4 semi-planar (HEVC RExt decode on drivers that export it as - // two planes) — the presenter imports the full-size chroma plane like any other. - AV_PIX_FMT_NV24 => fourcc(b'N', b'V', b'2', b'4'), - _ => return None, - }) -} - #[cfg(test)] mod tests { use super::*; + use punktfunk_core::quic::{CODEC_AV1, CODEC_H264, CODEC_HEVC, CODEC_PYROWAVE}; + + /// The reconnect rule, as the invariant it is: an exhausted codec must come back as + /// one this client can decode ALL THE WAY DOWN, and must never come back as itself. + /// + /// This is the "first-class path" the risk register asks for, tested where it can be + /// tested exhaustively — the on-glass half needs a host, a GPU and a decode failure + /// nobody can schedule. + #[test] + fn an_exhausted_codec_reconnects_only_onto_one_with_a_cpu_rung() { + let sw = software_decodable_codecs(); + assert_eq!(sw, CODEC_H264 | CODEC_AV1, "M8's CPU rung set"); + assert_eq!(sw & CODEC_HEVC, 0, "software HEVC is what M8 dropped"); + + // The shipping case: a desktop advertises H.264+HEVC, HEVC runs out of rungs. + assert_eq!( + last_rung_verdict(CODEC_HEVC, CODEC_H264 | CODEC_HEVC, RungLoss::Codec), + LastRungVerdict::Retry { caps: CODEC_H264 } + ); + // With hardware AV1 also advertised, both survivors stay on the table — the host + // picks; we only ever REMOVE. + assert_eq!( + last_rung_verdict( + CODEC_HEVC, + CODEC_H264 | CODEC_HEVC | CODEC_AV1, + RungLoss::Codec + ), + LastRungVerdict::Retry { + caps: CODEC_H264 | CODEC_AV1 + } + ); + // A client that offered HEVC alone has nowhere to go: reconnecting would + // negotiate the same dead end, so say so instead of looping. + assert_eq!( + last_rung_verdict(CODEC_HEVC, CODEC_HEVC, RungLoss::Codec), + LastRungVerdict::Dead + ); + // The retry NEVER re-offers the codec that just failed... + for advertised in 0u8..16 { + for negotiated in [CODEC_H264, CODEC_HEVC, CODEC_AV1] { + if let LastRungVerdict::Retry { caps } = + last_rung_verdict(negotiated, advertised, RungLoss::Codec) + { + assert_eq!(caps & negotiated, 0, "{negotiated:#x} re-offered"); + // ...and, when the CODEC is what has no CPU rung, never offers one + // that would reach the same refusal a session later. + assert_eq!(caps & !software_decodable_codecs(), 0); + assert_ne!(caps, 0, "Retry must carry something to advertise"); + } + } + } + // PyroWave is not in the software set and never reaches this rule (its sessions + // renegotiate the codec on failure instead of demoting) — but if it ever did, the + // answer must be Dead, not a retry that offers a codec with no CPU decoder. + assert_eq!( + last_rung_verdict(CODEC_PYROWAVE, CODEC_PYROWAVE, RungLoss::Codec), + LastRungVerdict::Dead + ); + } + + /// A picture SHAPE the CPU rung cannot decode is not "this codec has no CPU rung", + /// and the review found the rule conflating them: a 4:4:4 H.264 session ended with + /// "no other codec is available" while an HEVC retry — whose hardware rungs never + /// even ran — would have worked. + #[test] + fn a_shape_refusal_may_retry_onto_a_codec_with_no_cpu_rung() { + // The one that used to die. HEVC has no CPU rung, but nothing about HEVC failed: + // this client asked for 4:4:4, the host resolved it, and only the CPU DECODER is + // 4:2:0-only. A reconnect without H.264 re-resolves the shape too. + assert_eq!( + last_rung_verdict(CODEC_H264, CODEC_H264 | CODEC_HEVC, RungLoss::Shape), + LastRungVerdict::Retry { caps: CODEC_HEVC } + ); + // Same inputs, the OTHER diagnosis: hardware H.264 exhausted and the CPU rung + // has no H.264 at all (impossible in this build, but the rule must not depend on + // that) — then HEVC really is the same losing bet and the session ends. + assert_eq!( + last_rung_verdict(CODEC_H264, CODEC_H264 | CODEC_HEVC, RungLoss::Codec), + LastRungVerdict::Dead + ); + // The user's PyroWave opt-in survives a shape refusal — but never ALONE: the + // host's `resolve_codec` keeps PyroWave out of its precedence ladder, so a Hello + // offering nothing else resolves to no codec and the host refuses the session. + assert_eq!( + last_rung_verdict( + CODEC_H264, + CODEC_H264 | CODEC_HEVC | CODEC_PYROWAVE, + RungLoss::Shape + ), + LastRungVerdict::Retry { + caps: CODEC_HEVC | CODEC_PYROWAVE + } + ); + assert_eq!( + last_rung_verdict(CODEC_H264, CODEC_H264 | CODEC_PYROWAVE, RungLoss::Shape), + LastRungVerdict::Dead + ); + // And a shape refusal still never re-offers the codec that raised it — the codec + // is fixed at Welcome, so it is the only lever there is. + for advertised in 0u8..16 { + for negotiated in [CODEC_H264, CODEC_HEVC, CODEC_AV1] { + if let LastRungVerdict::Retry { caps } = + last_rung_verdict(negotiated, advertised, RungLoss::Shape) + { + assert_eq!(caps & negotiated, 0, "{negotiated:#x} re-offered"); + assert_ne!(caps, 0, "Retry must carry something to advertise"); + } + } + } + } + + /// The one HEVC promise the client can refuse to make BEFORE the Hello: decode + /// pinned to software has no HEVC rung at any level, so advertising it guarantees + /// the reconnect flow rather than risking it. + #[test] + fn a_software_pin_takes_hevc_off_the_advertisement() { + // The pin is read the way `Decoder::new` reads it: env first, then the setting — + // so a run with the override actually set has nothing here to assert about. + if std::env::var_os("PUNKTFUNK_DECODER").is_some() { + return; + } + assert!(decode_pinned_to_software("software")); + assert!(!decode_pinned_to_software("auto")); + assert!(!decode_pinned_to_software("vulkan")); + assert!(!decode_pinned_to_software("")); + } + + /// A settings file written before M10 must still stream. + /// + /// This is the upgrade path, not a nicety: all three desktop Settings UIs offered + /// `vulkan` / `vaapi` / `d3d11va` as decoder choices, so those strings are sitting in + /// shipped users' stores right now. They named **libavcodec's** rungs, which M10 + /// deleted — and the pre-M10 code answered an unavailable named rung with a hard + /// error. Left as-is, an upgrade would have ended every one of those sessions with a + /// message about a decoder the user never chose by that name. + /// + /// So each maps onto the rung that replaced it, and the mapping is checked as a pair: + /// the LEGACY name must move, and the `native-*` names must NOT (they were always the + /// exact pins, and a migration that rewrote them would be a second bug). + #[test] + fn a_pre_m10_decoder_preference_migrates_onto_its_native_rung() { + for (stored, want) in [ + ("vulkan", "native-vulkan"), + ("vaapi", "native-vaapi"), + ("d3d11va", "native-d3d11va"), + ] { + assert_eq!(migrate_decoder_pref(stored), want, "stored {stored:?}"); + } + // Everything else is passed through verbatim — including `auto`/``/`hardware` + // (the auto family `native_vulkan_gate` matches on), `software`, the three exact + // pins, and a value this build has never heard of, which must reach the ladder + // unchanged so it falls through to the CPU rung rather than becoming a silent + // hardware pin. + for pass in [ + "auto", + "", + "hardware", + "software", + "native-vulkan", + "native-vaapi", + "native-d3d11va", + "something-else", + ] { + assert_eq!(migrate_decoder_pref(pass), pass, "passthrough {pass:?}"); + } + // The migrated names are exactly the pin constants the ladder compares against — + // a typo here would read as "some unknown decoder" and land the session on auto. + assert_eq!(migrate_decoder_pref("vulkan"), "native-vulkan"); + #[cfg(target_os = "linux")] + assert_eq!( + migrate_decoder_pref("vaapi"), + crate::video_vaapi_native::DECODER_PIN + ); + #[cfg(windows)] + assert_eq!( + migrate_decoder_pref("d3d11va"), + crate::video_d3d11_native::DECODER_PIN + ); + // …and the migrated Vulkan name is one `native_vulkan_gate` admits, on a device + // that can run the codec. (`decode_pinned_to_software` above pins the other + // direction: none of these is the software pin.) + assert!(native_vulkan_gate( + &migrate_decoder_pref("vulkan"), + CODEC_H264, + true, + VIDEO_CODEC_OP_DECODE_H264 + )); + } + + /// `CpuPlanarFrame` is what the presenter uploads with no stride: prove the copy + /// really does undo the decoder's padding, and that a short plane is REFUSED rather + /// than read past. + #[test] + fn planar_frames_are_tightly_packed_and_short_planes_are_refused() { + let color = ColorDesc { + primaries: 1, + transfer: 1, + matrix: 1, + full_range: false, + }; + // 4x2 luma, 2x1 chroma, all planes padded by 3 bytes per row. + let y: Vec = vec![1, 2, 3, 4, 9, 9, 9, 5, 6, 7, 8, 9, 9, 9]; + let u: Vec = vec![10, 11, 9, 9, 9]; + let v: Vec = vec![20, 21, 9, 9, 9]; + let none = punktfunk_core::reanchor::LocalRecovery::NONE; + let f = + CpuPlanarFrame::from_i420(4, 2, [&y, &u, &v], [7, 5, 5], color, true, none).unwrap(); + assert_eq!(f.plane(0), &[1, 2, 3, 4, 5, 6, 7, 8]); + assert_eq!(f.plane(1), &[10, 11]); + assert_eq!(f.plane(2), &[20, 21]); + assert_eq!(f.plane_dims(0), (4, 2)); + assert_eq!(f.plane_dims(1), (2, 1)); + // Odd dimensions round the chroma plane UP — the last column/row still has a + // chroma sample and dropping it would read past the plane on the next frame. + assert_eq!(CpuPlanarFrame::chroma_dims(5, 3), (3, 2)); + // A plane shorter than its own geometry is a disagreement with the decoder, not + // something to truncate into a plausible picture. + let short: Vec = vec![1, 2, 3]; + assert!( + CpuPlanarFrame::from_i420(4, 2, [&short, &u, &v], [7, 5, 5], color, true, none) + .is_err() + ); + // A stride narrower than the picture is the same class of disagreement. + assert!( + CpuPlanarFrame::from_i420(4, 2, [&y, &u, &v], [2, 5, 5], color, true, none).is_err() + ); + } fn decode_device(vendor_id: u32, device_name: &str) -> VulkanDecodeDevice { VulkanDecodeDevice { @@ -1084,7 +2839,6 @@ mod tests { vendor_id, device_name: device_name.into(), graphics_qf: 0, - graphics_queue_flags: 0, decode_qf: 0, decode_video_caps: 0, instance_extensions: Vec::new(), @@ -1109,6 +2863,97 @@ mod tests { } } + /// The demotion streak's escape hatch, stated as the invariant it is: an `Ok` + /// clears the streak only when it PROVES the rung works. + /// + /// Concealment (`Ok(None)` with a recovery request) proves nothing — it is the + /// STREAM that was damaged — and before M4's review it cleared the streak + /// anyway, because the `Ok(_)` arm matched `Ok(None)` too. Two shapes followed + /// from that, and this test pins both away: + /// + /// * a driver failing every other AU on a lossy link: `Err` / concealment / + /// `Err` / concealment … the concealment zeroed the count and + /// [`VAAPI_DEMOTE_AFTER`] was never reached; + /// * and a rung that conceals forever and ships nothing: a frozen picture with + /// no path down the ladder at all. + #[test] + fn only_an_answer_that_proves_the_rung_works_clears_the_demotion_streak() { + // A shipped frame is proof, concealed or not (the AU carried damage AND a + // picture — the decoder is plainly alive). + assert!(clears_demotion_streak(true, false)); + assert!(clears_demotion_streak(true, true)); + // A CLEAN no-output AU is proof too: the decoder ran and objected to + // nothing (it buffered, or skipped an H.265 RASL picture after an open-GOP + // join). Treating that as suspicious would demote healthy sessions. + assert!(clears_demotion_streak(false, false)); + // Concealment with no picture is the one that proves nothing. + assert!(!clears_demotion_streak(false, true)); + + // The streak arithmetic that follows, spelled out on the milder and + // likelier shape: a broken driver alternating with concealment must still + // reach the demotion threshold. + let mut fails = 0u32; + for concealed_ok in [false, true, false, true, false] { + if concealed_ok { + if clears_demotion_streak(false, true) { + fails = 0; + } + } else { + fails += 1; // an Err from the driver's own verdict + } + } + assert!( + fails >= VAAPI_DEMOTE_AFTER, + "three driver errors interleaved with concealment must still reach the \ + demotion threshold — they got to {fails}" + ); + + // ---- The AV1 shape (M7), and the reason its recovery wait is an `Err` ---- + // + // A native rung waiting to re-anchor after a failure produces no picture for + // every AU of the wait, and all three codecs say so with an ERROR: H.264 and + // H.265 through their planners' `PlanError::AwaitingIdr`, AV1 through + // `VkDecodeError::AwaitingKeyAv1`. So the streak ticks for the whole wait and + // a rung that never recovers reaches the threshold. + let mut fails = 0u32; + for errored in [true; 5] { + // the failing AU, then four skipped ones + if errored { + fails += 1; + } else if clears_demotion_streak(false, false) { + fails = 0; + } + } + assert!(fails >= VAAPI_DEMOTE_AFTER); + + // The counterfactual is the whole point, and it is what the AV1 rung was + // first wired as: answer the skipped AUs with a CLEAN `Ok(None)` instead — + // no picture, no warnings, nothing to object to — and every one of them + // clears the streak. The `Err` from each failure is then alone, and + // `VAAPI_DEMOTE_AFTER` is unreachable no matter how long the session runs. + // + // The stream this strands is real and named in `NativeVulkanDecoder::new`: + // an AV1 sequence with `film_grain_params_present = 1` on a device without + // the grain decode profile fails at `ensure_state` — at EVERY key frame, and + // only at a key frame. Key frame `Err`, inter frames "clean", next key frame + // `Err`: a frozen screen for the whole session, `refused N · damaged 0 · + // run 0` on the stats line, and the `!delivered` fall-through to + // FFmpeg-Vulkan below never reached. + let mut fails = 0u32; + for errored in [true, false, false, true, false, false, true, false, false] { + if errored { + fails += 1; + } else if clears_demotion_streak(false, false) { + fails = 0; + } + } + assert!( + fails < VAAPI_DEMOTE_AFTER, + "a recovery wait answered as a CLEAN AU zeroes the streak once per frame \ + — which is why it must not be answered that way; it got to {fails}" + ); + } + /// Auto's hardware order (both OSes): Vulkan-first on NVIDIA (on Linux: no usable /// VAAPI) and ALL AMD (Vulkan decode outperforms VAAPI on RADV — on-glass verdict; /// VanGogh additionally chroma-fringes over VAAPI); Intel/unknown take the proven @@ -1132,23 +2977,520 @@ mod tests { assert!(!decode_device(0x8086, "Intel(R) Arc(TM) Pro Graphics").prefer_vulkan_first()); } - /// Lock the DRM FourCC magic numbers against typos — these are the exact values - /// `` defines, and a wrong one is what painted the Steam Deck green. + /// AV1 is advertised on a HARDWARE fact, never on a decoder existing. + /// + /// The standing open item M7 closes. `ffmpeg::decoder::find(AV1)` says yes + /// wherever libdav1d is linked, so the old advertisement told the host "send me + /// AV1" on machines that would then decode it on the CPU — and codec negotiation + /// happens once, so there is no falling back afterwards. #[test] - fn drm_fourcc_constants() { - assert_eq!(fourcc(b'N', b'V', b'1', b'2'), 0x3231_564e); - assert_eq!(fourcc(b'P', b'0', b'1', b'0'), 0x3031_3050); + fn av1_is_advertised_only_where_hardware_can_decode_it() { + // No device at all: no claim. + assert!(!av1_hardware_decodable(None)); + + // A decode-capable device that does NOT list AV1 among its codec + // operations. `video_decode` alone is not the question — plenty of devices + // decode H.264 and H.265 and no AV1. + let mut dev = decode_device(0x10de, "no-av1"); + dev.decode_video_caps = VIDEO_CODEC_OP_DECODE_H264 | VIDEO_CODEC_OP_DECODE_H265; + #[cfg(not(windows))] + assert!( + !av1_hardware_decodable(Some(&dev)), + "H.264+H.265 decode support says nothing about AV1" + ); + + // The AV1 operation bit is the yes. + let mut dev = decode_device(0x1002, "vangogh-ish"); + dev.decode_video_caps = + VIDEO_CODEC_OP_DECODE_H264 | VIDEO_CODEC_OP_DECODE_H265 | VIDEO_CODEC_OP_DECODE_AV1; + assert!(av1_hardware_decodable(Some(&dev))); + + // A device whose decode queue is absent cannot be taken at its caps word. + let mut dev = decode_device(0x1002, "no-decode-queue"); + dev.decode_video_caps = VIDEO_CODEC_OP_DECODE_AV1; + dev.video_decode = false; + #[cfg(not(windows))] + assert!(!av1_hardware_decodable(Some(&dev))); + } + + /// The native-Vulkan admission gate (WP-C, widened by the 2026-08-05 ladder + /// decision, by M3 WP-2's HEVC wiring and by M7's AV1 wiring): the pin AND the auto + /// family admit on a capable H.264, HEVC **or AV1** session, every explicit + /// other-backend pin refuses (a `native-vaapi` pin must not land on Vulkan just + /// because the device could), and the + /// codec/device legs still refuse for every choice. The codec's OWN caps bit is the + /// device leg: admitting HEVC on an H.264-only decode family would create a video + /// session for an operation the family cannot run, which is undefined behaviour + /// rather than an error. + /// A pin with stray whitespace is still a pin, and the gate must accept it. + /// + /// This is a regression test with a field cost: `"native-vulkan "` (one trailing + /// space, which a Windows `.cmd` adds for free) matched no arm of + /// `native_vulkan_gate`, so the rung fell through to `auto` with nothing logged — + /// on a box where `auto` picks a different rung, that reads exactly like the pin + /// being refused for a hardware reason. The second half is what makes it a *shared* + /// rule: `decode_pinned_to_software` reads the same variable, and its own docs say + /// a second reading is a second place to drift. + #[test] + fn a_decoder_pin_survives_the_whitespace_a_shell_script_adds() { assert_eq!( - drm_fourcc_for(ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_NV12), - Some(0x3231_564e) + resolve_decoder_pref(Some("native-vulkan "), "auto"), + "native-vulkan", + "a trailing space must not turn a pin into an unrecognised value" ); assert_eq!( - drm_fourcc_for(ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_NV24), - Some(0x3432_564e) + resolve_decoder_pref(Some(" software\t"), "auto"), + "software" + ); + // Trimmed to nothing means ABSENT — fall back to the stored setting rather than + // pinning to "", which the gate would otherwise accept as the auto family. + assert_eq!( + resolve_decoder_pref(Some(" "), "native-vaapi"), + "native-vaapi" ); assert_eq!( - drm_fourcc_for(ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_RGBA), - None + resolve_decoder_pref(Some(""), "native-vaapi"), + "native-vaapi" + ); + assert_eq!(resolve_decoder_pref(None, "native-vaapi"), "native-vaapi"); + // …and the trimmed value is what the gate actually admits. + assert!( + native_vulkan_gate( + &resolve_decoder_pref(Some("native-vulkan "), "auto"), + punktfunk_core::quic::CODEC_HEVC, + true, + VIDEO_CODEC_OP_DECODE_H265, + ), + "the whole point: the trimmed pin reaches the gate and is admitted" + ); + } + + #[test] + fn native_vulkan_gate_admits_pin_and_auto_family_per_codec_on_a_capable_family() { + // Pin the raw spec values, not the implementation constants — a typo'd bit + // would refuse every real driver's caps and native would silently never + // engage (the program's own nb_queries=0 lesson: silent non-engagement is + // the failure mode nothing flags). + assert_eq!( + VIDEO_CODEC_OP_DECODE_H264, 0x1, + "VK_VIDEO_CODEC_OPERATION_DECODE_H264_BIT_KHR" + ); + assert_eq!( + VIDEO_CODEC_OP_DECODE_H265, 0x2, + "VK_VIDEO_CODEC_OPERATION_DECODE_H265_BIT_KHR" + ); + assert_eq!( + VIDEO_CODEC_OP_DECODE_AV1, 0x4, + "VK_VIDEO_CODEC_OPERATION_DECODE_AV1_BIT_KHR" + ); + const H264_OP: u32 = VIDEO_CODEC_OP_DECODE_H264; + const H265_OP: u32 = VIDEO_CODEC_OP_DECODE_H265; + const AV1_OP: u32 = VIDEO_CODEC_OP_DECODE_AV1; + for choice in ["native-vulkan", "auto", "", "hardware"] { + // The pin and the whole auto family admit both codecs pf-vkdecode + // speaks, on a family that advertises the matching op… + assert!( + native_vulkan_gate(choice, CODEC_H264, true, H264_OP), + "{choice:?}" + ); + assert!( + native_vulkan_gate(choice, CODEC_HEVC, true, H265_OP), + "{choice:?}" + ); + // …including the ordinary case of a family that runs both. + assert!( + native_vulkan_gate(choice, CODEC_H264, true, H264_OP | H265_OP), + "{choice:?}" + ); + assert!( + native_vulkan_gate(choice, CODEC_HEVC, true, H264_OP | H265_OP), + "{choice:?}" + ); + // Each codec needs ITS OWN bit: an H.264-only family (the common case on + // older silicon) must not take an HEVC session, and vice versa. + assert!( + !native_vulkan_gate(choice, CODEC_HEVC, true, H264_OP), + "{choice:?}" + ); + assert!( + !native_vulkan_gate(choice, CODEC_H264, true, H265_OP), + "{choice:?}" + ); + // AV1 joined the auto family at M9, on the evidence rule and not on a + // date: `native_evidence(Vulkan, CODEC_AV1)` is verified (250/250 + // bit-identical to libavcodec on an RTX 5070 Ti, M7). Before M9 this pair + // asserted `choice == "native-vulkan"`; the flip is what changed, and it + // changed HERE. + assert!( + native_vulkan_gate(choice, CODEC_AV1, true, AV1_OP), + "{choice:?}" + ); + assert!( + native_vulkan_gate(choice, CODEC_AV1, true, H264_OP | H265_OP | AV1_OP), + "{choice:?}" + ); + // …and the pin is still not a licence to skip the device leg: an AV1 + // session on a family that does not advertise the AV1 op would create a + // video session for an operation the family cannot run. + assert!( + !native_vulkan_gate(choice, CODEC_AV1, true, H264_OP | H265_OP), + "{choice:?}" + ); + assert!( + !native_vulkan_gate(choice, CODEC_AV1, false, AV1_OP), + "{choice:?}" + ); + // No Vulkan-Video-capable presenter device. + assert!( + !native_vulkan_gate(choice, CODEC_H264, false, H264_OP), + "{choice:?}" + ); + assert!( + !native_vulkan_gate(choice, CODEC_HEVC, false, H265_OP), + "{choice:?}" + ); + // A decode family advertising NO codec op, or only a foreign one, + // refuses even with the extension stack present — the caps BIT is the + // codec gate, not `video_decode`. + assert!( + !native_vulkan_gate(choice, CODEC_H264, true, 0), + "{choice:?}" + ); + assert!( + !native_vulkan_gate(choice, CODEC_HEVC, true, 0), + "{choice:?}" + ); + assert!( + !native_vulkan_gate(choice, CODEC_H264, true, AV1_OP), + "{choice:?}" + ); + assert!( + !native_vulkan_gate(choice, CODEC_HEVC, true, AV1_OP), + "{choice:?}" + ); + } + // Never for an explicit OTHER-backend pin, capable device or not. + // NOTE: the pre-M10 `vulkan`/`vaapi`/`d3d11va` spellings never reach this gate — + // `migrate_decoder_pref` rewrites them first — so what is asserted here is the + // gate's own rule: a pin naming ANOTHER backend is not a licence to run this one. + for choice in ["native-vaapi", "native-d3d11va", "software"] { + assert!( + !native_vulkan_gate(choice, CODEC_H264, true, H264_OP), + "{choice:?}" + ); + assert!( + !native_vulkan_gate(choice, CODEC_HEVC, true, H265_OP), + "{choice:?}" + ); + assert!( + !native_vulkan_gate(choice, CODEC_AV1, true, AV1_OP), + "{choice:?}" + ); + } + // The decoder the gate implies — the construction sites `expect()` this + // exact agreement, so a codec admitted with no decoder behind it would be a + // panic rather than a demotion. + assert_eq!( + native_codec(CODEC_H264).map(|(c, _)| c), + Some(NativeCodec::H264) + ); + assert_eq!( + native_codec(CODEC_HEVC).map(|(c, _)| c), + Some(NativeCodec::H265) + ); + // AV1 has a decoder AND the caps bit here — being in this map is what the + // pin construction path reads. Whether `auto` may pick it is the gate's + // decision above, and deliberately not this one's. + assert_eq!( + native_codec(CODEC_AV1), + Some((NativeCodec::Av1, VIDEO_CODEC_OP_DECODE_AV1)) + ); + assert!(native_codec(CODEC_PYROWAVE).is_none()); + assert!(native_codec(0).is_none()); + } + + /// The evidence table, asserted as the FACT it is — which rung/codec pairs have + /// actually decoded on hardware and which have not. + /// + /// This test is the reason the table can be trusted a milestone from now. M9 turned + /// native rungs on by default and M10 deleted every libavcodec rung beneath them; the + /// argument for doing that honestly rests entirely on the claim "these five pairs are + /// proven and these six are not", and on the session log saying so. A + /// table nobody checks drifts into a table that says everything is fine — which is + /// the exact failure this whole program exists to end, one layer up. + #[test] + fn the_evidence_table_says_exactly_which_rungs_have_run_on_hardware() { + for (rung, codec, what) in [ + ( + NativeRung::Vulkan, + CODEC_H264, + "native Vulkan H.264 (M2 WP-D)", + ), + (NativeRung::Vulkan, CODEC_HEVC, "native Vulkan H.265 (M3)"), + ( + NativeRung::Vulkan, + CODEC_AV1, + "native Vulkan AV1 (M7, RTX 5070 Ti)", + ), + (NativeRung::D3d11va, CODEC_H264, "native D3D11VA H.264 (M5)"), + (NativeRung::D3d11va, CODEC_HEVC, "native D3D11VA H.265 (M5)"), + ] { + assert!( + native_evidence(rung, codec).verified, + "{what} has hardware parity recorded" + ); + } + for (rung, codec, why) in [ + ( + NativeRung::D3d11va, + CODEC_AV1, + "the DXVA AV1 leg never ran (M7)", + ), + ( + NativeRung::Vaapi, + CODEC_H264, + "no VAAPI device was reachable", + ), + ( + NativeRung::Vaapi, + CODEC_HEVC, + "no VAAPI device was reachable", + ), + ( + NativeRung::Vaapi, + CODEC_AV1, + "no VAAPI device was reachable", + ), + ( + NativeRung::Software, + CODEC_H264, + "openh264 never ran on glass", + ), + (NativeRung::Software, CODEC_AV1, "rav1d never ran on glass"), + ] { + assert!( + !native_evidence(rung, codec).verified, + "{why} — claiming otherwise is the dishonesty this program must not ship" + ); + } + // A codec leg nobody wrote an arm for reads as UNVERIFIED, never as its + // neighbour's evidence: the next codec this program grows must land in the + // session log's WARNING branch by default, not by somebody remembering to add a + // row. + assert!(!native_evidence(NativeRung::Vulkan, CODEC_PYROWAVE).verified); + assert!(!native_evidence(NativeRung::Software, CODEC_HEVC).verified); + assert!(!native_evidence(NativeRung::D3d11va, 0).verified); + // Every answer explains itself in the session log. + for rung in [ + NativeRung::Vulkan, + NativeRung::D3d11va, + NativeRung::Vaapi, + NativeRung::Software, + ] { + for codec in [CODEC_H264, CODEC_HEVC, CODEC_AV1, 0] { + assert!( + !native_evidence(rung, codec).note.is_empty(), + "{} / {codec} must carry a note", + rung.name() + ); + } + } + } + + /// M10's admission rule, stated as the pair of facts it is: an unproven rung runs + /// wherever the only thing below it is the CPU, and wherever it runs it is NAMED — + /// because naming it is the only protection left there. + /// + /// The four pairs below are the unproven ones. `log_rung` turns exactly these into a + /// `warn` line carrying their note, and that line is what a field report about M10 gets + /// read against. Which of them `auto` may pick FIRST is + /// [`native_rung_admitted`]'s decision, asserted in the test after this one. + #[test] + fn every_rung_runs_and_the_unproven_ones_are_named() { + let unproven = [ + (NativeRung::Vaapi, CODEC_H264), + (NativeRung::Vaapi, CODEC_HEVC), + (NativeRung::Vaapi, CODEC_AV1), + (NativeRung::D3d11va, CODEC_AV1), + ]; + for (rung, codec) in unproven { + let e = native_evidence(rung, codec); + assert!( + !e.verified, + "{} / {codec:#x} is claimed proven — if a hardware run really happened, \ + move it into the verified half of the table on purpose", + rung.name() + ); + assert!( + e.note.contains("NEVER") || e.note.contains("never"), + "{} / {codec:#x}: the note is what the session log prints at warn — it \ + must say plainly that nothing has run it, got {:?}", + rung.name(), + e.note + ); + } + // ...and the pairs that ARE proven stay proven. Nothing about M10 changes what + // hardware has run; deleting the filter must not quietly relabel the evidence. + for (rung, codec) in [ + (NativeRung::Vulkan, CODEC_H264), + (NativeRung::Vulkan, CODEC_HEVC), + (NativeRung::Vulkan, CODEC_AV1), + (NativeRung::D3d11va, CODEC_H264), + (NativeRung::D3d11va, CODEC_HEVC), + ] { + assert!(native_evidence(rung, codec).verified, "{}", rung.name()); + } + } + + /// The evidence FILTER: which rung `auto` may pick first, given what is under it. + /// + /// This is the rule that keeps M10 from shipping a default no hardware has ever run. + /// The case it exists for is a Linux Intel (or unknown-vendor) desktop: the vendor + /// order puts native VAAPI first, pf-vaadec has decoded nothing anywhere, and directly + /// below it sits native Vulkan Video with three drivers, a 92-minute soak and 250/250 + /// AV1 behind it. A rung that produces WRONG PIXELS leaves only through the error-streak + /// demotion, and the field has already shown that streak failing to trip (the B580's + /// strobing between clean anchors and corrupt inter frames) — so "it will demote if it + /// misbehaves" is not a guarantee, and the choice has to be made BEFORE the session + /// runs. Hence: yield to proven code when there is proven code to yield to, and only + /// then. + #[test] + fn an_unproven_rung_yields_to_a_proven_one_and_to_nothing_else() { + // The Linux Intel/unknown arm: VAAPI first, native Vulkan Video under it. Every + // codec pf-vaadec speaks is unproven on it, and every one of them is proven on the + // rung below — so `auto` takes none of them. + for codec in [CODEC_H264, CODEC_HEVC, CODEC_AV1] { + assert!( + !native_rung_admitted(NativeRung::Vaapi, codec, Some(NativeRung::Vulkan)), + "codec {codec:#x}: a never-run VAAPI rung must not go first when the \ + device can run the proven Vulkan rung for it" + ); + // …and the same rung IS admitted when there is nothing proven below it: on + // NVIDIA/AMD it is reached after Vulkan, and on a box whose Vulkan device + // cannot run this codec at all the fall would be to the CPU. Taking hardware + // decode away to protect a session from an unproven decoder is the worse answer. + assert!( + native_rung_admitted(NativeRung::Vaapi, codec, None), + "codec {codec:#x}: with only the CPU below, the unproven rung runs" + ); + // It yields to PROVEN code, not to any code: the CPU rung has never run on + // glass either, so it is not something to fall onto in preference. + assert!(native_rung_admitted( + NativeRung::Vaapi, + codec, + Some(NativeRung::Software) + )); + } + // A rung with hardware behind it is admitted whatever is below it — that is what + // the evidence was collected for, and the filter must never demote a proven rung. + for (rung, codec) in [ + (NativeRung::Vulkan, CODEC_H264), + (NativeRung::Vulkan, CODEC_HEVC), + (NativeRung::Vulkan, CODEC_AV1), + (NativeRung::D3d11va, CODEC_H264), + (NativeRung::D3d11va, CODEC_HEVC), + ] { + for below in [ + None, + Some(NativeRung::Vulkan), + Some(NativeRung::Vaapi), + Some(NativeRung::Software), + ] { + assert!( + native_rung_admitted(rung, codec, below), + "{} / {codec:#x} is proven and must run", + rung.name() + ); + } + } + // Windows, Intel/unknown auto: the DXVA AV1 leg has never run, and the ladder + // passes `None` there on purpose — that vendor family is the one with a measured + // wrong-pixel report against Vulkan decode, so what is really below it is the CPU. + // This asserts the ARGUMENT the call site passes, which is where the judgement + // lives; `Some(Vulkan)` would bar it, and that is deliberately not what it passes. + assert!(native_rung_admitted(NativeRung::D3d11va, CODEC_AV1, None)); + assert!(!native_rung_admitted( + NativeRung::D3d11va, + CODEC_AV1, + Some(NativeRung::Vulkan) + )); + // The CPU rung is last everywhere, so nothing is ever below it and it always runs + // — including for a codec it has no decoder for, which is `last_rung_verdict`'s + // problem and not the filter's. + for codec in [CODEC_H264, CODEC_HEVC, CODEC_AV1] { + assert!(native_rung_admitted(NativeRung::Software, codec, None)); + } + } + + /// The device half of the filter: "native Vulkan Video is below me" is a claim about + /// THIS GPU, not about the ladder's shape. + /// + /// Without it the Linux Intel arm would bar VAAPI on a box whose Vulkan device cannot + /// decode the session's codec — no decode family, or a family without that codec's + /// operation — and hand the session to the CPU to protect it from a rung it needed. + #[test] + fn the_rung_below_must_be_one_this_device_can_actually_run() { + const H264_OP: u32 = VIDEO_CODEC_OP_DECODE_H264; + const AV1_OP: u32 = VIDEO_CODEC_OP_DECODE_AV1; + // The ordinary Mesa/Intel case: a decode family that advertises this codec. + assert!(native_vulkan_usable(CODEC_H264, true, H264_OP)); + // No Vulkan Video at all, and a family that runs some OTHER codec: neither is a + // rung to fall onto. + assert!(!native_vulkan_usable(CODEC_H264, false, H264_OP)); + assert!(!native_vulkan_usable(CODEC_H264, true, AV1_OP)); + assert!(!native_vulkan_usable(CODEC_H264, true, 0)); + // A codec no native rung speaks (PyroWave rides its own path) is not a Vulkan rung. + assert!(!native_vulkan_usable(CODEC_PYROWAVE, true, u32::MAX)); + // Composed, this is the whole Linux Intel decision, both ways round: an H.264 + // session on an H.264-capable device takes Vulkan; an AV1 session on that same + // device has no proven rung below VAAPI, so VAAPI runs (and warns). + let below = + |wire, caps| native_vulkan_usable(wire, true, caps).then_some(NativeRung::Vulkan); + assert!(!native_rung_admitted( + NativeRung::Vaapi, + CODEC_H264, + below(CODEC_H264, H264_OP) + )); + assert!(native_rung_admitted( + NativeRung::Vaapi, + CODEC_AV1, + below(CODEC_AV1, H264_OP) + )); + } + + /// What this client advertises it can decode is a statement about OUR rungs, and it + /// did not move when the FFmpeg rungs were deleted. + /// + /// That invariance is the point: the wire's codec negotiation is a promise, M10 + /// deleted the FFmpeg rungs, and a client whose Hello changed on that deletion would + /// have renegotiated every session in the field for a refactor. It used to be a + /// libavcodec registry walk, which would have answered differently — and, worse, + /// would have answered about decoders the ladder never reaches. + #[test] + fn advertised_codecs_describe_our_rungs_and_not_libavcodecs_registry() { + let bits = decodable_codecs(); + assert_eq!( + bits, + CODEC_H264 | CODEC_HEVC | CODEC_AV1, + "the three codecs the native rungs speak" + ); + assert_eq!( + bits & CODEC_PYROWAVE, + 0, + "pyrowave rides decodable_codecs_for" + ); + // The CPU rung's codecs are a subset — the ladder must never advertise a codec + // whose LAST rung it does not have... except HEVC, which is the one deliberate + // exception this module documents at length. + assert_eq!( + software_decodable_codecs() & !bits, + 0, + "a codec with a CPU rung but no advertisement would be unreachable" + ); + assert_eq!( + bits & !software_decodable_codecs(), + CODEC_HEVC, + "HEVC is the ONE advertised codec with no CPU rung (last_rung_verdict owns it)" ); } } diff --git a/crates/pf-client-core/src/video_color.rs b/crates/pf-client-core/src/video_color.rs index 87be7439..3201b72c 100644 --- a/crates/pf-client-core/src/video_color.rs +++ b/crates/pf-client-core/src/video_color.rs @@ -1,13 +1,12 @@ //! The stream's per-frame colour signalling (`ColorDesc`) + the Y′CbCr→RGB CSC matrix (`csc_rows`). -#![allow(clippy::unnecessary_cast)] -use ffmpeg_next as ffmpeg; - -/// The stream's colour signaling, read PER-FRAME from the decoder (HEVC VUI → the -/// `AVFrame` CICP fields). The Windows host switches an HDR desktop to Main10 BT.2020 PQ -/// **in-band** (the Welcome still says SDR — clients are expected to follow the VUI, as -/// the Windows/Apple/Android clients do), so rendering must follow the frames, not the -/// handshake — else PQ content drawn as BT.709 comes out washed out and desaturated. +/// The stream's colour signaling, read PER-FRAME out of the bitstream's own VUI / +/// sequence header (pf-bitstream, on every rung — the libavcodec `AVFrame` CICP read this +/// used to have went with M10's rungs). The Windows host switches an HDR desktop to +/// Main10 BT.2020 PQ **in-band** (the Welcome still says SDR — clients are expected to +/// follow the VUI, as the Windows/Apple/Android clients do), so rendering must follow the +/// frames, not the handshake — else PQ content drawn as BT.709 comes out washed out and +/// desaturated. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct ColorDesc { /// H.273 code points as signaled (2 = unspecified → the renderer picks the SDR default). @@ -18,24 +17,6 @@ pub struct ColorDesc { } impl ColorDesc { - /// Read the CICP fields off a raw decoded frame. Public: the Windows client's raw-FFI - /// D3D11VA/software decoders build their per-frame `ColorDesc` with it too (same - /// `ffmpeg-next` major, so the `AVFrame` type unifies across the workspace). - /// - /// # Safety - /// `frame` must point to a valid `AVFrame` (alive for the duration of the call). - pub unsafe fn from_raw(frame: *const ffmpeg::ffi::AVFrame) -> ColorDesc { - // SAFETY: caller guarantees a live AVFrame; these are plain enum field reads. - unsafe { - ColorDesc { - primaries: (*frame).color_primaries as u32 as u8, - transfer: (*frame).color_trc as u32 as u8, - matrix: (*frame).colorspace as u32 as u8, - full_range: (*frame).color_range == ffmpeg::ffi::AVColorRange::AVCOL_RANGE_JPEG, - } - } - } - /// PQ (SMPTE ST.2084) transfer — the HDR10 signal. pub fn is_pq(&self) -> bool { self.transfer == 16 @@ -55,7 +36,9 @@ impl ColorDesc { /// `65535/65472` recovers exact `code/1023`. pub fn csc_rows(desc: ColorDesc, depth: u8, msb_packed: bool) -> [[f32; 4]; 3] { // BT.601 (5/6), BT.2020 (9/10); everything else — incl. unspecified — is the host's - // BT.709 SDR default (mirrors the software path's swscale coefficient choice). + // BT.709 SDR default. Since M8 this is the ONLY coefficient choice in the client: + // the software rung's swscale (which defaulted to BT.601 and needed correcting) is + // gone, and its planes come through this function like every hardware lane's. let (kr, kb) = match desc.matrix { 5 | 6 => (0.299, 0.114), 9 | 10 => (0.2627, 0.0593), diff --git a/crates/pf-client-core/src/video_d3d11.rs b/crates/pf-client-core/src/video_d3d11.rs index de87e32f..d85be9ce 100644 --- a/crates/pf-client-core/src/video_d3d11.rs +++ b/crates/pf-client-core/src/video_d3d11.rs @@ -1,18 +1,26 @@ -//! D3D11VA hardware decode (Windows) for the Vulkan presenter — the vendor-agnostic DXVA -//! path, and auto's FIRST choice on Intel/unknown vendors. Intel's Windows driver DOES -//! advertise Vulkan Video (Arc drivers since 2023 — don't trust the capability gate to -//! keep Intel off it), but FFmpeg-Vulkan on it is field-broken (B580, 2026-07: strobing + -//! ~7 ms decodes) where this path streams clean; on NVIDIA/AMD it is the fallback rung -//! below Vulkan Video, in `auto` and via mid-session demotion. +//! The D3D11 side of the DXVA rung (Windows): the decode DEVICE and the shareable +//! hand-off ring the decoded surfaces are converted into. Auto's first choice on +//! Intel/unknown vendors — Intel's Windows driver DOES advertise Vulkan Video (Arc drivers +//! since 2023 — don't trust the capability gate to keep Intel off it), but Vulkan decode +//! on it was field-broken (B580, 2026-07: strobing + ~7 ms decodes) where this path +//! streams clean; on NVIDIA/AMD it is the fallback rung below Vulkan Video, in `auto` and +//! via mid-session demotion. +//! +//! **What decodes into these surfaces is [`crate::video_d3d11_native`]** (M5: pf-dxvadec +//! plans driven into `ID3D11VideoDecoder`). This module held libavcodec's D3D11VA hwaccel +//! as well until M10 excised FFmpeg from the client; what is left is the half both rungs +//! always shared, and it is the field-proven half. //! //! Ported from the retired in-process WinUI presenter's decoder (`clients/windows/src/video.rs`) //! with one structural change: that presenter sampled D3D11 textures directly, while ours draws //! with Vulkan. Bridging rules, all learned the hard way there: //! -//! * The **decode pool stays libavcodec-derived** (`get_format` sets no frames context): a -//! hand-built pool validated on NVIDIA was rejected by Intel at the first -//! `SubmitDecoderBuffers` — and Intel is the GPU this backend exists for. That also means the -//! decode surfaces carry no share flags, so they can't be imported into Vulkan directly. +//! * The decode POOL is not built here. libavcodec's rung let libavcodec derive it +//! (`get_format` set no frames context) after a hand-built pool validated on NVIDIA was +//! rejected by Intel at the first `SubmitDecoderBuffers` — and Intel is the GPU this +//! backend exists for; the native rung declares its own pool in `video_d3d11_native`, +//! against pf-dxvadec's pinned bind flags. Either way the decode surfaces carry no share +//! flags, so they can't be imported into Vulkan directly — hence the ring below. //! * Each decoded slice goes through the fixed-function **`ID3D11VideoProcessor`** //! (`VideoProcessorBlt`, NV12/P010 → BGRA8 — the conversion every Windows video player //! exercises on every vendor) into a small ring of **shareable RGBA textures** created with @@ -35,14 +43,14 @@ //! //! The decode device is created on the **presenter's adapter** (matched by the Vulkan device's //! LUID) so the shared textures never cross GPUs on a multi-adapter box. +//! +//! Device creation ([`create_device`]) and the video-processor ring ([`HandoffRing`]) are +//! `pub(crate)` because `video_d3d11_native` is their only consumer. use crate::video::ColorDesc; -use crate::video_libav::AvBuffer; -use anyhow::{anyhow, bail, Context as _, Result}; -use ffmpeg_next as ffmpeg; -use std::ffi::c_void; +use anyhow::{anyhow, Context as _, Result}; use std::ptr; -use windows::core::{Interface, GUID}; +use windows::core::Interface; use windows::Win32::d3d11::{ D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Multithread, ID3D11Texture2D, ID3D11VideoContext1, ID3D11VideoDevice, ID3D11VideoProcessor, ID3D11VideoProcessorEnumerator, @@ -63,9 +71,8 @@ use windows::Win32::dxgi::{ DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P601, DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709, DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020, DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P2020, DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P601, DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709, - DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_NV12, DXGI_FORMAT_P010, - DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_RATIONAL, DXGI_SAMPLE_DESC, DXGI_SHARED_RESOURCE_READ, - DXGI_SHARED_RESOURCE_WRITE, + DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_RATIONAL, DXGI_SAMPLE_DESC, + DXGI_SHARED_RESOURCE_READ, DXGI_SHARED_RESOURCE_WRITE, }; use windows::Win32::windef::RECT; use windows::Win32::winnt::HANDLE; @@ -81,19 +88,6 @@ const RING_SLOTS: usize = 6; /// (which demotes to software) instead of wedging the decode loop. const ACQUIRE_TIMEOUT_MS: u32 = 2000; -/// Probe pool size — mirrors what libavcodec sizes for a worst-case DPB (legacy value). -const DECODE_POOL_SIZE: i32 = 12; - -/// `D3D11_BIND_DECODER` — the decode pool's ONLY bind flag (see `get_format_d3d11`). -const BIND_DECODER: u32 = 0x200; - -// DXVA decode-profile GUIDs (`dxva.h`), defined locally so no extra windows-rs feature or -// metadata surface is pulled in for four constants. -const PROFILE_H264_VLD_NOFGT: GUID = GUID::from_u128(0x1b81be68_a0c7_11d3_b984_00c04f2e73c5); -const PROFILE_HEVC_VLD_MAIN: GUID = GUID::from_u128(0x5b11d51b_2f4c_4452_bcc3_09f2a1160cc0); -const PROFILE_HEVC_VLD_MAIN10: GUID = GUID::from_u128(0x107af0e0_ef1a_4d19_aba8_67a163073d13); -const PROFILE_AV1_VLD_PROFILE0: GUID = GUID::from_u128(0xb8be4ccb_cf53_46ba_8d59_d6b8a6da5d2a); - /// One decoded frame, parked in a ring slot the presenter imports by NT handle. Plain POD — /// the ring (and its handles) belong to the decoder and outlive every in-flight frame; the /// presenter must NOT close the handle. Cross-API exclusion + visibility ride the slot's @@ -110,7 +104,7 @@ pub struct D3d11Frame { /// pass-through flavor) — the presenter's Vulkan import must match it exactly. pub rgb10: bool, /// Intra keyframe (IDR/I) — the pump's post-loss re-anchor signal. See - /// `crate::video::VkVideoFrame`. + /// [`crate::video::DecodedImage::is_keyframe`]. pub keyframe: bool, /// The ring slot's NT shared handle (`IDXGIResource1::CreateSharedHandle`), stable for the /// ring's lifetime. Raw `isize` so the frame crosses the pump→presenter channel. @@ -121,171 +115,20 @@ pub struct D3d11Frame { pub generation: u32, } -// --- FFmpeg hwcontext_d3d11va ABI (repr(C) mirrors, same as the legacy decoder) -------------- - -/// `hwcontext_d3d11va.h` — `AVHWDeviceContext::hwctx` for D3D11VA. FFmpeg installs the -/// `ID3D11Multithread` default lock + multithread protection during init, which is what lets -/// the presenter-side device share textures with the decode thread safely. -#[repr(C)] -struct AVD3D11VADeviceContext { - device: *mut c_void, // ID3D11Device* - device_context: *mut c_void, // ID3D11DeviceContext* - video_device: *mut c_void, // ID3D11VideoDevice* - video_context: *mut c_void, // ID3D11VideoContext* - lock: *mut c_void, // void (*)(void*) - unlock: *mut c_void, // void (*)(void*) - lock_ctx: *mut c_void, -} - -/// `hwcontext_d3d11va.h` — `AVHWFramesContext::hwctx`. A user-built frames context gets NO -/// default bind flags (BindFlags 0 → `CreateTexture2D` E_INVALIDARG); only the probe below -/// builds one, and it sets `BIND_DECODER` exactly like libavcodec's own path. -#[repr(C)] -struct AVD3D11VAFramesContext { - texture: *mut c_void, // ID3D11Texture2D* (null → FFmpeg allocates the pool) - bind_flags: u32, // UINT BindFlags - misc_flags: u32, // UINT MiscFlags - texture_infos: *mut c_void, // AVD3D11FrameDescriptor* (FFmpeg-managed) -} - -// Hand-written mirrors of libav's `AVD3D11VADeviceContext` / `AVD3D11VAFramesContext` -// (hwcontext_d3d11va.h) — `ffmpeg-sys-next` binds neither, and we WRITE `device` / `bind_flags` -// through them, so a wrong offset is silent corruption of libav's context rather than a compile -// error. ⚠ These two structs are duplicated in the other crate that talks to the same libav -// contexts (pf-encode's `ffmpeg_win.rs` and pf-client-core's `video_d3d11.rs`); they must agree -// with libav AND with each other, and these assertions are what makes a drift in either a build -// failure instead of a runtime mystery. -const _: () = { - use std::mem::{offset_of, size_of}; - type P = *mut c_void; - assert!(size_of::() == 7 * size_of::

()); - assert!(offset_of!(AVD3D11VADeviceContext, device) == 0); - assert!(offset_of!(AVD3D11VADeviceContext, device_context) == size_of::

()); - assert!(offset_of!(AVD3D11VADeviceContext, video_device) == 2 * size_of::

()); - assert!(offset_of!(AVD3D11VADeviceContext, video_context) == 3 * size_of::

()); - assert!(offset_of!(AVD3D11VADeviceContext, lock) == 4 * size_of::

()); - assert!(offset_of!(AVD3D11VADeviceContext, unlock) == 5 * size_of::

()); - assert!(offset_of!(AVD3D11VADeviceContext, lock_ctx) == 6 * size_of::

()); - // ptr, u32, u32, ptr — the two 32-bit flags pack into one pointer-sized slot with no padding. - assert!(size_of::() == 3 * size_of::

()); - assert!(offset_of!(AVD3D11VAFramesContext, texture) == 0); - assert!(offset_of!(AVD3D11VAFramesContext, bind_flags) == size_of::

()); - assert!(offset_of!(AVD3D11VAFramesContext, misc_flags) == size_of::

() + 4); - assert!(offset_of!(AVD3D11VAFramesContext, texture_infos) == 2 * size_of::

()); -}; - -fn averr(what: &str, code: i32) -> anyhow::Error { - anyhow!("{what}: {}", ffmpeg::Error::from(code)) -} - -/// libavcodec's `get_format` callback: pick the D3D11 hw surface format and nothing else. -/// Deliberately does NOT build a frames context — with `hw_device_ctx` set and `hw_frames_ctx` -/// left null, libavcodec derives the decode pool itself (`ff_decode_get_hw_frames_ctx`), -/// applying every vendor quirk: DXVA surface alignment (128 for HEVC/AV1), DPB-based pool -/// sizing, and the decoder-only `D3D11_BIND_DECODER` flags. A hand-built context validated on -/// NVIDIA was rejected by Intel at the first `SubmitDecoderBuffers` (E_INVALIDARG) — the -/// vendor-proof path is the one the ffmpeg CLI/mpv ship. -unsafe extern "C" fn get_format_d3d11( - avctx: *mut ffmpeg::ffi::AVCodecContext, - mut list: *const ffmpeg::ffi::AVPixelFormat, -) -> ffmpeg::ffi::AVPixelFormat { - use ffmpeg::ffi::*; - // SAFETY: libav calls this `get_format` callback with a context and a list it owns; the list - // is terminated by `AV_PIX_FMT_NONE`, so the walk stays inside it, and `avctx`'s fields are - // read/set only while libav holds it live for the call. - unsafe { - if (*avctx).hw_device_ctx.is_null() { - return AVPixelFormat::AV_PIX_FMT_NONE; - } - while *list != AVPixelFormat::AV_PIX_FMT_NONE { - if *list == AVPixelFormat::AV_PIX_FMT_D3D11 { - return AVPixelFormat::AV_PIX_FMT_D3D11; - } - list = list.add(1); - } - AVPixelFormat::AV_PIX_FMT_NONE - } -} - -/// Does the adapter expose a DXVA decode profile for `codec_id`? Checked before building the -/// FFmpeg hwdevice because hwaccel selection (`get_format`) only runs on the FIRST access -/// unit — an unsupported profile would otherwise burn the opening IDR and recover through the -/// mid-stream demotion path instead of committing to software up front. -fn decode_profile_supported(device: &ID3D11Device, codec_id: ffmpeg::codec::Id) -> Result<()> { - let video: ID3D11VideoDevice = device - .cast() - .context("device lacks ID3D11VideoDevice (created without VIDEO_SUPPORT)")?; - // SAFETY: COM calls on the live `ID3D11VideoDevice` obtained by the checked `cast` above; the - // count bounds the loop and each profile is returned by value. - let profiles: Vec = unsafe { - let n = video.GetVideoDecoderProfileCount(); - (0..n) - .filter_map(|i| video.GetVideoDecoderProfile(i).ok()) - .collect() - }; - let (wanted, format, name): (GUID, DXGI_FORMAT, &str) = match codec_id { - ffmpeg::codec::Id::H264 => (PROFILE_H264_VLD_NOFGT, DXGI_FORMAT_NV12, "H.264 VLD NoFGT"), - ffmpeg::codec::Id::HEVC => (PROFILE_HEVC_VLD_MAIN, DXGI_FORMAT_NV12, "HEVC Main"), - ffmpeg::codec::Id::AV1 => (PROFILE_AV1_VLD_PROFILE0, DXGI_FORMAT_NV12, "AV1 Profile 0"), - other => bail!("no DXVA profile known for {other:?}"), - }; - let ok = profiles.contains(&wanted) - // SAFETY: same live video device; the two arguments are a borrowed local GUID and a plain - // format enum. - && unsafe { video.CheckVideoDecoderFormat(&wanted, format) } - .map(|b| b.as_bool()) - .unwrap_or(false); - if !ok { - bail!("adapter exposes no {name} decode profile"); - } - // 10-bit (a mid-session HDR upgrade needs Main10): informational — if it's missing, the - // decode error → software demotion + keyframe re-request path covers the switch. - if codec_id == ffmpeg::codec::Id::HEVC { - let main10 = profiles.contains(&PROFILE_HEVC_VLD_MAIN10) - // SAFETY: as above — borrowed static GUID plus a plain format enum. - && unsafe { video.CheckVideoDecoderFormat(&PROFILE_HEVC_VLD_MAIN10, DXGI_FORMAT_P010) } - .map(|b| b.as_bool()) - .unwrap_or(false); - tracing::info!(main10, "HEVC Main10 (10-bit/HDR) decode profile"); - } - Ok(()) -} - -/// Predict whether D3D11VA decode will work by doing EXACTLY what the decoder's `get_format` -/// leads to — allocate an `AVHWFramesContext` (decoder-only pool) and initialize it, which -/// creates the real NV12 decode surface array. On a GPU/driver that can't create the pool this -/// fails here, up front, so the session commits to software from the first frame (a clean, -/// gap-free stream) instead of dying mid-stream on the opening IDR. -unsafe fn d3d11va_decode_supported(hw_device: *mut ffmpeg::ffi::AVBufferRef) -> bool { - use ffmpeg::ffi::*; - // SAFETY: `hw_device` is a valid `AVBufferRef` by this fn's contract; the frames context is - // allocated, configured and released within this scope, and every libav return is checked - // before use. - unsafe { - // Scope-bound: this probe owns the frames ctx for the length of the check and the drop - // below releases it on BOTH exits, instead of the early return relying on the null case and - // the success path unref'ing by hand. - let Some(frames_ref) = AvBuffer::from_raw(av_hwframe_ctx_alloc(hw_device)) else { - return false; - }; - let frames = (*frames_ref.as_ptr()).data as *mut AVHWFramesContext; - (*frames).format = AVPixelFormat::AV_PIX_FMT_D3D11; - (*frames).sw_format = AVPixelFormat::AV_PIX_FMT_NV12; - (*frames).width = 1920; - (*frames).height = 1152; // 128-aligned 1080p surface (the HEVC DXVA alignment) - (*frames).initial_pool_size = DECODE_POOL_SIZE; - let fhw = (*frames).hwctx as *mut AVD3D11VAFramesContext; - (*fhw).bind_flags = BIND_DECODER; - let r = av_hwframe_ctx_init(frames_ref.as_ptr()); - r >= 0 - } -} +// ⚠ This struct carried a `native: bool` until M10, because TWO rungs filled the ring — +// libavcodec's D3D11VA hwaccel and `video_d3d11_native` — and both delivered +// `DecodedImage::D3d11`, so the `stats:` decode-path tag read `d3d11va` for either and no +// soak log could tell them apart (fixed in `1573a987`). With the libavcodec rung deleted +// there is one filler, the tag is unconditionally `native-d3d11va`, and a permanently-true +// flag would be a claim nothing can falsify. If a second rung ever shares this ring again, +// the flag has to come back WITH it — the lesson is that the tag must name the rung that +// actually wrote the pixels, not the family it belongs to. /// Create the decode device on the presenter's adapter. `luid` is the Vulkan device's /// `VkPhysicalDeviceIDProperties::deviceLUID` (little-endian LowPart‖HighPart) — matching it /// keeps the shared textures on one GPU. `None`/no match falls back to the first hardware /// adapter (single-GPU boxes; a WARP-only box fails out to software decode). -fn create_device(luid: Option<[u8; 8]>) -> Result<(ID3D11Device, ID3D11DeviceContext)> { +pub(crate) fn create_device(luid: Option<[u8; 8]>) -> Result<(ID3D11Device, ID3D11DeviceContext)> { // SAFETY: DXGI factory creation takes no pointer and returns an owned factory or an error, // checked by `?`. let factory: IDXGIFactory1 = unsafe { CreateDXGIFactory1() }.context("CreateDXGIFactory1")?; @@ -349,10 +192,10 @@ fn create_device(luid: Option<[u8; 8]>) -> Result<(ID3D11Device, ID3D11DeviceCon .context("D3D11CreateDevice")?; let device = device.ok_or_else(|| anyhow!("D3D11CreateDevice returned no device"))?; let context = context.ok_or_else(|| anyhow!("D3D11CreateDevice returned no context"))?; - // The decode (FFmpeg video context) and our copy (immediate context) run on the decode - // thread, but FFmpeg's own workers touch the device too — same protection the legacy - // shared device enabled (FFmpeg would install it during hwdevice init anyway; explicit - // keeps the invariant obvious). + // The decode video context and our copy (immediate context) run on the decode thread, + // and D3D11's own driver threads touch the device too — the same protection the legacy + // shared device enabled, and the same one libavcodec's hwdevice init used to install + // for us. Explicit keeps the invariant obvious now that nothing else sets it. if let Ok(mt) = device.cast::() { // Returns the PREVIOUS protection state — nothing to act on. // SAFETY: a COM call on the live `ID3D11Multithread` from a checked `cast`; it takes a @@ -528,18 +371,41 @@ impl SharedRing { } } -pub(crate) struct D3d11vaDecoder { - ctx: *mut ffmpeg::ffi::AVCodecContext, - /// The D3D11VA hwdevice, owned. Nothing reads this field after construction — the codec context - /// took its own ref via `av_buffer_ref` — it exists so the device outlives the decoder and is - /// unref'd exactly once when it drops. Declared after `ctx` so it still releases AFTER the - /// `Drop` below frees packet/frame/context, which is the order the hand-written unref had. - /// `dead_code` is answered here rather than by removing the field (that would free the device - /// early) or by an underscore name (that would hide what it is). - #[allow(dead_code)] - hw_device: AvBuffer, - packet: *mut ffmpeg::ffi::AVPacket, - frame: *mut ffmpeg::ffi::AVFrame, +/// One decoded picture, as [`HandoffRing::present`] needs to see it. +/// +/// A struct rather than seven positional parameters because six of them are integers and +/// booleans: a caller that swaps `width` and `height`, or `array_slice` and a dimension, +/// compiles clean and renders a wrong picture. Named fields make each of those a build error. +pub(crate) struct HandoffSource<'a> { + /// The decode pool's texture ARRAY — the pool `video_d3d11_native` created. + pub texture: &'a ID3D11Texture2D, + /// The picture's slice within that array — the decoder's DPB slot, which IS the DXVA + /// surface index. + pub array_slice: u32, + /// The FRAME size. The surface is taller (DXVA alignment), which is exactly what the + /// stream source rect excludes — see the blit below. + pub width: u32, + pub height: u32, + /// The picture's colour signalling, per frame and never latched (the host flips PQ + /// in-band with a new SPS). + pub color: ColorDesc, + /// Intra keyframe (IDR/I) — the pump's post-loss re-anchor signal. + pub keyframe: bool, + /// Which decoder produced it, for the one-time layout log a field report leans on. + pub decoder: &'a str, +} + +/// The shipping hand-off: the video processor, its ring of shareable RGBA textures, and the +/// D3D11 objects they live on. Everything from "here is a decoded NV12/P010 surface" to "here +/// is a [`D3d11Frame`] the presenter can import". +/// +/// Extracted verbatim from `D3d11vaDecoder`, the libavcodec D3D11VA rung that owned this ring +/// until M10 deleted it, so the M5 native rung ([`crate::video_d3d11_native`]) filled the +/// identical ring rather than growing a second copy of it: this is the half with the field +/// history (the NVIDIA NV12-import TDR, the Intel green bar, the keyed-mutex protocol), and two +/// copies of it would have been two chances to lose that history. Nothing about the hand-off +/// changed in the extraction; only its owner did, and today that owner is the sole one. +pub(crate) struct HandoffRing { device: ID3D11Device, context: ID3D11DeviceContext, /// Creates the per-ring video processor + views. @@ -552,198 +418,88 @@ pub(crate) struct D3d11vaDecoder { /// ([`crate::video::VulkanDecodeDevice::d3d11_hdr10`]) — PQ streams get the HDR /// pass-through ring; without it they keep the tonemap-to-sRGB ring. hdr10_out: bool, - /// The selected decoder's registry name (`(*codec).name`) — `"av1"` vs `"libdav1d"` - /// is the difference between hardware decode and a silent CPU fallback, so every - /// log a field report leans on carries it. - name: String, } -// SAFETY: the libav pointers are this decoder's own allocations (freed once in `Drop`) and the COM -// interfaces it holds are reference-counted with interlocked counts, so moving the whole struct to -// another thread and releasing it there is sound. D3D11's immediate context is not thread-SAFE but -// it is thread-AGNOSTIC: it requires serialised use, which `&mut self` on every method gives, not -// use from one fixed thread. The presenter never touches these objects — it reaches the shared -// textures through their NT handles on its own device. Moved, never shared; deliberately NOT `Sync`. -unsafe impl Send for D3d11vaDecoder {} - -impl D3d11vaDecoder { +impl HandoffRing { + /// Take the interfaces the hand-off needs off a decode device, up front — their absence + /// must route the session to another rung NOW, not burn the opening IDR. pub(crate) fn new( - codec_id: ffmpeg::codec::Id, - luid: Option<[u8; 8]>, + device: ID3D11Device, + context: ID3D11DeviceContext, hdr10_out: bool, - ) -> Result { - use ffmpeg::ffi; - let (device, context) = create_device(luid)?; - // The adapter must expose the codec's DXVA profile — checked here, not at the first AU. - decode_profile_supported(&device, codec_id)?; - // The hand-off converter's interfaces, up front (their absence must route to software - // decode NOW, not burn the opening IDR). + ) -> Result { let video_device: ID3D11VideoDevice = device .cast() .context("device lacks ID3D11VideoDevice (created without VIDEO_SUPPORT)")?; let video_context1: ID3D11VideoContext1 = context .cast() .context("context lacks ID3D11VideoContext1 (pre-1703 Windows?)")?; - // SAFETY: a self-contained builder: every libav allocation is made here and null-checked, - // the D3D11VA hwctx fields are filled from the live device/context borrowed above, and - // what survives is moved into the decoder, which frees each exactly once in `Drop`. - unsafe { - let hw_device = - ffi::av_hwdevice_ctx_alloc(ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA); - if hw_device.is_null() { - bail!("av_hwdevice_ctx_alloc(D3D11VA) failed"); - } - let devctx = (*hw_device).data as *mut ffi::AVHWDeviceContext; - let d3dctx = (*devctx).hwctx as *mut AVD3D11VADeviceContext; - // Hand FFmpeg an owned ref to the device + immediate context (it Releases them when - // the hwdevice ctx is freed). `into_raw()` transfers a +1 ref without releasing. - (*d3dctx).device = device.clone().into_raw(); - (*d3dctx).device_context = context.clone().into_raw(); - // lock left null → FFmpeg installs the ID3D11Multithread default lock in init. - let r = ffi::av_hwdevice_ctx_init(hw_device); - if r < 0 { - let mut hw = hw_device; - ffi::av_buffer_unref(&mut hw); - bail!("av_hwdevice_ctx_init: {}", ffmpeg::Error::from(r)); - } - // Owned from here: every `bail!` below drops it, so none of them unref by hand. - let hw_device = AvBuffer::from_raw(hw_device) - .context("av_hwdevice_ctx_alloc(D3D11VA) gave no device")?; - // Up-front viability probe (see `d3d11va_decode_supported`). - if !d3d11va_decode_supported(hw_device.as_ptr()) { - bail!("GPU can't create the D3D11VA decode surface pool"); - } - // NOT `avcodec_find_decoder`: the ID lookup returns the registry's FIRST - // decoder, and for AV1 that is libdav1d (upstream orders the hwaccel-only - // native decoder last) — a software decoder that silently ignores - // `hw_device_ctx` and fails every frame's D3D11-format guard mid-stream, - // even when the DXVA profile + pool probes above all passed. Select by - // capability instead: the first decoder that can drive AV_PIX_FMT_D3D11 - // via hw_device_ctx, or fail here at open. - let codec = - crate::video::find_hw_decoder(codec_id, ffi::AVPixelFormat::AV_PIX_FMT_D3D11)?; - let name = crate::video::codec_name(codec); - let ctx = ffi::avcodec_alloc_context3(codec); - (*ctx).hw_device_ctx = ffi::av_buffer_ref(hw_device.as_ptr()); - (*ctx).get_format = Some(get_format_d3d11); - (*ctx).flags |= ffi::AV_CODEC_FLAG_LOW_DELAY as i32; - (*ctx).thread_count = 1; // hwaccel: threads only add latency - // On top of the DPB-based pool libavcodec sizes: margin for the frames briefly held - // between decode and the ring copy (the copy runs immediately, so this is small). - (*ctx).extra_hw_frames = 4; - let r = ffi::avcodec_open2(ctx, codec, ptr::null_mut()); - if r < 0 { - let mut ctx = ctx; - ffi::avcodec_free_context(&mut ctx); - bail!("avcodec_open2 (D3D11VA): {}", ffmpeg::Error::from(r)); - } - Ok(D3d11vaDecoder { - ctx, - hw_device, - packet: ffi::av_packet_alloc(), - frame: ffi::av_frame_alloc(), - device, - context, - video_device, - video_context1, - ring: None, - hdr10_out, - name, - }) - } + Ok(HandoffRing { + device, + context, + video_device, + video_context1, + ring: None, + hdr10_out, + }) } - /// The selected decoder's registry name (e.g. `"av1"`) — see the field doc. - pub(crate) fn name(&self) -> &str { - &self.name + /// The video device, for a caller that also needs it (the native rung enumerates decode + /// profiles and creates its decoder through the same interface). + pub(crate) fn video_device(&self) -> &ID3D11VideoDevice { + &self.video_device } - pub(crate) fn decode(&mut self, au: &[u8]) -> Result> { - use ffmpeg::ffi; - // SAFETY: `packet`/`frame`/`ctx` are this decoder's own allocations, live for its whole - // lifetime; `au` outlives the synchronous copy out of it, and every libav return is - // checked before use. - unsafe { - let r = ffi::av_new_packet(self.packet, au.len() as i32); - if r < 0 { - return Err(averr("av_new_packet", r)); - } - ptr::copy_nonoverlapping(au.as_ptr(), (*self.packet).data, au.len()); - let r = ffi::avcodec_send_packet(self.ctx, self.packet); - ffi::av_packet_unref(self.packet); - if r < 0 { - return Err(averr("send_packet", r)); - } - let mut out = None; - loop { - let r = ffi::avcodec_receive_frame(self.ctx, self.frame); - if r == ffmpeg::ffi::AVERROR(ffmpeg::ffi::EAGAIN) { - break; - } - if r < 0 { - return Err(averr("receive_frame", r)); - } - let lifted = self.lift(); - // The decode surface goes back to the pool NOW — the ring copy (queued ahead - // of any later decoder write on the same immediate context) already owns the - // pixels. No cross-thread AVFrame guard exists in this backend at all. - ffi::av_frame_unref(self.frame); - out = Some(lifted?); // newest wins (one-in/one-out streams make this moot) - } - Ok(out) - } - } - - /// Convert the decoded slice into the next ring slot (`VideoProcessorBlt`, NV12/P010 → - /// BGRA8) under its keyed mutex and describe the hand-off. The mutex acquire also + /// Convert one decoded surface into the next ring slot (`VideoProcessorBlt`, NV12/P010 → + /// BGRA8/RGB10A2) under its keyed mutex and describe the hand-off. The mutex acquire also /// back-pressures against the presenter still reading this slot (only possible if the /// stream runs `RING_SLOTS` ahead of present). - fn lift(&mut self) -> Result { - use ffmpeg::ffi; - // SAFETY: `self.frame` is this decoder's own `AVFrame`; the format check below is what - // proves it carries a D3D11 texture before anything reads the surface out of it. + /// + pub(crate) fn present(&mut self, source: HandoffSource<'_>) -> Result { + let HandoffSource { + texture: src, + array_slice, + width, + height, + color, + keyframe, + decoder, + } = source; + // AddRef'd locals so the mutable `ring` borrow below doesn't lock all of `self`. + let video_device = self.video_device.clone(); + let video_context1 = self.video_context1.clone(); + let context = self.context.clone(); + // (Re)build the ring + video processor on first use, a stream size change, or a + // flavor change (the host flips PQ in-band; SDR↔HDR swaps the slot format, so + // it rebuilds like a resize — bit DEPTH alone still never rebuilds: an SDR + // 10-bit stream and an 8-bit one share the same output flavor). + let pq_out = self.hdr10_out && color.is_pq(); + let rebuild = self + .ring + .as_ref() + .is_none_or(|r| r.width != width || r.height != height || r.pq_out != pq_out); + if rebuild { + let generation = self.ring.as_ref().map_or(0, |r| r.generation + 1); + self.ring = Some(SharedRing::build( + &self.device, + &video_device, + width, + height, + generation, + pq_out, + )?); + } + let ring = self.ring.as_mut().expect("ring built above"); + let slot_idx = ring.next; + ring.next = (ring.next + 1) % ring.slots.len(); + let slot = &ring.slots[slot_idx]; + + // SAFETY: every call below is a COM call on a live interface — the video device and + // context AddRef'd above, the ring's processor/enumerator/views built by + // `SharedRing::build`, and the caller's `src` texture, whose liveness for the call is + // this method's contract. Out-params are local `Option`s checked before use; the + // `ManuallyDrop` refs the stream struct carries are balanced explicitly below. unsafe { - if (*self.frame).format != ffi::AVPixelFormat::AV_PIX_FMT_D3D11 as i32 { - bail!("decoder returned a software frame (no D3D11 surface)"); - } - let width = (*self.frame).width as u32; - let height = (*self.frame).height as u32; - let color = ColorDesc::from_raw(self.frame); - // AddRef'd locals so the mutable `ring` borrow below doesn't lock all of `self`. - let video_device = self.video_device.clone(); - let video_context1 = self.video_context1.clone(); - let context = self.context.clone(); - // (Re)build the ring + video processor on first use, a stream size change, or a - // flavor change (the host flips PQ in-band; SDR↔HDR swaps the slot format, so - // it rebuilds like a resize — bit DEPTH alone still never rebuilds: an SDR - // 10-bit stream and an 8-bit one share the same output flavor). - let pq_out = self.hdr10_out && color.is_pq(); - let rebuild = self - .ring - .as_ref() - .is_none_or(|r| r.width != width || r.height != height || r.pq_out != pq_out); - if rebuild { - let generation = self.ring.as_ref().map_or(0, |r| r.generation + 1); - self.ring = Some(SharedRing::build( - &self.device, - &video_device, - width, - height, - generation, - pq_out, - )?); - } - let ring = self.ring.as_mut().expect("ring built above"); - let slot_idx = ring.next; - ring.next = (ring.next + 1) % ring.slots.len(); - let slot = &ring.slots[slot_idx]; - - let raw = (*self.frame).data[0] as *mut c_void; - let src: ID3D11Texture2D = ID3D11Texture2D::from_raw_borrowed(&raw) - .ok_or_else(|| anyhow!("null D3D11 texture on decoded frame"))? - .clone(); - let index = (*self.frame).data[1] as usize as u32; - // Input view over THIS slice of the decode array (cheap per-frame object). let mut iv_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC { FourCC: 0, // surface format speaks for itself @@ -751,10 +507,10 @@ impl D3d11vaDecoder { // Anonymous.Texture2D zeroed (MipSlice 0); ArraySlice is per-frame below. ..Default::default() }; - iv_desc.Anonymous.Texture2D.ArraySlice = index; + iv_desc.Anonymous.Texture2D.ArraySlice = array_slice; let mut in_view = None; video_device - .CreateVideoProcessorInputView(&src, &ring.enumerator, &iv_desc, Some(&mut in_view)) + .CreateVideoProcessorInputView(src, &ring.enumerator, &iv_desc, Some(&mut in_view)) .ok() .context("CreateVideoProcessorInputView")?; let in_view = in_view.expect("input view created"); @@ -844,9 +600,9 @@ impl D3d11vaDecoder { height, src_desc.Width, src_desc.Height, - index, + array_slice, color.is_pq(), - &self.name, + decoder, ); Ok(D3d11Frame { width, @@ -870,8 +626,7 @@ impl D3d11vaDecoder { } }, rgb10: ring.pq_out, - // SAFETY: `self.frame` is the live decoded AVFrame for this call. - keyframe: crate::video::frame_is_keyframe(self.frame), + keyframe, handle, generation, }) @@ -879,27 +634,17 @@ impl D3d11vaDecoder { } } -impl Drop for D3d11vaDecoder { - fn drop(&mut self) { - use ffmpeg::ffi; - // SAFETY: each pointer is this decoder's own allocation and nothing else holds it; `Drop` - // runs exactly once and each free nulls its pointer through the `&mut`, so none can be - // released twice. - unsafe { - ffi::av_packet_free(&mut self.packet); - ffi::av_frame_free(&mut self.frame); - ffi::avcodec_free_context(&mut self.ctx); - // `hw_device` is an `AvBuffer` and unrefs itself when the field drops, right after this. - } - // `ring` drops after the codec: no decode can be in flight past avcodec_free_context, - // and the slots' CloseHandle only closes OUR handle — a presenter-side import that is - // still parked keeps its own reference to the payload. - } -} - /// One-time dump of the first decoded surface's layout — the forensics for a new GPU/driver. /// `tex_*` is the DXVA-aligned decode surface (>= the frame); the gap is the padding the /// stream source rect excludes. +/// +/// Keyed by DECODER rather than latched once per process. Two rungs shared this hand-off +/// until M10 (libavcodec's D3D11VA and the native one), and a single process-wide latch +/// meant a session that pinned the native rung and then demoted logged the native layout +/// and nothing else, leaving the rung that actually painted the session's frames +/// undocumented in exactly the report that needs it. One rung fills the ring today, so the +/// set holds one short entry — kept keyed because the property is about which decoder +/// wrote the surface, and that is the question a new-GPU forensics report asks. fn log_layout_once( width: u32, height: u32, @@ -909,9 +654,17 @@ fn log_layout_once( pq: bool, decoder: &str, ) { - use std::sync::atomic::{AtomicBool, Ordering}; - static ONCE: AtomicBool = AtomicBool::new(true); - if ONCE.swap(false, Ordering::Relaxed) { + use std::collections::HashSet; + use std::sync::{Mutex, OnceLock}; + static SEEN: OnceLock>> = OnceLock::new(); + let seen = SEEN.get_or_init(|| Mutex::new(HashSet::new())); + // A poisoned lock costs a log line, never a frame: a panic while holding it can only have + // happened inside the set, and the worst outcome of ignoring it is a repeated line. + let first = match seen.lock() { + Ok(mut seen) => seen.insert(decoder.to_owned()), + Err(_) => false, + }; + if first { tracing::info!( width, height, diff --git a/crates/pf-client-core/src/video_d3d11_native.rs b/crates/pf-client-core/src/video_d3d11_native.rs new file mode 100644 index 00000000..04ad598e --- /dev/null +++ b/crates/pf-client-core/src/video_d3d11_native.rs @@ -0,0 +1,2437 @@ +//! Native D3D11VA decode — M5 of the native-decode program: `ID3D11VideoDecoder` driven +//! straight from pf-bitstream's per-AU plans, with no libavcodec anywhere in the path. +//! +//! It is the DXVA counterpart of `video_vk_native` and it replaces exactly one half of +//! [`crate::video_d3d11`]: what WRITES the decode surface. The other half — the fixed-function +//! `ID3D11VideoProcessor` blitting NV12/P010 into a ring of shareable RGBA textures the +//! presenter imports by NT handle ([`HandoffRing`]) — is shared code, byte for byte, because +//! it is the field-proven half (the NVIDIA NV12-import TDR that forced RGB, the Intel green +//! bar that forced the stream source rect, the key-0 keyed-mutex protocol). This rung +//! therefore is NOT zero-copy, and deliberately so: that constraint governs the Vulkan path, +//! where the decoded image IS the presented image. +//! +//! # Admission +//! +//! `PUNKTFUNK_DECODER=native-d3d11va` reaches every leg of this rung, and since M10 deleted +//! libavcodec's D3D11VA hwaccel `auto` does too — this is the only DXVA rung there is. The +//! evidence behind the two legs is NOT the same, and the session log distinguishes them +//! (`video::native_evidence`, and the table in `video`'s module docs): +//! +//! * **H.264 and H.265** — frame-hash parity against libavcodec on an RTX 4090 and an AMD +//! iGPU plus a 30-minute soak (M5). +//! * **AV1** — wired in M7, has decoded nothing on any hardware. Until M10 `auto` skipped it +//! in favour of the libavcodec rung below; with that gone the alternative is the CPU, so it +//! runs and the session log says so at `warn`. +//! +//! A refusal or an init failure logs and falls through to the standard ladder, so neither the +//! pin nor the `auto` admission can cost a session its decoder. +//! +//! # The decode pool — the part that has already failed once +//! +//! [`crate::video_d3d11`]'s module docs record it plainly: a **hand-built decode pool +//! validated on NVIDIA was rejected by Intel at the first `SubmitDecoderBuffers`**, which is +//! why the libavcodec rung left the pool to libavcodec. A native decoder has no such luxury — +//! it must own its pool — so this is the highest-risk code in the milestone, and the answer +//! is not to invent a pool but to reproduce libavcodec's exactly. What that path does, from +//! `ff_dxva2_common_frame_params` and `d3d11va_frames_init`: +//! +//! * **ONE `ID3D11Texture2D` with `ArraySize = pool size`**, not N individual textures. The +//! array slice is the DXVA surface index, which is what makes `DXVA_PicEntry::Index7Bits` +//! and the DPB slot the same number. +//! * **`BindFlags = D3D11_BIND_DECODER`, and nothing else.** Not `SHADER_RESOURCE`, not +//! `RENDER_TARGET`: a decode pool that also claims a sampling bind flag is precisely the +//! sort of request a driver may honour on one vendor and reject on another. The hand-off's +//! `CreateVideoProcessorInputView` needs no bind flag at all. +//! * **`MiscFlags = 0`** — no sharing. The shareable textures are the RGBA ring's, on the +//! other side of the video processor. +//! * **Dimensions aligned to the codec's granule** (16 for H.264, 128 for HEVC and AV1 — +//! [`pf_dxvadec::align_surface`]), so the surface is TALLER than the frame. That padding is +//! the green bar the hand-off's stream source rect already excludes. The alignment applies +//! to the TEXTURE only: `D3D11_VIDEO_DECODER_DESC` gets the CODED size, exactly as +//! `d3d11va_create_decoder` passes `avctx->coded_width/coded_height` while +//! `ff_dxva2_common_frame_params` allocates at `FFALIGN(coded, surface_alignment)`. +//! * **`Usage = D3D11_USAGE_DEFAULT`, `MipLevels = 1`, `SampleDesc.Count = 1`**, format NV12 +//! or P010 per profile. +//! +//! Everything else about pool sizing is [`pf_dxvadec::pool_size`], which is unit-tested; the +//! driver's own `ConfigMinRenderTargetBuffCount` is honoured there. +//! +//! # What is decided here vs decided in pf-dxvadec +//! +//! Nothing in this file can be tested by any gate this program runs — it is `cfg(windows)`, +//! so neither the macOS host nor the Linux container compiles it, and the Windows box only +//! `cargo check`s. Every decision that could be a pure function therefore lives in +//! [`pf_dxvadec`] with unit tests: the DXVA buffer layouts, the profile table, the +//! decoder-config choice, the surface alignment, the pool size, the bitstream packing rules, +//! the buffer DESCRIPTORS, and the whole plan → picparams/qmatrix/slice-control (AV1: +//! tile-control) conversion. What is left here is enumeration, allocation and submission — +//! the parts that genuinely need a device. +//! +//! # Three codecs, one submission path +//! +//! H.264, HEVC and — since M7 — AV1 Profile 0. AV1 is not a fourth flavour of the same +//! submission: its buffer SET is different (no quantization matrix at all; `DXVA_Tile_AV1` +//! records where the other two put slice control), its bitstream buffer holds tile data +//! rather than start-code-prefixed NALUs, and its access unit is a TEMPORAL UNIT that may +//! decode several frames of which at most one displays. What it shares — and what it must +//! not fork — is the session, the pool, the slot map, `DecoderBeginFrame`/`EndFrame` and +//! the hand-off ring, because those are the parts hardware has already found the traps in. + +use anyhow::{anyhow, bail, Context as _, Result}; +use pf_dxvadec::{Codec, DxvaProfile}; +use windows::core::{Interface, GUID}; +use windows::Win32::d3d11::{ + ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D, ID3D11VideoContext, ID3D11VideoDecoder, + ID3D11VideoDecoderOutputView, ID3D11VideoDevice, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, + D3D11_VDOV_DIMENSION_TEXTURE2D, D3D11_VIDEO_DECODER_BUFFER_BITSTREAM, + D3D11_VIDEO_DECODER_BUFFER_DESC, D3D11_VIDEO_DECODER_BUFFER_INVERSE_QUANTIZATION_MATRIX, + D3D11_VIDEO_DECODER_BUFFER_PICTURE_PARAMETERS, D3D11_VIDEO_DECODER_BUFFER_SLICE_CONTROL, + D3D11_VIDEO_DECODER_BUFFER_TYPE, D3D11_VIDEO_DECODER_CONFIG, D3D11_VIDEO_DECODER_DESC, + D3D11_VIDEO_DECODER_OUTPUT_VIEW_DESC, +}; +use windows::Win32::dxgi::{DXGI_FORMAT, DXGI_SAMPLE_DESC}; + +use crate::video::{ColorDesc, DecodeHealth, StreamFormat}; +use crate::video_d3d11::{create_device, D3d11Frame, HandoffRing, HandoffSource}; + +/// `D3D11_BIND_DECODER` — the decode pool's ONLY bind flag (module docs). +const BIND_DECODER: u32 = 0x200; + +/// `DecoderBeginFrame` answers `E_PENDING` while the hardware is still busy with an earlier +/// picture. libavcodec's `ff_dxva2_common_end_frame` retries up to 50 times, sleeping +/// `av_usleep(2000)` between attempts — a hundred milliseconds in total, and these two +/// constants are that budget rather than a smaller one of our own. +/// +/// A shorter budget looks safer and is not: `E_PENDING` means the hardware is BUSY, not +/// wedged, and a 4K decoder that needs longer than the budget gets an `Err` — which ticks +/// the ladder's demotion streak for the offence of being busy. The retry loop only ever runs +/// while the decoder is working, so the wait is bounded by the work in flight; a genuinely +/// wedged decoder still surfaces, a tenth of a second later. +const BEGIN_FRAME_RETRIES: u32 = 50; +const BEGIN_FRAME_BACKOFF: std::time::Duration = std::time::Duration::from_millis(2); +/// `E_PENDING`. +const E_PENDING: i32 = 0x8000_000A_u32 as i32; + +/// The environment value that pins this rung. +pub(crate) const DECODER_PIN: &str = "native-d3d11va"; + +/// One codec's planning state. The negotiated codec picks it once, at construction — the +/// same shape `video_vk_native`'s `Codec` has, and for the same reason: everything below +/// the plan is codec-agnostic, so forking the session/pool/submission machinery per codec +/// would fork the part that is hardest to get right. +enum Planner { + H264(Box), + H265(Box), + Av1(Box), +} + +/// What a decoded picture is, for the hand-off — separated from [`Submission`] +/// because AV1 can need it for a picture whose submission happened several access +/// units ago. +/// +/// A `show_existing_frame` carries a frame header with no dimensions, no colour +/// and no frame type of its own (AV1 5.9.2: the shown frame's state is LOADED), +/// so the only honest source for those is what the picture was decoded with. +/// [`Session::held`] remembers exactly this, per surface. +#[derive(Debug, Clone, Copy)] +struct PictureFacts { + /// The picture's colour signalling and keyframe-ness. + colour: ColorDesc, + keyframe: bool, + /// Display size — the conformance-window crop on H.264/H.265, the render size + /// on AV1 — which is what the hand-off blits. + width: u32, + height: u32, +} + +/// The two AV1 buffers that have no H.264/H.265 counterpart. +struct Av1Buffers { + /// Where this frame's tiles and tile-group regions are in the access unit. + bitstream: pf_dxvadec::Av1Bitstream, + /// One `DXVA_Tile_AV1` per TILE, rows and columns final, offsets rebased by + /// the packer into the driver's own mapping. + tiles: Vec, +} + +/// What one planned AU produced, reduced to the codec-agnostic facts submission needs. +struct Submission { + /// The DXVA picture-parameters buffer, as bytes. + pic_params: Vec, + /// The DXVA inverse-quantization-matrix buffer, as bytes — `None` when the buffer must + /// NOT be submitted (HEVC with `scaling_list_enabled_flag` clear, which is every + /// punktfunk HEVC stream; see `pf_dxvadec::DecodePlanDxvaH265::qmatrix`). + qmatrix: Option>, + /// `NumMBsInBuffer` for the bitstream and slice-control descriptors: the coded picture + /// in macroblocks on the H.264 path, 0 on the HEVC one. Both are libavcodec's values + /// (`commit_bitstream_and_slice_buffer` in `dxva2_h264.c` and `dxva2_hevc.c`). + mb_count: u32, + /// Slice NALU ranges within the AU, for the bitstream packer. + slice_ranges: Vec>, + /// The surface (array slice) the picture decodes into. + setup_slot: u8, + /// The picture id the slot map was told that surface holds. + /// + /// Carried so a caller can give the ledger entry BACK — which AV1 needs and the + /// other two codecs do not (see [`NativeD3d11Decoder::frame_av1`]). All three + /// conversions produce it; dropping it here made the AV1 leak invisible. + setup_id: u64, + /// Which codec's slice-control record the packer's locations become. + codec: Codec, + /// What the hand-off needs to blit this picture. + facts: PictureFacts, + /// The plan carried an integrity warning: a reference the DPB no longer held, a + /// `frame_num` gap, a NALU walk that stopped early. The picture would be decoded from a + /// substitute, so it is never submitted — see [`NativeD3d11Decoder::decode`]. + concealed: bool, + /// AV1 only: the tile-control and bitstream inputs, which are a different + /// buffer SET rather than a different flavour of the same one — no + /// quantization matrix, no slice-control records, and `slice_ranges` and + /// `mb_count` above unused. `None` on H.264 and H.265, and that is what + /// [`NativeD3d11Decoder::fill_and_submit`] dispatches on. + av1: Option, + /// AV1 only: does this frame DISPLAY? An AV1 temporal unit may decode several + /// frames of which at most one is shown; the hidden ones are references for + /// what follows and are never blitted. Always `true` on H.264/H.265, where an + /// access unit is a picture and every picture displays. + show: bool, +} + +/// Everything about the stream that a decode session is BUILT FROM — the session's identity, +/// read off the SPS the planner just activated rather than off the negotiated format. +/// +/// Every field here decides an object that cannot be changed after creation: the coded size +/// and the DPB depth size the decoder, the pool and the slot map; the chroma format and the +/// luma bit depth pick the profile GUID and with it the surfaces' `DXGI_FORMAT`. A change in +/// any of them is a renegotiation, and the session is rebuilt WHOLE — a half-rebuilt session +/// hands out surface indices its pool does not have, or decodes 10-bit samples into 8-bit +/// surfaces. +/// +/// That last one is not hypothetical: `colour_of`'s docs record that the Windows host flips +/// an HDR desktop to PQ/BT.2020 in-band with a new SPS mid-stream. An SPS that also moved the +/// luma depth 8 → 10 at an unchanged coded size would, if this struct held only the size and +/// the depth, leave an `HEVC_VLD_MAIN` decoder writing into an NV12 pool while the picture +/// parameters told the driver the samples are ten bits wide. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct StreamShape { + coded_width: u32, + coded_height: u32, + max_dpb_frames: usize, + chroma_format_idc: u8, + bit_depth_luma_minus8: u8, + bit_depth_chroma_minus8: u8, +} + +impl StreamShape { + fn bit_depth(&self) -> u8 { + 8 + self.bit_depth_luma_minus8 + } + + /// The session shape one AV1 plan implies. + /// + /// ⚠ The coded size is the SEQUENCE header's **maximum** frame size, not this + /// frame's. AV1 lets every frame pick its own size up to that maximum, and + /// `DXVA_PicParams_AV1` carries both (`max_width`/`max_height` beside + /// `width`/`height`) precisely so the decoder object and its pool can be built + /// once for the largest of them. libavcodec does the same thing — + /// `set_context_with_sequence` calls `ff_set_dimensions(avctx, + /// seq->max_frame_width_minus_1 + 1, …)`, and it is `avctx->coded_width` that + /// reaches `D3D11_VIDEO_DECODER_DESC`. Sizing the session from the frame + /// instead would rebuild the decoder, the pool and the slot map — dropping + /// every reference — the first time a stream resized a frame downward, which + /// AV1 permits without a key frame. + /// + /// The DPB depth is a constant of the codec: eight reference slots + /// (`NUM_REF_FRAMES`), and [`pf_dxvadec::SlotMap`] adds the current picture, so + /// the pool is nine surfaces — libavcodec's `num_surfaces = 1 + 8` for AV1. + fn of_av1(plan: &pf_dxvadec::AuPlanAv1) -> StreamShape { + let depth = plan.picture.bit_depth.saturating_sub(8); + StreamShape { + coded_width: u32::from(plan.sequence.max_frame_width_minus_1) + 1, + coded_height: u32::from(plan.sequence.max_frame_height_minus_1) + 1, + max_dpb_frames: pf_dxvadec::NUM_REF_SLOTS, + chroma_format_idc: plan.picture.chroma_format_idc, + // AV1 codes ONE bit depth for all three planes (`high_bitdepth` / + // `twelve_bit` in the colour config), so the luma and chroma fields + // here are the same number by construction and `Session::build`'s + // "no DXGI format carries both" refusal can never fire for AV1. + bit_depth_luma_minus8: depth, + bit_depth_chroma_minus8: depth, + } + } +} + +/// The live decoder plus everything sized to the stream it was built for. Rebuilt whole on a +/// renegotiation (any [`StreamShape`] change), because every one of these is derived from the +/// SPS and a half-rebuilt decoder is the shape of a corrupt reference. +struct Session { + decoder: ID3D11VideoDecoder, + /// The decode pool: ONE texture array (module docs), kept alive for the session and + /// handed to the video processor as the blit source. + pool: ID3D11Texture2D, + /// One output view per array slice — `DecoderBeginFrame`'s target. + views: Vec, + slots: pf_dxvadec::SlotMap, + /// What each surface of the pool currently holds — written on every AV1 + /// decode, read only by `show_existing_frame` ([`PictureFacts`]). Empty of + /// meaning on H.264/H.265, which never re-present an old surface. + /// + /// Indexed by surface, and stale entries are unreachable rather than cleaned: + /// a surface is only ever named through the slot map, so an entry can be read + /// only while the map still says that slot holds the picture that wrote it. + held: Vec>, + /// The SPS facts this session was built from; anything else is a rebuild. + shape: StreamShape, + /// The profile [`StreamShape::chroma_format_idc`] and the luma depth chose — which is + /// not necessarily the one the NEGOTIATED format chose at construction. + profile: DxvaProfile, +} + +pub(crate) struct NativeD3d11Decoder { + /// Kept for pool creation on a renegotiation. + device: ID3D11Device, + /// Kept so the session's teardown/rebuild happens on a live context; the hand-off holds + /// its own clone for the blit. + #[allow(dead_code)] + context: ID3D11DeviceContext, + video_device: ID3D11VideoDevice, + video_context: ID3D11VideoContext, + /// The decoder, its surface pool and its slot map, sized to the stream. Declared BEFORE + /// `handoff` so it drops first: Rust drops fields in declaration order, and the ring must + /// outlive the decode surfaces whose contents it converted — the same ordering the FFmpeg + /// rung gets by freeing its codec context in `Drop` before its `handoff` field falls. + session: Option, + /// The shared `VideoProcessorBlt` → shareable-RGBA hand-off. + handoff: HandoffRing, + planner: Planner, + codec: Codec, + /// `StatusReportFeedbackNumber`, monotonic from 1 — 0 is what a driver reads out of a + /// buffer nobody wrote, so it is never a legitimate tag. + status_id: u32, + health: DecodeHealth, + want_recovery: bool, +} + +// SAFETY: every field is either owned plain data or a reference-counted COM interface with +// interlocked counts, so moving the whole struct to another thread and releasing it there is +// sound. D3D11's immediate context is not thread-SAFE but it is thread-AGNOSTIC: it requires +// serialised use, which `&mut self` on every method gives, not use from one fixed thread. The +// presenter never touches these objects — it reaches the shared textures through their NT +// handles on its own device. Moved, never shared; deliberately NOT `Sync`. (Identical +// argument to `D3d11vaDecoder`'s, and for the identical reason.) +unsafe impl Send for NativeD3d11Decoder {} + +impl NativeD3d11Decoder { + /// Build the decoder on the presenter's adapter. + /// + /// Everything that can fail as a REFUSAL fails here, before a single AU: the codec, the + /// negotiated picture shape, the adapter's profile list, and the decoder config. That is + /// the ladder's cheap exit — a construction failure falls through to the next rung with a + /// clean stream, where a first-AU failure would burn the opening IDR and only exit + /// through an error-streak demotion. + /// + /// The DECODER itself is not created here: its `D3D11_VIDEO_DECODER_DESC` needs the coded + /// picture size, which only the in-band SPS knows. The negotiated [`StreamFormat`] is + /// enough to pick a profile, and that profile is enough to prove the adapter can decode + /// this session at all — but it is NOT the profile the session is built with. That one is + /// derived per session from the SPS ([`StreamShape`]), because the negotiated format and + /// the in-band one can disagree, and when they do the SPS is the one that decodes. + pub(crate) fn new( + codec: Codec, + stream: StreamFormat, + luid: Option<[u8; 8]>, + hdr10_out: bool, + ) -> Result { + let profile = pf_dxvadec::profile_for(codec, stream.chroma_format_idc, stream.bit_depth) + .ok_or_else(|| { + anyhow!( + "no DXVA profile for {codec:?} chroma_format_idc {} at {} bits", + stream.chroma_format_idc, + stream.bit_depth + ) + })?; + let (device, context) = create_device(luid)?; + let handoff = HandoffRing::new(device.clone(), context.clone(), hdr10_out)?; + let video_device = handoff.video_device().clone(); + let video_context: ID3D11VideoContext = context + .cast() + .context("context lacks ID3D11VideoContext (created without VIDEO_SUPPORT)")?; + profile_supported(&video_device, profile)?; + let planner = match codec { + Codec::H264 => Planner::H264(Box::new(pf_dxvadec::H264Planner::new())), + Codec::H265 => Planner::H265(Box::new(pf_dxvadec::H265Planner::new())), + Codec::Av1 => Planner::Av1(Box::new(pf_dxvadec::Av1Planner::new())), + }; + tracing::info!( + ?codec, + negotiated_profile = profile.name, + chroma = stream.chroma_format_idc, + bits = stream.bit_depth, + "native D3D11VA decoder built (pf-dxvadec, pinned)" + ); + Ok(NativeD3d11Decoder { + device, + context, + video_device, + video_context, + session: None, + handoff, + planner, + codec, + status_id: 0, + // No per-operation status query exists in D3D11VA the way Vulkan Video's + // `RESULT_STATUS_ONLY` does — `ID3D11VideoContext` exposes no per-picture status + // read at all — so `failed` can only ever be 0 here and the flag says so + // honestly. A report that cannot tell "clean" from "unmeasured" is the founding + // failure of this program; claiming query support we do not have would recreate + // it exactly. + health: DecodeHealth { + status_queries: false, + ..DecodeHealth::default() + }, + want_recovery: false, + }) + } + + /// The rung's name, for the logs a field report leans on. + pub(crate) fn name(&self) -> &'static str { + DECODER_PIN + } + + /// This session's decode integrity — see [`DecodeHealth`]. + pub(crate) fn health(&self) -> DecodeHealth { + self.health + } + + /// Drain the "this stream needs a keyframe" request raised by concealment. + pub(crate) fn take_recovery_request(&mut self) -> bool { + std::mem::take(&mut self.want_recovery) + } + + /// Plan, convert and submit one access unit. + /// + /// The three answers, and why they differ: + /// * `Ok(Some(frame))` — a picture, converted into the hand-off ring. + /// * `Ok(None)` — nothing to show, and NOT an error: an AU whose plan needed concealment + /// (its picture is not fit to present, so it is dropped and recovery is requested), or + /// an HEVC RASL picture skipped after an open-GOP join (the spec's own answer, 8.1.3 + /// NOTE). Making either an `Err` would tick the demotion streak on exactly the lossy + /// links and open-GOP joins this rung exists to handle. + /// * `Err` — the decoder could not run. Streak-eligible, counted as `refused`. + pub(crate) fn decode(&mut self, au: &[u8]) -> Result> { + if matches!(self.planner, Planner::Av1(_)) { + return self.decode_av1(au); + } + let submission = match self.plan(au) { + Ok(Some(submission)) => submission, + // A skipped RASL picture: no plan, no error, nothing to show. It costs no + // health entry either — the decoder was never fed. + Ok(None) => return Ok(None), + Err(e) => { + self.health.note(false, true, 0); + tracing::warn!(error = %format!("{e:#}"), "native D3D11VA refused the access unit"); + return Err(e); + } + }; + if submission.concealed { + // The plan needed a substitute for something lost. Fold it, ask for recovery, + // and do NOT submit: a concealed picture is not fit to present, and submitting + // it would put a wrong reference in the DPB for every AU after it. + self.health.note(true, false, 0); + self.want_recovery = true; + return Ok(None); + } + let frame = match self.submit(au, &submission) { + Ok(frame) => frame, + Err(e) => { + self.health.note(false, true, 0); + tracing::warn!(error = %format!("{e:#}"), "native D3D11VA submission failed"); + return Err(e); + } + }; + self.health.note(false, false, 0); + Ok(Some(frame)) + } + + /// One AV1 **temporal unit**: decode every frame in it, present at most one. + /// + /// This is the whole of what AV1 adds to this rung's contract, and it is the + /// SPEC's shape rather than an assumption about punktfunk hosts. A temporal + /// unit may carry several frame headers; the vendored 250-packet conformance + /// vector decodes **274 frames** and shows 250, so 24 of its units carry a + /// hidden picture (an alt-ref that later frames predict from) ahead of the one + /// that displays. Those hidden frames must be DECODED — they are references — + /// and must never reach the presenter, which would show each of them for a + /// frame and stutter every time. + /// + /// AV1 admits at most one shown frame per temporal unit, so "the last shown + /// frame wins" cannot silently drop a picture; a stream that broke that rule + /// would present its last one and is not conformant. + /// + /// # Concealment is per UNIT here, per picture on the other two codecs + /// + /// A damaged frame is still CONVERTED — that is what assigns its DPB slot, and + /// skipping it would desynchronise this rung's slot map from the planner's + /// store and turn every later reference to it into a hard `Err`, i.e. a + /// demotion streak earned by one lost packet. It is simply not submitted, and + /// then nothing from the unit is presented: a shown frame that predicts from a + /// concealed reference in the same unit is not fit to display either, and the + /// unit is the smallest thing this rung can honestly drop. + fn decode_av1(&mut self, au: &[u8]) -> Result> { + let plans = match &mut self.planner { + Planner::Av1(planner) => match planner.plan_au(au) { + Ok(plans) => plans, + Err(e) => { + self.health.note(false, true, 0); + tracing::warn!( + error = %format!("{e:?}"), + "native D3D11VA refused the AV1 temporal unit" + ); + return Err(anyhow!("plan: {e:?}")); + } + }, + _ => bail!("decode_av1 on a non-AV1 planner"), + }; + + let mut shown = None; + let mut concealed = false; + for plan in &plans { + let damaged = plan + .warnings + .iter() + .any(pf_dxvadec::is_integrity_warning_av1); + concealed |= damaged; + match self.frame_av1(au, plan, damaged) { + Ok(Some(frame)) => shown = Some(frame), + Ok(None) => {} + Err(e) => { + self.health.note(false, true, 0); + tracing::warn!(error = %format!("{e:#}"), "native D3D11VA AV1 frame failed"); + return Err(e); + } + } + } + if concealed { + // A frame may already have been blitted before a LATER frame of the + // same unit turned out to be damaged, and dropping it here is safe + // rather than merely tolerable: `D3d11Frame` is plain POD (no handle + // ownership, no `Drop`), and the ring's keyed mutex is taken and + // released with key 0 by the producer around the blit itself, so a + // slot nobody consumed is simply reused when the ring comes round. + // The alternative — deferring every blit to the end of the unit — + // would be worse: a frame's surface is only safe to read before + // anything else in the unit can be assigned its slot. + self.health.note(true, false, 0); + self.want_recovery = true; + return Ok(None); + } + self.health.note(false, false, 0); + Ok(shown) + } + + /// One frame of a temporal unit: converted, submitted unless `damaged`, and + /// blitted only if it is the frame the unit displays. + /// + /// # The frame that refreshes nothing + /// + /// A frame with `refresh_frame_flags == 0` is legal AV1 — shown once, referenced + /// never — and it enters the planner's store NOWHERE, so the planner can never + /// report it removed. The conversion nevertheless assigned it a ledger slot (it + /// has to: that slot is the surface it decodes into). Left alone, that slot is + /// held for the session's whole life and NINE such frames exhaust the ledger + /// with `SlotError::Full` — a session that dies of correct streams. The Vulkan + /// rung closes it in `pf_vkdecode::decoder_av1`; this is the same close, and it + /// runs on the concealed path too, because a converted-but-unsubmitted frame + /// took a slot just the same. + fn frame_av1( + &mut self, + au: &[u8], + plan: &pf_dxvadec::AuPlanAv1, + damaged: bool, + ) -> Result> { + // `show_existing_frame` decodes nothing at all: it re-displays a picture + // some earlier hidden frame put in a reference slot. + if plan.dpb.stored.is_none() { + return self.show_existing_av1(plan); + } + let sub = self.plan_frame_av1(au, plan)?; + let shown = if damaged { + // Converted (so the slot map stayed in step with the planner's store), + // deliberately not submitted (fn docs). + // + // ⚠ And the surface's `held` entry is CLEARED rather than left. The slot + // map now says this slot holds THIS picture, while the surface still + // carries whatever the previous occupant decoded; a later + // `show_existing_frame` naming it would find the old picture's facts and + // blit the old picture's pixels. `None` makes that path return + // `Ok(None)` — nothing shown — which is what the unit's concealment + // already asked for. + if let Some(session) = self.session.as_mut() { + if let Some(held) = session.held.get_mut(usize::from(sub.setup_slot)) { + *held = None; + } + } + None + } else { + self.decode_into(au, &sub)?; + if let Some(session) = self.session.as_mut() { + // What this surface now holds, for a later `show_existing_frame`. + if let Some(held) = session.held.get_mut(usize::from(sub.setup_slot)) { + *held = Some(sub.facts); + } + } + if sub.show { + Some(self.present(sub.setup_slot, sub.facts)?) + } else { + None + } + }; + + // The slot nothing will ever ask for again (fn docs). Released AFTER the + // blit above, so the surface is read before anything can be assigned it. + if plan.header.refresh_frame_flags == 0 { + if let Some(session) = self.session.as_mut() { + if session.slots.release(sub.setup_id) { + tracing::trace!( + id = sub.setup_id, + slot = sub.setup_slot, + "AV1 frame refreshes no reference slot — returning its surface" + ); + } + if let Some(held) = session.held.get_mut(usize::from(sub.setup_slot)) { + *held = None; + } + } + } + Ok(shown) + } + + /// Convert one AV1 frame, (re)building the session when the sequence moved. + /// + /// ⚠ `self.status_id` is deliberately NOT advanced here. AV1 submissions carry + /// a zero `StatusReportFeedbackNumber` — libavcodec's `dxva2_av1.c` has the + /// assignment commented out because setting it breaks decoding on some NVIDIA + /// drivers, and Chromium ships the zero for the same reason — so + /// [`pf_dxvadec::plan_to_dxva_av1`] takes no id to write. + fn plan_frame_av1(&mut self, au: &[u8], plan: &pf_dxvadec::AuPlanAv1) -> Result { + let session = ensure_session( + &mut self.session, + &self.device, + &self.video_device, + self.codec, + StreamShape::of_av1(plan), + )?; + let dxva = pf_dxvadec::plan_to_dxva_av1(au, plan, &mut session.slots) + .map_err(|e| anyhow!("plan → DXVA: {e}"))?; + Ok(Submission { + pic_params: pf_dxvadec::as_bytes(&dxva.pic_params).to_vec(), + // AV1 transmits no quantization matrix: its matrices are SELECTED by + // index (`qm_y`/`qm_u`/`qm_v`) out of tables the decoder already has. + // `dxva2_av1_end_frame` passes `NULL, 0` for the pair and the generic + // layer then submits no such buffer at all. + qmatrix: None, + mb_count: 0, + slice_ranges: Vec::new(), + setup_slot: dxva.setup_slot, + setup_id: dxva.setup_id, + codec: Codec::Av1, + facts: PictureFacts { + colour: colour_of(plan.picture.colour), + keyframe: plan.picture.is_key, + // The RENDER size, which is AV1's display region — the counterpart + // of the other two codecs' conformance-window crop, and (with + // superres) not the same as the decoded `upscaled_width`. + // + // ⚠ Treated as a CROP, which is what the native Vulkan rung does + // (`decoder_av1`'s `DisplayCrop`) and what the goldens hash ("the + // 320x240 render region"). libavcodec instead keeps the frame at + // `upscaled_width` x `frame_height` and expresses the render size + // as a sample aspect RATIO, so on a stream where the two differ + // this rung shows less picture than libavcodec would. No + // punktfunk host emits such a stream and neither vendored vector + // is one; the choice is here so both native rungs answer alike, + // not because it is settled. + // + // ⚠ CLAMPED to the decoded picture. AV1's render size is a display + // HINT with no upper bound in 5.9.6 — a stream may legally ask to + // be shown at more than it coded — and a crop taken from it + // unclamped hands `VideoProcessorBlt` a source rectangle larger + // than the surface. The same clamp is in the Vulkan rung's + // `DisplayCrop` (`pf_vkdecode::decoder_av1`). + width: plan.picture.render_width.min(plan.picture.upscaled_width), + height: plan.picture.render_height.min(plan.picture.frame_height), + }, + concealed: false, + av1: Some(Av1Buffers { + bitstream: dxva.bitstream, + tiles: dxva.tiles, + }), + show: plan.picture.show_frame, + }) + } + + /// A `show_existing_frame` access unit: blit a surface the pool already holds. + /// + /// The picture's geometry and colour come from [`Session::held`] rather than + /// from this plan, because a `show_existing_frame` header carries none of its + /// own (AV1 5.9.2 LOADS the shown frame's state) — see [`PictureFacts`]. + /// + /// Everything here is `Ok(None)` rather than an error when the slot is empty: + /// that case is already reported as `MissingShowExisting`, which is an + /// integrity warning, so the caller has concealed the unit and asked for a + /// keyframe before this could return. + fn show_existing_av1(&mut self, plan: &pf_dxvadec::AuPlanAv1) -> Result> { + let target = self.session.as_ref().and_then(|session| { + let id = plan.dpb.outputs.first().copied()?; + let slot = session.slots.slot_of(id)?; + let facts = (*session.held.get(usize::from(slot))?)?; + Some((slot, facts)) + }); + // Showing a KEY frame this way resets the whole reference store (7.20), so + // the plan's removals are real and this rung's slot map has to follow them + // — or the map fills up and the next assignment fails. + if let Some(session) = self.session.as_mut() { + for &id in &plan.dpb.removed { + session.slots.release(id); + } + } + match target { + Some((slot, facts)) => self.present(slot, facts).map(Some), + None => Ok(None), + } + } + + /// Plan one AU and convert it, (re)building the session when the stream's shape moved. + /// + /// `Ok(None)` is the RASL skip and nothing else. + fn plan(&mut self, au: &[u8]) -> Result> { + self.status_id = self.status_id.wrapping_add(1).max(1); + let status_id = self.status_id; + match &mut self.planner { + Planner::H264(planner) => { + let plan = planner.plan_au(au).map_err(|e| anyhow!("plan: {e}"))?; + let concealed = plan.warnings.iter().any(pf_dxvadec::is_integrity_warning); + let session = ensure_session( + &mut self.session, + &self.device, + &self.video_device, + self.codec, + StreamShape { + coded_width: plan.picture.coded_width, + coded_height: plan.picture.coded_height, + max_dpb_frames: plan.picture.max_dpb_frames, + chroma_format_idc: plan.picture.chroma_format_idc, + bit_depth_luma_minus8: plan.picture.bit_depth_luma_minus8, + bit_depth_chroma_minus8: plan.picture.bit_depth_chroma_minus8, + }, + )?; + let dxva = pf_dxvadec::plan_to_dxva(&plan, &mut session.slots, status_id) + .map_err(|e| anyhow!("plan → DXVA: {e}"))?; + Ok(Some(Submission { + pic_params: pf_dxvadec::as_bytes(&dxva.pic_params).to_vec(), + // H.264 always carries the matrices: libavcodec's `dxva2_h264_end_frame` + // submits the buffer unconditionally, and the PPS's lists are always + // meaningful (the parser has applied Table 7-2's fallback rules). + qmatrix: Some(pf_dxvadec::as_bytes(&dxva.qmatrix).to_vec()), + mb_count: dxva.mb_count, + slice_ranges: dxva.slice_ranges, + setup_slot: dxva.setup_slot, + setup_id: dxva.setup_id, + codec: Codec::H264, + facts: PictureFacts { + colour: colour_of(plan.picture.colour), + keyframe: plan.picture.is_idr, + width: plan.picture.display_crop.width, + height: plan.picture.display_crop.height, + }, + concealed, + av1: None, + show: true, + })) + } + Planner::H265(planner) => { + let plan = match planner.plan_au(au) { + Ok(plan) => plan, + // An HEVC stream joined at a CRA carries leading pictures whose + // references precede the join; the spec's answer is to decode and output + // nothing for them. Never an error — mapping it to one would make every + // open-GOP join beg the host for a keyframe it has no reason to send. + Err(pf_dxvadec::PlanErrorH265::RaslSkipped { poc }) => { + tracing::debug!(poc, "RASL picture skipped after an open-GOP join"); + return Ok(None); + } + Err(e) => bail!("plan: {e}"), + }; + let concealed = plan + .warnings + .iter() + .any(pf_dxvadec::is_integrity_warning_h265); + let session = ensure_session( + &mut self.session, + &self.device, + &self.video_device, + self.codec, + StreamShape { + coded_width: plan.picture.coded_width, + coded_height: plan.picture.coded_height, + max_dpb_frames: plan.picture.max_dpb_frames, + chroma_format_idc: plan.picture.chroma_format_idc, + bit_depth_luma_minus8: plan.picture.bit_depth_luma_minus8, + bit_depth_chroma_minus8: plan.picture.bit_depth_chroma_minus8, + }, + )?; + let dxva = pf_dxvadec::plan_to_dxva_h265(&plan, &mut session.slots, status_id) + .map_err(|e| anyhow!("plan → DXVA: {e}"))?; + Ok(Some(Submission { + pic_params: pf_dxvadec::as_bytes(&dxva.pic_params).to_vec(), + // `None` unless the sequence enables scaling lists — the buffer is then + // not submitted at all, which is libavcodec's own condition. + qmatrix: dxva + .qmatrix + .as_ref() + .map(|qm| pf_dxvadec::as_bytes(qm).to_vec()), + // libavcodec's HEVC path leaves `NumMBsInBuffer` 0: HEVC has no + // macroblocks, and the field has no CTB spelling. + mb_count: 0, + slice_ranges: dxva.slice_ranges, + setup_slot: dxva.setup_slot, + setup_id: dxva.setup_id, + codec: Codec::H265, + facts: PictureFacts { + colour: colour_of(plan.picture.colour), + keyframe: plan.picture.is_irap, + width: plan.picture.display_crop.width, + height: plan.picture.display_crop.height, + }, + concealed, + av1: None, + show: true, + })) + } + // An AV1 access unit is a temporal UNIT: `plan_au` answers with a + // `Vec`, and one `Submission` cannot represent it. The AV1 path is + // [`Self::decode_av1`], which walks the unit frame by frame and comes + // back here per frame through [`Self::plan_frame_av1`]. + Planner::Av1(_) => bail!( + "an AV1 temporal unit is planned frame by frame (decode_av1), not through plan()" + ), + } + } + + /// Decode one picture and hand it off — the H.264/H.265 shape, where an access + /// unit is a picture and every picture displays. + fn submit(&mut self, au: &[u8], sub: &Submission) -> Result { + self.decode_into(au, sub)?; + self.present(sub.setup_slot, sub.facts) + } + + /// `DecoderBeginFrame` → the codec's buffers → `SubmitDecoderBuffers` → + /// `DecoderEndFrame`. Writes the decode surface and NOTHING else. + /// + /// Split from the hand-off because AV1 decodes frames that are never shown: a + /// hidden alt-ref is a reference for what follows, and blitting it would put it + /// on the presenter's screen for one frame. + /// + /// Buffer order matches libavcodec's exactly (picture parameters, quantization + /// matrices, bitstream, slice control): a driver is entitled to care, and + /// matching the path every Windows player exercises costs nothing. + fn decode_into(&mut self, au: &[u8], sub: &Submission) -> Result<()> { + let session = self + .session + .as_ref() + .ok_or_else(|| anyhow!("no decode session (plan should have built one)"))?; + let view = session + .views + .get(usize::from(sub.setup_slot)) + .ok_or_else(|| anyhow!("setup surface {} is outside the pool", sub.setup_slot))?; + + begin_frame(&self.video_context, &session.decoder, view)?; + // From here the decoder is INSIDE a frame; every exit must end it, or the next AU's + // `DecoderBeginFrame` fails and the session is wedged. `end_frame` is therefore + // called on both paths rather than only on success. + let result = self.fill_and_submit(au, sub, session); + // SAFETY: a COM call on the live video context, ending the frame this method began on + // the live decoder. Its own failure is reported only when nothing worse happened. + let ended = unsafe { self.video_context.DecoderEndFrame(&session.decoder) }; + result?; + ended.ok().context("DecoderEndFrame") + } + + /// The shared `VideoProcessorBlt` → shareable-RGBA hand-off, for a surface the + /// pool already holds. + /// + /// Takes a surface index and the picture's facts rather than a [`Submission`], + /// because AV1's `show_existing_frame` presents a picture whose submission was + /// several access units ago. + fn present(&mut self, slot: u8, facts: PictureFacts) -> Result { + // `pool` is the decode texture array and `slot` its slice — the very shape + // libavcodec's `data[0]`/`data[1]` describe, which is why this is the same + // call its D3D11VA rung made. + let pool = self + .session + .as_ref() + .ok_or_else(|| anyhow!("no decode session to present from"))? + .pool + .clone(); + self.handoff.present(HandoffSource { + texture: &pool, + array_slice: u32::from(slot), + width: facts.width, + height: facts.height, + color: facts.colour, + keyframe: facts.keyframe, + decoder: DECODER_PIN, + }) + } + + /// The decoder buffers, filled and submitted. Split out so the caller can guarantee + /// `DecoderEndFrame` on every path. + fn fill_and_submit(&self, au: &[u8], sub: &Submission, session: &Session) -> Result<()> { + match &sub.av1 { + Some(av1) => self.fill_and_submit_av1(au, av1, sub, session), + None => self.fill_and_submit_slices(au, sub, session), + } + } + + /// AV1's buffer set: picture parameters, bitstream, **tile control**. + /// + /// Three, never four — `dxva2_av1_end_frame` hands `ff_dxva2_common_end_frame` + /// a `NULL, 0` quantization matrix and the generic layer's `if (qm_size > 0)` + /// then skips the buffer entirely. AV1 transmits no matrix at all: its + /// quantiser matrices are selected by index out of tables the decoder has. + /// + /// The tile records go in the SLICE_CONTROL buffer, which is where the other + /// two codecs put their `DXVA_Slice_*_Short` records — a different structure + /// (sixteen bytes, one per TILE, carrying that tile's grid position) in the + /// same buffer slot. + /// + /// `NumMBsInBuffer` is 0 on all three descriptors. That is not symmetry with + /// HEVC, it is `dxva2_av1.c` read literally: it writes `dsc11->NumMBsInBuffer = + /// 0` on the bitstream descriptor and passes a literal `0` as + /// `ff_dxva2_commit_buffer`'s `mb_count` for the tiles. There is no tile-count + /// spelling of the field, and inventing one would be a fresh divergence on the + /// exact call an Intel driver has already rejected a hand-built variant of. + fn fill_and_submit_av1( + &self, + au: &[u8], + av1: &Av1Buffers, + sub: &Submission, + session: &Session, + ) -> Result<()> { + // Written in libavcodec's own order — picture parameters, bitstream, tile + // control — because that is the order it maps, fills and releases the + // driver's buffers in, and this file's method is to reproduce that path + // rather than to assume the order is free. + let pp_size = write_buffer( + &self.video_context, + &session.decoder, + D3D11_VIDEO_DECODER_BUFFER_PICTURE_PARAMETERS, + |dst| { + copy_into(dst, &sub.pic_params)?; + Ok(sub.pic_params.len()) + }, + )?; + + // The bitstream is packed IN PLACE in the driver's mapping — no staging + // copy — and hands back the tile records the control buffer below is built + // from, their `DataOffset`s rebased into that mapping. That ordering is why + // the two cannot be one step. + let mut packed = None; + let bs_size = write_buffer( + &self.video_context, + &session.decoder, + D3D11_VIDEO_DECODER_BUFFER_BITSTREAM, + |dst| { + let p = pf_dxvadec::pack_av1(au, &av1.bitstream, &av1.tiles, dst) + .map_err(|e| anyhow!("AV1 tile pack: {e}"))?; + let size = p.data_size as usize; + packed = Some(p); + Ok(size) + }, + )?; + let packed = packed.expect("the writer above ran or returned an error"); + + let tile_bytes = pf_dxvadec::slice_bytes(&packed.tiles); + let tc_size = write_buffer( + &self.video_context, + &session.decoder, + D3D11_VIDEO_DECODER_BUFFER_SLICE_CONTROL, + |dst| { + copy_into(dst, tile_bytes)?; + Ok(tile_bytes.len()) + }, + )?; + + // The descriptor SET comes from pf-dxvadec, which has CPU tests for it on + // every CI leg — the buffer types, the order, the sizes and the zero + // `NumMBsInBuffer`. Two of review 13's three structural defects lived in + // descriptors built inside this file, where nothing could see them; this + // arm is built from the tested table and only the byte counts are checked + // against what the writers above actually wrote. + let descs = pf_dxvadec::descriptors_av1(&packed); + let written = [ + (pf_dxvadec::BUFFER_PICTURE_PARAMETERS, pp_size), + (pf_dxvadec::BUFFER_BITSTREAM, bs_size), + (pf_dxvadec::BUFFER_SLICE_CONTROL, tc_size), + ]; + let mut out: Vec = Vec::with_capacity(descs.len()); + for desc in &descs { + let wrote = written + .iter() + .find(|(kind, _)| *kind == desc.buffer_type) + .map(|(_, size)| *size) + .ok_or_else(|| anyhow!("no writer for AV1 buffer type {}", desc.buffer_type))?; + if wrote != desc.data_size as usize { + bail!( + "AV1 buffer type {} was written with {wrote} bytes, the descriptor \ + declares {}", + desc.buffer_type, + desc.data_size + ); + } + out.push(buffer_desc( + buffer_kind(desc.buffer_type)?, + desc.data_size as usize, + desc.num_mbs_in_buffer, + )); + } + + // SAFETY: a COM call on the live video context with the live decoder and a slice of + // fully-initialized descriptors that outlives the call. Every buffer named by a + // descriptor was released back to the driver by `write_buffer` before this runs, + // which is what makes them submittable. + unsafe { + self.video_context + .SubmitDecoderBuffers(&session.decoder, &out) + } + .ok() + .context("SubmitDecoderBuffers (AV1)") + } + + /// The H.264/H.265 buffer set: picture parameters, [quantization matrices], + /// bitstream, slice control. + fn fill_and_submit_slices(&self, au: &[u8], sub: &Submission, session: &Session) -> Result<()> { + let mut descs: Vec = Vec::with_capacity(4); + + let pp_size = write_buffer( + &self.video_context, + &session.decoder, + D3D11_VIDEO_DECODER_BUFFER_PICTURE_PARAMETERS, + |dst| { + copy_into(dst, &sub.pic_params)?; + Ok(sub.pic_params.len()) + }, + )?; + descs.push(buffer_desc( + D3D11_VIDEO_DECODER_BUFFER_PICTURE_PARAMETERS, + pp_size, + 0, + )); + + // The quantization matrices, when the stream has any. An HEVC sequence with scaling + // lists disabled submits NO such buffer — libavcodec's condition exactly — because + // the picture parameters have already told the driver to ignore the matrix, and a + // driver that honours what it was handed anyway would dequantize against it. + if let Some(qmatrix) = &sub.qmatrix { + let qm_size = write_buffer( + &self.video_context, + &session.decoder, + D3D11_VIDEO_DECODER_BUFFER_INVERSE_QUANTIZATION_MATRIX, + |dst| { + copy_into(dst, qmatrix)?; + Ok(qmatrix.len()) + }, + )?; + descs.push(buffer_desc( + D3D11_VIDEO_DECODER_BUFFER_INVERSE_QUANTIZATION_MATRIX, + qm_size, + 0, + )); + } + + // The bitstream buffer is packed IN PLACE in the driver's mapping — no staging copy — + // and hands back the slice locations the control buffer below is built from. That + // ordering is why the two cannot be one step. + let mut packed = None; + let bs_size = write_buffer( + &self.video_context, + &session.decoder, + D3D11_VIDEO_DECODER_BUFFER_BITSTREAM, + |dst| { + let p = pf_dxvadec::pack(au, &sub.slice_ranges, dst) + .map_err(|e| anyhow!("bitstream pack: {e}"))?; + let size = p.data_size as usize; + packed = Some(p); + Ok(size) + }, + )?; + descs.push(buffer_desc( + D3D11_VIDEO_DECODER_BUFFER_BITSTREAM, + bs_size, + sub.mb_count, + )); + let packed = packed.expect("the writer above ran or returned an error"); + + let sc_size = write_buffer( + &self.video_context, + &session.decoder, + D3D11_VIDEO_DECODER_BUFFER_SLICE_CONTROL, + |dst| match sub.codec { + Codec::H264 => { + let records = pf_dxvadec::slice_control(&packed.records); + let bytes = pf_dxvadec::slice_bytes(&records); + copy_into(dst, bytes)?; + Ok(bytes.len()) + } + Codec::H265 => { + let records = pf_dxvadec::slice_control_h265(&packed.records); + let bytes = pf_dxvadec::slice_bytes(&records); + copy_into(dst, bytes)?; + Ok(bytes.len()) + } + // Unreachable: an AV1 submission carries `av1: Some(..)` and + // `fill_and_submit` dispatched it to the other arm. Spelled as a + // refusal rather than a catch-all so that adding a fourth codec + // fails to compile here instead of silently packing its tiles as + // H.264 slices. + Codec::Av1 => bail!("AV1 does not submit slice-control records"), + }, + )?; + descs.push(buffer_desc( + D3D11_VIDEO_DECODER_BUFFER_SLICE_CONTROL, + sc_size, + sub.mb_count, + )); + + // SAFETY: a COM call on the live video context with the live decoder and a slice of + // fully-initialized descriptors that outlives the call. Every buffer named by a + // descriptor was released back to the driver by `write_buffer` before this runs, + // which is what makes them submittable. + unsafe { + self.video_context + .SubmitDecoderBuffers(&session.decoder, &descs) + } + .ok() + .context("SubmitDecoderBuffers") + } +} + +/// pf-bitstream's H.273 code points as the presenter's [`ColorDesc`]. +/// +/// Per picture, never latched at session start: the Windows host switches an HDR desktop to +/// PQ/BT.2020 IN-BAND with a new SPS mid-stream, and a backend that captured the first AU's +/// colour would paint HDR frames washed out. (pf-bitstream applies E.2.1's "unspecified" +/// inference where the VUI is silent, so these are always meaningful code points.) +fn colour_of(colour: pf_dxvadec::ColourDescription) -> ColorDesc { + ColorDesc { + primaries: colour.colour_primaries, + transfer: colour.transfer_characteristics, + matrix: colour.matrix_coefficients, + full_range: colour.video_full_range, + } +} + +/// Does the adapter expose this decode profile, for this surface format? +/// +/// Checked at construction rather than at the first AU, for the same reason libavcodec's +/// D3D11VA rung checked it there: an unsupported profile discovered mid-stream costs the +/// opening IDR and exits only through a demotion streak. +fn profile_supported(video: &ID3D11VideoDevice, profile: DxvaProfile) -> Result<()> { + let wanted = GUID::from_u128(profile.guid); + // SAFETY: COM calls on the live video device; the count bounds the loop and each profile + // is returned by value. + let profiles: Vec = unsafe { + let n = video.GetVideoDecoderProfileCount(); + (0..n) + .filter_map(|i| video.GetVideoDecoderProfile(i).ok()) + .collect() + }; + if !profiles.contains(&wanted) { + bail!("adapter exposes no {} decode profile", profile.name); + } + // SAFETY: same live device; the arguments are a borrowed local GUID and a plain format + // enum. + let ok = unsafe { video.CheckVideoDecoderFormat(&wanted, profile.dxgi_format as DXGI_FORMAT) } + .map(|b| b.as_bool()) + .unwrap_or(false); + if !ok { + bail!( + "adapter's {} profile cannot decode into DXGI format {}", + profile.name, + profile.dxgi_format + ); + } + Ok(()) +} + +/// Build the session if there is none, or rebuild it when the stream's shape moved. +/// +/// The shape is read off the SPS the planner just activated, never off the negotiated format: +/// the decoder object, the surface pool, the slot map AND the profile are all derived from it +/// (see [`StreamShape`]), and a partially-rebuilt session hands out surface indices the pool +/// does not have — or decodes at a sample width its surfaces cannot hold. Rebuilding whole is +/// the only correct answer, and it is what the plan → DXVA conversion's `CapacityMismatch` +/// refusal exists to force for the DPB-depth leg. +fn ensure_session<'a>( + slot: &'a mut Option, + device: &ID3D11Device, + video_device: &ID3D11VideoDevice, + codec: Codec, + shape: StreamShape, +) -> Result<&'a mut Session> { + let matches = slot.as_ref().is_some_and(|s| s.shape == shape); + if !matches { + if let Some(old) = slot.as_ref() { + // The old profile is worth a line of its own: a rebuild that also changes it is + // the in-band 8-bit → 10-bit flip, and a field report showing the decoder + // following the stream there is the difference between "HDR looked wrong" and a + // diagnosis. + tracing::info!( + was = ?old.shape, + was_profile = old.profile.name, + now = ?shape, + "stream renegotiated — rebuilding the native D3D11VA decode session" + ); + } + // Dropped BEFORE the replacement is built so the old pool's VRAM is released first — + // a 4K pool is on the order of a hundred megabytes and holding two while the new one + // allocates is how a rebuild fails on a small card. + *slot = None; + *slot = Some(Session::build(device, video_device, codec, shape)?); + } + Ok(slot.as_mut().expect("built or already matching")) +} + +impl Session { + fn build( + device: &ID3D11Device, + video_device: &ID3D11VideoDevice, + codec: Codec, + shape: StreamShape, + ) -> Result { + // A single `DXGI_FORMAT` carries one sample width for both planes, so a stream whose + // chroma is coded deeper than its luma has no surface this backend can allocate. + // Refused rather than approximated: the ladder walks on to the next rung. + if shape.bit_depth_chroma_minus8 != shape.bit_depth_luma_minus8 { + bail!( + "luma is {}-bit and chroma is {}-bit; no DXGI decode format carries both", + shape.bit_depth(), + 8 + shape.bit_depth_chroma_minus8 + ); + } + // Derived HERE, from the SPS, rather than latched from the negotiated format at + // construction — the two can disagree, and this is the one that decodes. + let profile = pf_dxvadec::profile_for(codec, shape.chroma_format_idc, shape.bit_depth()) + .ok_or_else(|| { + anyhow!( + "no DXVA profile for {codec:?} chroma_format_idc {} at {} bits", + shape.chroma_format_idc, + shape.bit_depth() + ) + })?; + profile_supported(video_device, profile)?; + let guid = GUID::from_u128(profile.guid); + // `DXGI_FORMAT` is a plain type alias in this windows-rs rev, so the profile's raw + // code point IS the format; the cast is the alias, not a conversion. + let format = profile.dxgi_format as DXGI_FORMAT; + let coded_width = shape.coded_width; + let coded_height = shape.coded_height; + // The SURFACES are aligned to the codec's granule; the DECODER is told the CODED + // size. That is libavcodec's split — `d3d11va_create_decoder` passes + // `avctx->coded_width/coded_height` into `D3D11_VIDEO_DECODER_DESC` while + // `ff_dxva2_common_frame_params` allocates the texture at `FFALIGN(coded, + // surface_alignment)` — and the two are not interchangeable: a driver may reject an + // over-large `SampleHeight`, or hand back a different config list for it. + let aligned_width = pf_dxvadec::align_surface(coded_width, codec); + let aligned_height = pf_dxvadec::align_surface(coded_height, codec); + let desc = D3D11_VIDEO_DECODER_DESC { + Guid: guid, + SampleWidth: coded_width, + SampleHeight: coded_height, + OutputFormat: format, + }; + + // Enumerate the driver's configs and pick a short-format one (pf-dxvadec's + // `pick_config` is the whole decision, and it is unit-tested). The driver's own + // struct is handed back to `CreateVideoDecoder` untouched: re-synthesising it from + // the three fields selection reads would drop the dozen `Config*` members a driver + // may care about. + // SAFETY: COM calls on the live video device with a borrowed local descriptor; the + // count bounds the loop and each config is written into a local that outlives its + // call. + let configs: Vec = unsafe { + let count = video_device + .GetVideoDecoderConfigCount(&desc) + .context("GetVideoDecoderConfigCount")?; + let mut out = Vec::with_capacity(count as usize); + for i in 0..count { + let mut config = D3D11_VIDEO_DECODER_CONFIG::default(); + if video_device + .GetVideoDecoderConfig(&desc, i, &mut config) + .ok() + .is_ok() + { + out.push(config); + } + } + out + }; + let facts: Vec = configs + .iter() + .map(|c| pf_dxvadec::ConfigFacts { + bitstream_raw: c.ConfigBitstreamRaw, + no_encryption: c.guidConfigBitstreamEncryption == GUID::zeroed(), + min_render_target_buffers: c.ConfigMinRenderTargetBuffCount, + }) + .collect(); + let index = pf_dxvadec::pick_config(codec, &facts).ok_or_else(|| { + anyhow!( + "{} offers no short-format ({}) decoder config among {} — this rung \ + implements the short slice format only, and this adapter offers none", + profile.name, + pf_dxvadec::short_slice_config(codec), + facts.len() + ) + })?; + let config = configs[index]; + + // SAFETY: a COM call on the live video device over two borrowed local descriptors; + // the returned decoder is owned by this `Session`. + let decoder = unsafe { video_device.CreateVideoDecoder(&desc, &config) } + .context("CreateVideoDecoder")?; + + let slots = pf_dxvadec::SlotMap::new(shape.max_dpb_frames); + let pool_size = + pf_dxvadec::pool_size(slots.capacity(), facts[index].min_render_target_buffers); + + // THE decode pool — one texture array, `D3D11_BIND_DECODER` only, no share flags. + // See the module docs for why every one of these fields is what it is. + let pool_desc = D3D11_TEXTURE2D_DESC { + Width: aligned_width, + Height: aligned_height, + MipLevels: 1, + ArraySize: pool_size, + Format: format, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: BIND_DECODER, + CPUAccessFlags: 0, + MiscFlags: 0, + }; + let mut pool = None; + // SAFETY: a `?`-checked `CreateTexture2D` on the live device, over a fully-initialized + // stack descriptor and a live `Option` out-param. + unsafe { device.CreateTexture2D(&pool_desc, None, Some(&mut pool)) } + .ok() + .context("create the D3D11VA decode surface pool")?; + let pool: ID3D11Texture2D = pool.expect("CreateTexture2D succeeded"); + + // One output view per array slice. The view is what `DecoderBeginFrame` targets, and + // its `ArraySlice` is the DXVA surface index — so `views[i]` decodes into surface i, + // which is DPB slot i. + let mut views = Vec::with_capacity(pool_size as usize); + for slice in 0..pool_size { + let mut view_desc = D3D11_VIDEO_DECODER_OUTPUT_VIEW_DESC { + DecodeProfile: guid, + ViewDimension: D3D11_VDOV_DIMENSION_TEXTURE2D, + ..Default::default() + }; + view_desc.Anonymous.Texture2D.ArraySlice = slice; + let mut view = None; + // SAFETY: COM calls on the live video device with the pool texture just created + // and a borrowed local descriptor; the out-param is checked before use. + unsafe { + video_device.CreateVideoDecoderOutputView(&pool, &view_desc, Some(&mut view)) + } + .ok() + .context("CreateVideoDecoderOutputView")?; + views.push(view.expect("output view created")); + } + + tracing::info!( + profile = profile.name, + coded_width, + coded_height, + aligned_width, + aligned_height, + bit_depth = shape.bit_depth(), + chroma_format_idc = shape.chroma_format_idc, + pool_size, + dpb_slots = slots.capacity(), + config_bitstream_raw = config.ConfigBitstreamRaw, + "native D3D11VA decode session built" + ); + Ok(Session { + decoder, + pool, + views, + slots, + held: vec![None; pool_size as usize], + shape, + profile, + }) + } +} + +/// `DecoderBeginFrame` with the `E_PENDING` retry loop — the hardware is still busy with an +/// earlier picture, which is a wait, not a failure. +fn begin_frame( + context: &ID3D11VideoContext, + decoder: &ID3D11VideoDecoder, + view: &ID3D11VideoDecoderOutputView, +) -> Result<()> { + for attempt in 0..BEGIN_FRAME_RETRIES { + // SAFETY: a COM call on the live video context with the live decoder and output view; + // the content-key arguments are the "no protected content" pair (size 0, null). + let hr = unsafe { context.DecoderBeginFrame(decoder, view, 0, None) }; + if hr.0 == E_PENDING { + // libavcodec's own back-off, to the microsecond — see the constants. + std::thread::sleep(BEGIN_FRAME_BACKOFF); + continue; + } + return hr + .ok() + .with_context(|| format!("DecoderBeginFrame (after {attempt} pending retries)")); + } + bail!("DecoderBeginFrame stayed E_PENDING for {BEGIN_FRAME_RETRIES} attempts") +} + +/// Map one decoder buffer, let `write` fill it, and release it back to the driver. +/// +/// The release is unconditional: a buffer left mapped wedges every later `GetDecoderBuffer` +/// of the same type, so a writer's error must not be allowed to skip it. Returns the number +/// of bytes the writer used, for the buffer's `DataSize`. +fn write_buffer( + context: &ID3D11VideoContext, + decoder: &ID3D11VideoDecoder, + kind: D3D11_VIDEO_DECODER_BUFFER_TYPE, + write: impl FnOnce(&mut [u8]) -> Result, +) -> Result { + let mut size = 0u32; + let mut ptr: *mut std::ffi::c_void = std::ptr::null_mut(); + // SAFETY: a COM call on the live video context and decoder; both out-params are locals + // that outlive the call, and neither is read before the HRESULT is checked. + unsafe { context.GetDecoderBuffer(decoder, kind, &mut size, &mut ptr) } + .ok() + .with_context(|| format!("GetDecoderBuffer({kind:?})"))?; + if ptr.is_null() { + // Nothing was mapped, so nothing must be released. + bail!("GetDecoderBuffer({kind:?}) returned a null mapping"); + } + // SAFETY: `GetDecoderBuffer` succeeded and reported a non-null pointer to a mapping of + // `size` bytes that the driver keeps valid until the matching `ReleaseDecoderBuffer` + // below — which runs before this borrow can escape, because the slice is confined to + // `write`'s call. Write-only, so uninitialized driver memory is never read; `u8` has no + // alignment requirement, and a decoder buffer never approaches `isize::MAX`. + let dst = unsafe { std::slice::from_raw_parts_mut(ptr.cast::(), size as usize) }; + let written = write(dst); + // SAFETY: releases exactly the buffer mapped above, on the same live context and decoder. + let released = unsafe { context.ReleaseDecoderBuffer(decoder, kind) }; + let written = written.with_context(|| format!("filling the {kind:?} decoder buffer"))?; + released + .ok() + .with_context(|| format!("ReleaseDecoderBuffer({kind:?})"))?; + Ok(written) +} + +/// pf-dxvadec's `BUFFER_*` code point as the windows-rs constant of the same name. +/// +/// Deliberately a match on the four constants rather than a numeric cast: the code +/// points are asserted against windows-rs's own values in +/// `pf_dxvadec::descriptors`, and going through the named constants here means the +/// Windows type's representation (newtype or alias) is never assumed. +fn buffer_kind(code: u32) -> Result { + Ok(match code { + pf_dxvadec::BUFFER_PICTURE_PARAMETERS => D3D11_VIDEO_DECODER_BUFFER_PICTURE_PARAMETERS, + pf_dxvadec::BUFFER_INVERSE_QUANTIZATION_MATRIX => { + D3D11_VIDEO_DECODER_BUFFER_INVERSE_QUANTIZATION_MATRIX + } + pf_dxvadec::BUFFER_SLICE_CONTROL => D3D11_VIDEO_DECODER_BUFFER_SLICE_CONTROL, + pf_dxvadec::BUFFER_BITSTREAM => D3D11_VIDEO_DECODER_BUFFER_BITSTREAM, + other => bail!("unknown DXVA buffer type {other}"), + }) +} + +/// A submission descriptor for one filled buffer. +/// +/// `mb_count` is `NumMBsInBuffer`, and it is NOT uniformly 0. libavcodec's H.264 path +/// computes `h->mb_width * h->mb_height` and writes it on both the BITSTREAM and the +/// SLICE_CONTROL descriptor (`commit_bitstream_and_slice_buffer`, for both slice formats, +/// the second through `ff_dxva2_commit_buffer`'s `mb_count` argument); its HEVC path writes +/// 0 on the same two, and its AV1 path writes 0 on all three. Picture parameters and +/// quantization matrices take 0 in every codec. +/// +/// The value is arguably redundant in VLD mode — the driver has the same two numbers in the +/// picture parameters — but this module's whole method is to reproduce libavcodec exactly, +/// on the evidence that a hand-built variant was rejected by Intel at the first +/// `SubmitDecoderBuffers`, and this is a field libav fills on precisely that call. +fn buffer_desc( + kind: D3D11_VIDEO_DECODER_BUFFER_TYPE, + size: usize, + mb_count: u32, +) -> D3D11_VIDEO_DECODER_BUFFER_DESC { + D3D11_VIDEO_DECODER_BUFFER_DESC { + BufferType: kind, + DataSize: size as u32, + NumMBsInBuffer: mb_count, + ..Default::default() + } +} + +/// Copy `src` into the driver's mapping, refusing rather than truncating. +fn copy_into(dst: &mut [u8], src: &[u8]) -> Result<()> { + if src.len() > dst.len() { + bail!( + "a {}-byte DXVA buffer does not fit the driver's {}-byte mapping", + src.len(), + dst.len() + ); + } + dst[..src.len()].copy_from_slice(src); + Ok(()) +} + +#[cfg(test)] +mod parity { + //! Frame-hash parity for this rung — the evidence M5 shipped without. + //! + //! `#[ignore]`d: it needs a real D3D11 video device. Run it on a Windows box with + //! + //! ```text + //! cargo test -p pf-client-core --lib video_d3d11_native -- --ignored --nocapture + //! ``` + //! + //! and pin a GPU on a multi-adapter box with `PF_DXVA_ADAPTER=` — .173 enumerates its AMD iGPU first, not the 4090, so an + //! unpinned run there reports the iGPU and that is a fact worth printing rather + //! than assuming. + //! + //! # What it proves, and against what + //! + //! The same thing `pf-vkdecode`'s `gpu_parity` proves for the Vulkan rung, against + //! the same reference: H.264 and H.265 decoding are exactly specified, so a + //! conformant decoder must reproduce libavcodec's SOFTWARE output bit for bit. The + //! goldens are therefore libavcodec's, not the FFmpeg D3D11VA rung's — ground truth + //! rather than a peer implementation, and the identical yardstick M3 was held to, + //! which makes the two rungs' verdicts directly comparable. It reads back the + //! DECODE surface, before the `VideoProcessorBlt`, so what is hashed is what this + //! rung is responsible for: the shared hand-off is the field-proven half. + //! + //! # Why the harness reorders and the rung does not + //! + //! This rung presents every picture the instant it decodes: `submit` blits + //! `setup_slot` and returns. It never consults `AuPlan::dpb.outputs`, which is + //! where display order lives — the native Vulkan rung keeps a display-order queue + //! for exactly that reason, and libavcodec's D3D11VA rung reorders internally. + //! + //! For punktfunk's own streams the two orders coincide (hosts emit zero-reorder + //! low-delay output with no B pictures), which is why this has never shown. Both + //! vendored conformance vectors DO reorder, though — the H.265 one's first B + //! picture at AU 3 is what localised the RPS slot defect — so a harness that hashed + //! in decode order would report a permutation against display-order goldens and + //! read like a decoder fault. + //! + //! So the harness hashes each decoded surface against the `PicId` the planner + //! assigned it, then emits those hashes in the planner's own output order. The + //! reordering is the TEST's, done by the same planner the rung already trusts, and + //! the divergence is recorded here rather than papered over: a stream that actually + //! reordered would present out of order through this rung today. + //! + //! # The crop + //! + //! The decode pool is aligned to the codec's granule and is therefore TALLER than + //! the picture, so the chroma plane starts at `RowPitch * texture_height`, not + //! `RowPitch * display_height` — reading it at the display height is the 1088-row + //! smear this project has already paid for once. + + use std::collections::HashMap; + + use pf_dxvadec::H264Planner; + use pf_dxvadec::H265Planner; + use sha2::Digest; + use windows::Win32::d3d11::ID3D11Resource; + use windows::Win32::d3d11::D3D11_CPU_ACCESS_READ; + use windows::Win32::d3d11::D3D11_MAPPED_SUBRESOURCE; + use windows::Win32::d3d11::D3D11_MAP_READ; + use windows::Win32::d3d11::D3D11_USAGE_STAGING; + use windows::Win32::dxgi::CreateDXGIFactory1; + use windows::Win32::dxgi::IDXGIFactory1; + use windows::Win32::dxgi::DXGI_ADAPTER_DESC1; + + use super::*; + + /// The vendored H.264 vector — the same file, at the same relative path, that + /// `pf-vkdecode`'s GPU legs decode. 250 access units, two slice NALUs per picture. + const TEST_25FPS_H264: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264" + ); + + /// The vendored H.265 twin: 250 access units, Main 8-bit 4:2:0, one slice each. + const TEST_25FPS_H265: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265" + ); + + /// libavcodec's per-display-frame NV12 hashes. Deliberately the SAME files the + /// Vulkan rung is held to, read across the crate boundary rather than copied: two + /// rungs measured against two copies of a golden set is two measurements, and the + /// point of this file is that they are one. + const GOLDENS_H264: &str = include_str!("../../pf-vkdecode/tests/data/test-25fps.nv12.sha256"); + const GOLDENS_H265: &str = + include_str!("../../pf-vkdecode/tests/data/test-25fps-h265.nv12.sha256"); + + /// Both vendored vectors are 250 display frames. + const FRAME_COUNT: usize = 250; + + /// The Main 10 vector: 50 frames of 320x240 HEVC Main 10 4:2:0, generated by + /// libx265 and hashed from libavcodec's software decode as tightly packed P010. + /// Its provenance, the generation commands and the reason P010 rather than + /// `yuv420p10le` is the golden layout are all in the golden file's header. + const TEST_MAIN10_H265: &[u8] = include_bytes!("../../pf-vkdecode/tests/data/test-main10.h265"); + const GOLDENS_MAIN10: &str = + include_str!("../../pf-vkdecode/tests/data/test-main10.p010.sha256"); + const MAIN10_FRAME_COUNT: usize = 50; + + /// The vendored AV1 vector — an **IVF** file, not an elementary stream, and + /// the same one `pf-vkdecode`'s AV1 legs decode. + const TEST_25FPS_AV1: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1" + ); + + /// libavcodec's per-DELIVERED-frame NV12 hashes for the AV1 vector, 320x240 — + /// read across the crate boundary like the other two, and with the strongest + /// provenance of the three: two independent ffmpeg builds agree byte for byte, + /// cros-codecs' own shipped MD5s reproduce, and libavcodec's Vulkan hwaccel + /// reproduces it on the target driver. + const GOLDENS_AV1: &str = + include_str!("../../pf-vkdecode/tests/data/test-25fps-av1.nv12.sha256"); + + /// 250 temporal units carrying **274 frames**, of which 250 are shown. The gap + /// is the whole reason the AV1 leg is not a third copy of the other two: 24 + /// units decode a hidden picture as well as the one they display. + const AV1_UNIT_COUNT: usize = 250; + const AV1_DECODED_COUNT: usize = 274; + const AV1_SHOWN_COUNT: usize = 250; + + /// The golden file's hash lines (comments and blanks skipped). + fn golden_hashes(file: &'static str) -> Vec<&'static str> { + file.lines() + .map(str::trim) + .filter(|line| !line.is_empty() && !line.starts_with('#')) + .collect() + } + + fn sha256_hex(data: &[u8]) -> String { + use std::fmt::Write as _; + sha2::Sha256::digest(data) + .iter() + .fold(String::with_capacity(64), |mut out, byte| { + let _ = write!(out, "{byte:02x}"); + out + }) + } + + /// Byte offsets of every Annex-B NAL header in `stream`, in order. + /// + /// Emulation prevention guarantees `00 00 01` cannot appear inside a NAL payload, + /// so scanning for it finds start codes and nothing else; the header begins on the + /// byte after. Hand-rolled rather than borrowed from the parser because + /// `pf-client-core` does not depend on the vendored crate — and kept honest by the + /// access-unit count both legs assert, which no plausible splitter bug survives. + fn nal_headers(stream: &[u8]) -> Vec { + let mut out = Vec::new(); + let mut i = 0usize; + while i + 3 <= stream.len() { + if stream[i..i + 3] == [0x00, 0x00, 0x01] { + out.push(i + 3); + i += 3; + } else { + i += 1; + } + } + out + } + + /// Split `stream` into access units, given a per-NAL `(is_slice, starts_a_picture)` + /// rule. A new AU begins at a non-VCL NALU following slices, or at a slice that + /// declares itself the first of a picture when the current AU already has slices — + /// the same rule pf-bitstream applies, spelled once for both codecs. + fn split_aus(stream: &[u8], classify: impl Fn(&[u8], usize) -> (bool, bool)) -> Vec<&[u8]> { + let mut aus = Vec::new(); + let mut au_start = 0usize; + let mut au_has_slice = false; + for header in nal_headers(stream) { + let (is_slice, first_in_picture) = classify(stream, header); + // The start code owning this header: three bytes, plus the optional + // leading zero byte of the four-byte form. + let mut start = header - 3; + if start > 0 && stream[start - 1] == 0x00 { + start -= 1; + } + if au_has_slice && (!is_slice || first_in_picture) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus + } + + /// H.264: one-byte NAL header, `nal_unit_type` in the low 5 bits (1 = non-IDR + /// slice, 5 = IDR slice), and `first_mb_in_slice == 0` is the top bit of the byte + /// after it. + fn split_h264_aus(stream: &[u8]) -> Vec<&[u8]> { + split_aus(stream, |s, h| { + let is_slice = matches!(s[h] & 0x1f, 1 | 5); + let first = is_slice && s.get(h + 1).is_some_and(|b| b & 0x80 != 0); + (is_slice, first) + }) + } + + /// H.265: TWO-byte NAL header, `nal_unit_type` in bits 1..7 of the first byte and + /// "is a slice" the numeric range `< 32`, so `first_slice_segment_in_pic_flag` is + /// the top bit of the byte at `+2` where H.264 reads `+1`. + fn split_h265_aus(stream: &[u8]) -> Vec<&[u8]> { + split_aus(stream, |s, h| { + let is_slice = (s[h] >> 1) & 0x3f < 32; + let first = is_slice && s.get(h + 2).is_some_and(|b| b & 0x80 != 0); + (is_slice, first) + }) + } + + /// The IVF container's frames, in file order. + /// + /// The AV1 vector is not an elementary stream: it is 32 bytes of `DKIF` header + /// followed by `[u32 size][u64 pts][size bytes]` per temporal unit. Hand-rolled + /// for the same reason `nal_headers` is — `pf-client-core` does not depend on + /// the vendored parser crate — and kept honest by the unit count the CPU guard + /// asserts, which no plausible reader bug survives. + fn split_ivf(stream: &[u8]) -> Vec<&[u8]> { + assert_eq!( + &stream[0..4], + b"DKIF", + "the vendored AV1 vector must be an IVF file" + ); + let header = usize::from(u16::from_le_bytes([stream[6], stream[7]])); + let mut out = Vec::new(); + let mut at = header; + while at + 12 <= stream.len() { + let size = u32::from_le_bytes( + stream[at..at + 4] + .try_into() + .expect("four bytes make a u32"), + ) as usize; + at += 12; + assert!( + at + size <= stream.len(), + "an IVF frame header claims {size} bytes past the end of the file" + ); + out.push(&stream[at..at + size]); + at += size; + } + out + } + + /// The decode order and the display order of a vector's pictures, as `PicId`s. + /// + /// Both come from a planner run ALONGSIDE the decoder's own, over the same access + /// units: the planner is deterministic, so the ids it hands this walk are the ids + /// it hands the rung, and no production code has to grow a test accessor. + struct Order { + /// One id per DECODED picture, in submission order — which is one per + /// access unit on H.264/H.265 and one per FRAME on AV1, where a unit can + /// carry more than one. + decode: Vec, + /// The same ids in the planner's output (bumping) order, flush included. + display: Vec, + /// The ids each ACCESS UNIT decodes, in submission order. + /// + /// Only AV1 fills it, and only AV1 needs it: its driver loop hands whole + /// temporal units to the production entry point, which plans them + /// internally, so this is how the harness knows which pictures came out of + /// which unit without a test accessor on the decoder. Empty on the other + /// two, where [`Order::decode`] is already one id per unit. + per_unit: Vec>, + } + + fn order_h264(aus: &[&[u8]]) -> Order { + let mut planner = H264Planner::new(); + let mut order = Order { + decode: Vec::new(), + display: Vec::new(), + per_unit: Vec::new(), + }; + for (index, au) in aus.iter().enumerate() { + let plan = planner + .plan_au(au) + .unwrap_or_else(|e| panic!("AU {index}: the clean vector must plan, got {e:?}")); + assert_eq!( + (plan.picture.display_crop.x, plan.picture.display_crop.y), + (0, 0), + "AU {index}: this rung hands the blit a size and no origin, so a \ + non-zero conformance-window offset would be cropped from the wrong \ + corner — by the rung, not just by this harness" + ); + order.decode.push( + plan.dpb.stored.unwrap_or_else(|| { + panic!("AU {index}: every picture of this vector is stored") + }), + ); + order.display.extend(plan.dpb.outputs.iter().copied()); + } + order.display.extend(planner.flush().outputs); + order + } + + fn order_h265(aus: &[&[u8]]) -> Order { + let mut planner = H265Planner::new(); + let mut order = Order { + decode: Vec::new(), + display: Vec::new(), + per_unit: Vec::new(), + }; + for (index, au) in aus.iter().enumerate() { + let plan = planner + .plan_au(au) + .unwrap_or_else(|e| panic!("AU {index}: the clean vector must plan, got {e:?}")); + assert_eq!( + (plan.picture.display_crop.x, plan.picture.display_crop.y), + (0, 0), + "AU {index}: a non-zero conformance-window offset is cropped from the \ + wrong corner by this rung" + ); + order.decode.push( + plan.dpb.stored.unwrap_or_else(|| { + panic!("AU {index}: every picture of this vector is stored") + }), + ); + order.display.extend(plan.dpb.outputs.iter().copied()); + } + order.display.extend(planner.flush().outputs); + order + } + + /// The AV1 vector's decode and display orders. + /// + /// Where the H.264/H.265 walks push one decoded picture per access unit, this + /// one pushes one per FRAME and an access unit may carry several — which is + /// the whole difference. `display` is still the planner's own output list; + /// AV1 has no bumping process, so a picture is output by the unit that shows + /// it and there is no flush to drain at the end. + fn order_av1(units: &[&[u8]]) -> Order { + let mut planner = pf_dxvadec::Av1Planner::new(); + let mut order = Order { + decode: Vec::new(), + display: Vec::new(), + per_unit: Vec::new(), + }; + for (index, unit) in units.iter().enumerate() { + let plans = planner + .plan_au(unit) + .unwrap_or_else(|e| panic!("unit {index}: the clean vector must plan, got {e:?}")); + let mut this_unit = Vec::new(); + for plan in &plans { + assert!( + plan.warnings.is_empty(), + "unit {index}: a clean vector must plan without warnings, got {:?}", + plan.warnings + ); + assert_eq!( + (plan.picture.render_width, plan.picture.render_height), + (320, 240), + "unit {index}: the goldens are the 320x240 render region" + ); + if let Some(id) = plan.dpb.stored { + order.decode.push(id); + this_unit.push(id); + } + order.display.extend(plan.dpb.outputs.iter().copied()); + } + order.per_unit.push(this_unit); + } + order + } + + /// The LUID of the adapter whose description contains `PF_DXVA_ADAPTER`, and the + /// descriptions of everything enumerated (printed, so a run always says which GPU + /// answered rather than leaving it to be inferred). + fn pinned_adapter() -> Option<[u8; 8]> { + let want = std::env::var("PF_DXVA_ADAPTER").ok(); + // SAFETY: DXGI factory creation takes no pointer and returns an owned factory + // or an error; the `Ok` binding is what proves one came back. + let Ok(factory) = (unsafe { CreateDXGIFactory1::() }) else { + eprintln!("adapters: CreateDXGIFactory1 failed"); + return None; + }; + let mut chosen = None; + for i in 0.. { + // SAFETY: a COM call on the live factory; `Ok` proves an adapter came back. + let Ok(adapter) = (unsafe { factory.EnumAdapters1(i) }) else { + break; + }; + // SAFETY: `DXGI_ADAPTER_DESC1` is plain-old-data, so all-zeroes is valid. + let mut desc: DXGI_ADAPTER_DESC1 = unsafe { std::mem::zeroed() }; + // SAFETY: a COM call on the adapter just enumerated, filling the zeroed + // local through the out-param; checked before the descriptor is read. + if unsafe { adapter.GetDesc1(&mut desc) }.is_err() { + continue; + } + let end = desc + .Description + .iter() + .position(|&c| c == 0) + .unwrap_or(desc.Description.len()); + let name = String::from_utf16_lossy(&desc.Description[..end]); + let mut luid = [0u8; 8]; + luid[..4].copy_from_slice(&desc.AdapterLuid.LowPart.to_le_bytes()); + luid[4..].copy_from_slice(&desc.AdapterLuid.HighPart.to_le_bytes()); + let hit = want + .as_deref() + .is_some_and(|w| name.to_lowercase().contains(&w.to_lowercase())); + eprintln!( + "adapter {i}: {name}{}", + if hit { " <= pinned" } else { "" } + ); + if hit && chosen.is_none() { + chosen = Some(luid); + } + } + if want.is_some() && chosen.is_none() { + panic!("PF_DXVA_ADAPTER matched no adapter (see the list above)"); + } + chosen + } + + /// GPU→CPU readback of one decode-pool slice, cropped to `display` and packed + /// tightly as NV12/P010 — byte-for-byte the layout the goldens hash. + struct Readback { + ctx: ID3D11DeviceContext, + staging: Option, + } + + impl Readback { + fn read( + &mut self, + device: &ID3D11Device, + pool: &ID3D11Texture2D, + slice: u32, + display: (u32, u32), + ) -> Vec { + let mut desc = D3D11_TEXTURE2D_DESC::default(); + // SAFETY: `GetDesc` fills a plain-old-data descriptor through an out-param + // on a live texture and returns nothing to check. + unsafe { pool.GetDesc(&mut desc) }; + + if self.staging.is_none() { + let staging_desc = D3D11_TEXTURE2D_DESC { + Width: desc.Width, + Height: desc.Height, + MipLevels: 1, + ArraySize: 1, + Format: desc.Format, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_STAGING, + BindFlags: 0, + CPUAccessFlags: D3D11_CPU_ACCESS_READ as u32, + MiscFlags: 0, + }; + let mut t: Option = None; + // SAFETY: one `?`-checked call on the live device over a fully + // initialised stack descriptor and a live `Option` out-param. + unsafe { device.CreateTexture2D(&staging_desc, None, Some(&mut t)) } + .ok() + .expect("create the readback staging texture"); + self.staging = t; + } + let staging = self.staging.clone().expect("staging texture"); + + let (width, height) = display; + assert!( + width <= desc.Width && height <= desc.Height, + "the display region {width}x{height} does not fit the {}x{} pool surface", + desc.Width, + desc.Height + ); + let ten_bit = desc.Format == pf_dxvadec::DXGI_FORMAT_P010; + let bytes_per_sample = if ten_bit { 2 } else { 1 }; + let row_bytes = width as usize * bytes_per_sample; + + // SAFETY: `src` and `dst` are the same device's textures of identical + // format and dimensions, so the single-subresource copy on the immediate + // context is valid; `slice` is the array slice the decoder just wrote and + // `MipLevels == 1` makes it the subresource index. `Map(D3D11_MAP_READ)` + // on a STAGING texture blocks until that copy has retired and yields + // `pData` valid for the whole resource: for NV12/P010 the luma plane is + // `desc.Height` rows at `RowPitch` and the chroma plane follows at byte + // offset `RowPitch * desc.Height`, so `total` below is exactly the mapped + // extent and every sub-slice read is inside it. `Unmap` pairs the `Map`. + let out = unsafe { + let src: ID3D11Resource = pool.cast().expect("pool -> resource"); + let dst: ID3D11Resource = staging.cast().expect("staging -> resource"); + self.ctx + .CopySubresourceRegion(&dst, 0, 0, 0, 0, &src, slice, None); + let mut map = D3D11_MAPPED_SUBRESOURCE::default(); + self.ctx + .Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut map)) + .ok() + .expect("Map the readback staging texture"); + let pitch = map.RowPitch as usize; + let aligned_h = desc.Height as usize; + let total = pitch * (aligned_h + aligned_h.div_ceil(2)); + let mapped = std::slice::from_raw_parts(map.pData as *const u8, total); + // The chroma plane starts at the ALIGNED height, never the display + // height — the pool surface is taller than the picture. + let chroma_off = pitch * aligned_h; + let mut out = Vec::with_capacity(row_bytes * (height as usize).div_ceil(2) * 3); + for y in 0..height as usize { + out.extend_from_slice(&mapped[y * pitch..y * pitch + row_bytes]); + } + for y in 0..(height as usize).div_ceil(2) { + let row = chroma_off + y * pitch; + out.extend_from_slice(&mapped[row..row + row_bytes]); + } + self.ctx.Unmap(&staging, 0); + out + }; + out + } + } + + /// Decode `aus` through a real `NativeD3d11Decoder`, hash every picture, and + /// compare the planner's display order against libavcodec's goldens. + fn parity_run( + codec: Codec, + stream: StreamFormat, + aus: &[&[u8]], + order: &Order, + goldens: &[&str], + expected_aus: usize, + label: &str, + ) { + assert_eq!( + aus.len(), + expected_aus, + "{label}: the vector must split into {expected_aus} access units — a \ + different count means this file's splitter disagrees with pf-bitstream's, \ + and nothing below it is meaningful" + ); + assert_eq!( + order.display.len(), + goldens.len(), + "{label}: the planner outputs {} pictures, the goldens carry {}", + order.display.len(), + goldens.len() + ); + + let luid = pinned_adapter(); + let mut decoder = NativeD3d11Decoder::new(codec, stream, luid, false) + .unwrap_or_else(|e| panic!("{label}: the box must host this profile — {e:#}")); + let mut readback = Readback { + ctx: decoder.context.clone(), + staging: None, + }; + + let mut by_id: HashMap = HashMap::new(); + for (index, au) in aus.iter().enumerate() { + let sub = decoder + .plan(au) + .unwrap_or_else(|e| panic!("AU {index}: plan failed — {e:#}")) + .unwrap_or_else(|| panic!("AU {index}: this vector has no skipped pictures")); + assert!( + !sub.concealed, + "AU {index}: a clean vector must need no concealment" + ); + let display = (sub.facts.width, sub.facts.height); + let slice = u32::from(sub.setup_slot); + decoder + .submit(au, &sub) + .unwrap_or_else(|e| panic!("AU {index}: submit failed — {e:#}")); + let session = decoder.session.as_ref().expect("submit built a session"); + let pool = session.pool.clone(); + let bytes = readback.read(&decoder.device, &pool, slice, display); + by_id.insert(order.decode[index], sha256_hex(&bytes)); + } + + let mut mismatches = 0usize; + for (n, (id, golden)) in order.display.iter().zip(goldens.iter()).enumerate() { + let got = by_id + .get(id) + .unwrap_or_else(|| panic!("display frame {n} names PicId {id}, never decoded")); + if got != golden { + if mismatches < 10 { + eprintln!("{label}: display frame {n} (PicId {id}): {got} != {golden}"); + } + mismatches += 1; + } + } + assert_eq!( + mismatches, + 0, + "{label}: {mismatches}/{} frames diverge from libavcodec (first 10 above; \ + frame 0 is intra-only — if IT mismatches suspect the readback geometry \ + (pitch/crop/plane offset) rather than the decode)", + goldens.len() + ); + eprintln!( + "{label}: {} frames bit-identical to libavcodec software decode", + goldens.len() + ); + } + + /// The AV1 leg of [`parity_run`], which cannot be shared with it: one temporal + /// unit produces a `Vec` of plans, so a unit is not a picture. + /// + /// # It drives the PRODUCTION entry point + /// + /// [`NativeD3d11Decoder::decode_av1`] takes the whole unit — the same call the + /// stream makes — so this leg exercises the unit loop, [`frame_av1`] with its + /// slot-map bookkeeping, the `show` suppression, [`Session::held`] and the + /// hand-off blit. An earlier version of this harness called `plan_frame_av1` + + /// `decode_into` per frame instead, which decoded the same pixels while + /// exercising none of that: the hidden frames were withheld by the HARNESS, and + /// its `hidden` counter was a statement about its own `if !sub.show`. + /// + /// [`frame_av1`]: NativeD3d11Decoder::frame_av1 + /// + /// # What the hidden frames do to the harness + /// + /// Everything the unit decodes is hashed — 274 surfaces — and the comparison + /// walks the planner's 250-entry OUTPUT list. So the 24 hidden pictures are + /// decoded, read back, hashed, and then never looked up, which is exactly + /// right: a golden set of what libavcodec DELIVERS cannot contain them. It also + /// makes the `PicId` indirection load-bearing in a way the other two legs only + /// hint at — there, decode order and display order are permutations of one + /// list; here they are lists of different LENGTHS, and hashing in decode order + /// would not merely be out of order, it would be 24 hashes too long. + /// + /// Reaching a hidden frame's pixels through the production path means asking + /// the decoder where it put them: [`Order::per_unit`] says which ids a unit + /// decoded, the session's slot map says which surface holds each, and + /// [`Session::held`] says how large it is. Those last two are production state + /// — `show_existing_frame` reads exactly the same pair — so a rung that filled + /// them wrongly fails here rather than merely disappointing a later stream. + /// + /// The hidden frames are not unverified, either: every shown frame after one + /// predicts from it, so a hidden picture decoded wrong shows up as a wrong hash + /// on the frames that reference it. + /// + /// ⚠ Still unexercised, because the vendored vector has none: + /// `show_existing_frame`. + fn av1_parity_run(units: &[&[u8]], order: &Order, goldens: &[&str]) { + assert_eq!( + units.len(), + AV1_UNIT_COUNT, + "the IVF reader disagrees with the vector's temporal-unit count" + ); + assert_eq!(order.decode.len(), AV1_DECODED_COUNT); + assert_eq!(order.per_unit.len(), units.len()); + assert_eq!(order.display.len(), goldens.len()); + + let luid = pinned_adapter(); + let mut decoder = NativeD3d11Decoder::new(Codec::Av1, StreamFormat::SDR_420_8, luid, false) + .unwrap_or_else(|e| panic!("AV1: the box must host AV1 Profile 0 — {e:#}")); + let mut readback = Readback { + ctx: decoder.context.clone(), + staging: None, + }; + + let mut by_id: HashMap = HashMap::new(); + let mut decoded = 0usize; + let mut presented = 0usize; + for (index, unit) in units.iter().enumerate() { + // The production call, whole unit in: it plans, decodes every frame, + // and hands back the ONE picture the unit displays (or nothing). + let frame = decoder + .decode_av1(unit) + .unwrap_or_else(|e| panic!("unit {index}: decode failed — {e:#}")); + if frame.is_some() { + presented += 1; + } + + // Read back everything the unit decoded — the withheld pictures too, + // which is the whole reason this cannot hash `frame`. + for &id in &order.per_unit[index] { + let (slot, facts, pool) = { + let session = decoder + .session + .as_ref() + .expect("the first unit built a session"); + let slot = session.slots.slot_of(id).unwrap_or_else(|| { + panic!("unit {index}: picture {id} holds no surface after its own unit") + }); + let facts = session.held[usize::from(slot)].unwrap_or_else(|| { + panic!( + "unit {index}: surface {slot} holds picture {id} and no facts — \ + `show_existing_frame` would have nothing to blit" + ) + }); + (slot, facts, session.pool.clone()) + }; + let bytes = readback.read( + &decoder.device, + &pool, + u32::from(slot), + (facts.width, facts.height), + ); + by_id.insert(id, sha256_hex(&bytes)); + decoded += 1; + } + } + assert_eq!(decoded, AV1_DECODED_COUNT); + assert_eq!( + presented, AV1_SHOWN_COUNT, + "every unit of this vector shows exactly one frame, so the production \ + path must have handed back {AV1_SHOWN_COUNT} pictures" + ); + let hidden = AV1_DECODED_COUNT - presented; + assert_eq!( + hidden, + AV1_DECODED_COUNT - AV1_SHOWN_COUNT, + "the rung must have decoded 24 frames it never handed back — this counts \ + what `decode_av1` RETURNED against what it decoded, so at zero the \ + `!sub.show` suppression is not working (or this vector stopped hiding \ + frames, which `the_av1_vector_hides_frames…` would catch first)" + ); + + let mut mismatches = 0usize; + for (n, (id, golden)) in order.display.iter().zip(goldens.iter()).enumerate() { + let got = by_id + .get(id) + .unwrap_or_else(|| panic!("display frame {n} names PicId {id}, never decoded")); + if got != golden { + if mismatches < 10 { + eprintln!("AV1: display frame {n} (PicId {id}): {got} != {golden}"); + } + mismatches += 1; + } + } + assert_eq!( + mismatches, + 0, + "AV1: {mismatches}/{} frames diverge from libavcodec (first 10 above; frame \ + 0 is a key frame — if IT mismatches suspect the readback geometry \ + (pitch/crop/plane offset) or the tile records rather than the reference \ + handling)", + goldens.len() + ); + eprintln!( + "AV1: {} delivered frames bit-identical to libavcodec, {hidden} hidden frames \ + decoded and withheld", + goldens.len() + ); + } + + #[test] + #[ignore = "needs a Windows D3D11 video device (see module docs)"] + fn av1_every_delivered_frame_hashes_bit_identical_to_libavcodec() { + let units = split_ivf(TEST_25FPS_AV1); + let order = order_av1(&units); + av1_parity_run(&units, &order, &golden_hashes(GOLDENS_AV1)); + } + + #[test] + #[ignore = "needs a Windows D3D11 video device (see module docs)"] + fn h264_every_frame_hashes_bit_identical_to_libavcodec() { + let aus = split_h264_aus(TEST_25FPS_H264); + let order = order_h264(&aus); + parity_run( + Codec::H264, + StreamFormat::SDR_420_8, + &aus, + &order, + &golden_hashes(GOLDENS_H264), + FRAME_COUNT, + "H.264", + ); + } + + #[test] + #[ignore = "needs a Windows D3D11 video device (see module docs)"] + fn h265_every_frame_hashes_bit_identical_to_libavcodec() { + let aus = split_h265_aus(TEST_25FPS_H265); + let order = order_h265(&aus); + parity_run( + Codec::H265, + StreamFormat::SDR_420_8, + &aus, + &order, + &golden_hashes(GOLDENS_H265), + FRAME_COUNT, + "H.265", + ); + } + + /// The ten-bit path, which no golden set in this program covered until now. + /// + /// The HDR legs proved a Main10 session BUILDS and streams clean, which is a + /// weaker claim than it looks: D3D11VA exposes no per-picture status query, so a + /// Main10 stream decoding to garbage logs exactly as cleanly as one decoding + /// correctly. This is the leg that can tell them apart. + /// + /// It also exercises geometry the 8-bit legs cannot: P010 samples are two bytes, + /// so a row is `width * 2`, and HEVC's 128-line granule pads a 240-line picture + /// to a 256-line surface — the chroma plane therefore starts a long way from + /// where the display height would put it. + #[test] + #[ignore = "needs a Windows D3D11 video device (see module docs)"] + fn main10_every_frame_hashes_bit_identical_to_libavcodec() { + let aus = split_h265_aus(TEST_MAIN10_H265); + let order = order_h265(&aus); + parity_run( + Codec::H265, + StreamFormat { + chroma_format_idc: 1, + bit_depth: 10, + }, + &aus, + &order, + &golden_hashes(GOLDENS_MAIN10), + MAIN10_FRAME_COUNT, + "HEVC Main 10", + ); + } + + // --------------------------------------------------------------------- + // CPU guards — NOT `#[ignore]`d, so ordinary CI notices when this file's + // splitter or the goldens drift away from pf-bitstream. + // --------------------------------------------------------------------- + + #[test] + fn the_local_splitter_agrees_with_the_planner_on_both_vectors() { + let h264 = split_h264_aus(TEST_25FPS_H264); + assert_eq!(h264.len(), FRAME_COUNT, "H.264 vector access units"); + let order = order_h264(&h264); + assert_eq!(order.decode.len(), FRAME_COUNT); + assert_eq!( + order.display.len(), + golden_hashes(GOLDENS_H264).len(), + "the H.264 planner's output count must match the golden count" + ); + + let h265 = split_h265_aus(TEST_25FPS_H265); + assert_eq!(h265.len(), FRAME_COUNT, "H.265 vector access units"); + let order = order_h265(&h265); + assert_eq!(order.decode.len(), FRAME_COUNT); + assert_eq!( + order.display.len(), + golden_hashes(GOLDENS_H265).len(), + "the H.265 planner's output count must match the golden count" + ); + } + + #[test] + fn the_main10_vector_really_is_ten_bit() { + let aus = split_h265_aus(TEST_MAIN10_H265); + assert_eq!( + aus.len(), + MAIN10_FRAME_COUNT, + "the Main 10 vector is {MAIN10_FRAME_COUNT} access units" + ); + let order = order_h265(&aus); + assert_eq!( + order.display.len(), + golden_hashes(GOLDENS_MAIN10).len(), + "the planner's output count must match the Main 10 golden count" + ); + + // The point of the leg. A regenerated vector that came out 8-bit would make + // `main10_every_frame_hashes_bit_identical_to_libavcodec` a second run of the + // 8-bit path wearing a ten-bit name — and it would pass, because the goldens + // would have been regenerated alongside it. + let mut planner = H265Planner::new(); + let plan = planner + .plan_au(aus[0]) + .expect("the Main 10 vector's first access unit must plan"); + assert_eq!( + ( + plan.picture.chroma_format_idc, + plan.picture.bit_depth_luma_minus8, + plan.picture.bit_depth_chroma_minus8 + ), + (1, 2, 2), + "the Main 10 vector must be 4:2:0 at ten bits" + ); + assert_eq!( + (plan.picture.coded_width, plan.picture.coded_height), + (320, 240), + "the golden frame size is 320x240" + ); + } + + #[test] + fn the_ivf_reader_agrees_with_the_planner_and_the_av1_goldens() { + let units = split_ivf(TEST_25FPS_AV1); + assert_eq!(units.len(), AV1_UNIT_COUNT, "AV1 temporal units"); + let order = order_av1(&units); + assert_eq!( + order.decode.len(), + AV1_DECODED_COUNT, + "the AV1 vector decodes 274 frames" + ); + assert_eq!( + order.display.len(), + golden_hashes(GOLDENS_AV1).len(), + "the AV1 planner's output count must match the golden count" + ); + assert_eq!(order.display.len(), AV1_SHOWN_COUNT); + } + + #[test] + fn the_av1_vector_hides_frames_and_that_is_what_makes_this_leg_different() { + // The claim the AV1 leg's docs rest on, asserted rather than assumed: an + // access unit is a TEMPORAL UNIT, 24 of these carry two frames, and the + // extra one is never delivered. If a regenerated vector ever stopped doing + // that, `av1_parity_run` would still pass while proving nothing the H.264 + // leg does not already prove — and its `hidden` assertion is what would + // catch it on hardware. + let units = split_ivf(TEST_25FPS_AV1); + let mut planner = pf_dxvadec::Av1Planner::new(); + let (mut frames, mut multi_frame_units, mut shown) = (0usize, 0usize, 0usize); + for unit in &units { + let plans = planner.plan_au(unit).expect("the clean vector plans"); + if plans.len() > 1 { + multi_frame_units += 1; + } + for plan in &plans { + frames += 1; + if plan.picture.show_frame { + shown += 1; + } + assert!( + plan.dpb.stored.is_some(), + "this vector uses no show_existing_frame" + ); + } + } + assert_eq!(frames, AV1_DECODED_COUNT); + assert_eq!(shown, AV1_SHOWN_COUNT); + assert_eq!( + multi_frame_units, + AV1_DECODED_COUNT - AV1_SHOWN_COUNT, + "24 units must carry a hidden frame as well as the shown one" + ); + } + + #[test] + fn both_vendored_vectors_really_do_reorder() { + // The module docs claim the harness must reorder because these vectors do. If + // that ever stops being true the claim is stale, and hashing in decode order + // would be the simpler harness — so assert the reason, not just the behaviour. + for (name, order) in [ + ("H.264", order_h264(&split_h264_aus(TEST_25FPS_H264))), + ("H.265", order_h265(&split_h265_aus(TEST_25FPS_H265))), + ] { + assert_ne!( + order.decode, order.display, + "{name}: this vector no longer reorders — the harness's PicId \ + indirection is now unnecessary and its docs are wrong" + ); + } + } +} diff --git a/crates/pf-client-core/src/video_libav.rs b/crates/pf-client-core/src/video_libav.rs deleted file mode 100644 index 0426a774..00000000 --- a/crates/pf-client-core/src/video_libav.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Shared libav ownership helpers for the hardware decoders (`video_vaapi`, `video_vulkan`, -//! `video_d3d11`). -//! -//! The host has its own copy of this in `pf-encode`'s `enc/libav.rs`. The two crates do not depend -//! on each other — host encode and client decode share no code path — and the client's copy needs -//! something the host's does not ([`AvBuffer::into_raw`], for the decoder contexts that take -//! ownership of our ref), so a shared crate would have to carry an ffmpeg dependency and both sets -//! of semantics to save ~20 lines. It is not worth the edge. - -use ffmpeg_next::ffi; - -/// An owned `AVBufferRef`, unref'd exactly once when it drops. -/// -/// Each decoder constructor creates a hwdevice and then does several more fallible things with it -/// — find the codec, alloc a context, open it — and every one of those failure branches used to -/// unref the device by hand, alongside a `Drop` doing it once more. Miss a branch and a decoder -/// device leaks per failed negotiation; double it up and the process aborts. Ownership lives here -/// instead, so an early `bail!` releases whatever exists and the branches carry no cleanup. -pub(crate) struct AvBuffer(*mut ffi::AVBufferRef); - -impl AvBuffer { - /// Take ownership of a freshly-created `AVBufferRef`, rejecting the null an ffmpeg allocator - /// returns on failure. - /// - /// # Safety - /// `p` must be null, or a live `AVBufferRef` whose ownership passes to the returned value — - /// nothing else may unref it. - pub(crate) unsafe fn from_raw(p: *mut ffi::AVBufferRef) -> Option { - (!p.is_null()).then_some(AvBuffer(p)) - } - - /// The borrowed pointer, for calls that read the ref without taking it (e.g. `av_buffer_ref`, - /// which makes its own). - pub(crate) fn as_ptr(&self) -> *mut ffi::AVBufferRef { - self.0 - } - - /// Give up ownership: the caller becomes responsible for the unref. - /// - /// This exists for the `get_format` callbacks, which hand a frames context to the codec — - /// `(*ctx).hw_frames_ctx = fr` means *the codec owns our ref now*, and it unrefs it when the - /// context closes. Dropping an `AvBuffer` there as well would be the double-unref this type is - /// meant to prevent, so the transfer is made explicit rather than left implicit. - pub(crate) fn into_raw(self) -> *mut ffi::AVBufferRef { - let p = self.0; - std::mem::forget(self); - p - } -} - -impl Drop for AvBuffer { - fn drop(&mut self) { - // SAFETY: `self.0` is the non-null ref `from_raw` took ownership of, and this type is its - // sole owner (neither `Clone` nor `Copy`; `as_ptr` only lends, and `into_raw` forgets - // instead of dropping), so this runs exactly once for that reference. `av_buffer_unref` - // drops the one reference and nulls the pointer through the `&mut`. - unsafe { ffi::av_buffer_unref(&mut self.0) }; - } -} diff --git a/crates/pf-client-core/src/video_pyrowave.rs b/crates/pf-client-core/src/video_pyrowave.rs index ce241e0b..9d6264fa 100644 --- a/crates/pf-client-core/src/video_pyrowave.rs +++ b/crates/pf-client-core/src/video_pyrowave.rs @@ -1,7 +1,7 @@ //! PyroWave client decode (design/pyrowave-codec-plan.md §4.5) — the wired-LAN wavelet //! codec's decoder, running as plain Vulkan compute on the PRESENTER's own VkDevice (the -//! whole point: decode + CSC + present on one device, zero interop). Bypasses FFmpeg -//! entirely: the AU is one self-delimiting pyrowave packet; `push_packet` → ready → +//! whole point: decode + CSC + present on one device, zero interop): the AU is one +//! self-delimiting pyrowave packet; `push_packet` → ready → //! `decode_gpu_buffer` recorded into OUR command buffer, submitted on the shared graphics //! queue under the device's [`QueueLock`], fence-waited (sub-ms — Phase-0 measured //! 0.067 ms GPU at 1080p on the RTX 5070 Ti). @@ -445,7 +445,7 @@ pub struct PyroWaveDecoder { } // SAFETY: used only from the single decode thread; the shared-queue accesses go through -// QueueLock, matching the FFmpeg-Vulkan backend's threading contract. +// QueueLock, matching every other Vulkan backend's threading contract. unsafe impl Send for PyroWaveDecoder {} impl PyroWaveDecoder { @@ -515,7 +515,7 @@ impl PyroWaveDecoder { as *const pw::VkDeviceCreateInfo, queue_info: &mut queue_info, queue_info_count: 1, - // The presenter/Skia/FFmpeg all serialize on this same lock. + // The presenter, Skia and every decode lane serialize on this same lock. queue_lock_callback: Some(queue_lock_cb), queue_unlock_callback: Some(queue_unlock_cb), userdata: Arc::as_ptr(&queue_lock) as *mut c_void, diff --git a/crates/pf-client-core/src/video_software.rs b/crates/pf-client-core/src/video_software.rs index e57bbb4a..d3ea8319 100644 --- a/crates/pf-client-core/src/video_software.rs +++ b/crates/pf-client-core/src/video_software.rs @@ -1,224 +1,818 @@ -//! CPU/libavcodec software decode backend (swscale → RGBA). +//! The CPU rung — the ladder's LAST one, and (M8) the first one with no FFmpeg in it. +//! +//! * **H.264 → openh264** (BSD-2). Already a workspace dependency: the host's GPU-less +//! encoder is the same library ([`pf-encode`'s `enc/sw.rs`]), so the licence posture and +//! the statically-bundled build were settled before this rung existed. +//! * **AV1 → rav1d** (BSD-2) — dav1d, ported to Rust. Picking it over the `dav1d` FFI +//! crate is a packaging decision, argued in `Cargo.toml`; picking it over *nothing* is +//! the plan's ("dav1d SW is the safety net"). Two properties come free with it: +//! there is no `avcodec_find_decoder(AV1)` to hand us libdav1d behind a +//! `hw_device_ctx` it silently ignores, and no C decoder in the process at all. +//! * **HEVC → dropped.** No permissively licensed software HEVC decoder exists (libde265 +//! is LGPL, which defeats the point of the excision). This rung REFUSES an HEVC +//! session with a typed [`NoSoftwareRung`], which is what the session layer turns into +//! a reconnect that advertises HEVC-less decode caps — see +//! [`crate::video::last_rung_verdict`]. Narrowing instead (limping on at 5 fps, or +//! freezing) is the failure mode this whole program exists to end. +//! +//! **Output is PLANES, not RGBA.** The decoder hands the presenter tightly-packed I420 +//! and the presenter's existing planar CSC shader does the colour, which deletes two +//! things at once: swscale's per-frame YUV→RGBA pass, and swscale's BT.601 default — +//! the footgun the old `convert_rgba` carried ~30 lines of correction code for. +//! +//! **Colour comes from pf-bitstream, not from the decoder.** openh264 reports no VUI at +//! all and rav1d reports its own sequence header, so a rung that trusted its decoder +//! would have two colour implementations to keep in step with the four hardware rungs' +//! one. Instead the H.264 leg plans every AU with [`H264Planner`] — the SAME planner +//! `pf-vkdecode`/`pf-dxvadec`/`pf-vaadec` submit from — and reads +//! `plan.picture.colour`. The signalled matrix/range therefore cannot differ between the +//! software rung and the hardware rungs, because it is literally the same code reading +//! the same SPS. The H.264 leg takes the recovery point SEI from the same plan, through +//! `pf-vkdecode`'s own [`RecoveryWatch`], so an intra-refresh session re-anchors here on +//! the same rule the native rung uses. +//! +//! **The picture envelope is checked BEFORE the decoder sees the AU, on both legs.** +//! 8-bit 4:2:0 only: openh264 has no wider support at all and rav1d is compiled +//! `bitdepth_8` here. H.264 reads it off the SPS the planner activated; AV1 reads it off +//! the sequence header with `dav1d_parse_sequence_header`. Both raise the SAME typed +//! [`NoSoftwareRung`] so the session reconnects. Letting the DECODER answer instead is +//! what the M8 review caught: rav1d refuses a 10-bit frame with `ENOPROTOOPT`, the pump +//! reads a generic error as survivable, and a Main 10 HDR stream — which is what hardware +//! AV1 sessions are — freezes forever, one keyframe request per identical AU. +//! +//! Threading: openh264's `num_threads` is documented upstream as "will probably just +//! segfault", so this stays single-threaded — the old libavcodec rung's slice threading has +//! no equivalent here. rav1d gets the machine's cores: `max_frame_delay = 1` is the knob +//! that removes frame delay (dav1d's `get_num_threads` then computes `n_fc = min(1, n_tc)`, +//! so exactly one frame is ever in flight whatever `n_threads` says), and `n_threads` +//! drives the INTRA-frame tile/row workers, which cost no latency at all. Pinning it to 1 +//! bought nothing and gave the rung reached only because the GPU already failed a single +//! core to decode 4K with. -use crate::video::{averr, CpuFrame}; +use crate::video::{CpuPlanarFrame, RungLoss}; use crate::video_color::ColorDesc; -use anyhow::{anyhow, Context as _, Result}; -use ffmpeg::format::Pixel; -use ffmpeg::software::scaling; -use ffmpeg::util::frame::Video as AvFrame; -use ffmpeg_next as ffmpeg; -use std::ptr; +use anyhow::{anyhow, bail, Context as _, Result}; +use pf_bitstream::h264::{H264Planner, PlanError}; +use pf_vkdecode::RecoveryWatch; + +/// The codecs this rung can decode at all. Deliberately its own enum rather than an +/// `ffmpeg::codec::Id`: the whole point of M8 was that nothing in here speaks FFmpeg, and +/// the ladder above still does only because its other rungs are not swapped yet (M10). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SwCodec { + H264, + Av1, +} + +impl SwCodec { + /// The wire codec bit this rung can serve, or `None` — the one place the + /// "which codecs does software cover" question is answered. + pub(crate) fn for_wire(codec: u8) -> Option { + match codec { + punktfunk_core::quic::CODEC_H264 => Some(SwCodec::H264), + punktfunk_core::quic::CODEC_AV1 => Some(SwCodec::Av1), + _ => None, + } + } +} + +/// This build has no software decoder for the session's stream — the ladder has run out +/// of rungs. +/// +/// A distinct type, not a formatted string, because the SESSION layer must be able to +/// tell this apart from every other decode failure: everything else is survivable (feed +/// the next AU, ask for an IDR), and this one is not survivable at all — it can only be +/// answered by reconnecting with something this client can actually decode. It rides out +/// through `anyhow` and is recovered with `downcast_ref`, so no signature in the ladder +/// changes shape for it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NoSoftwareRung { + /// The `quic::CODEC_*` bit of the session that has no CPU rung. + pub codec: u8, + /// `None` — the CODEC itself has no CPU decoder (HEVC). `Some(what)` — the codec + /// does, but not for THIS stream's picture shape (10-bit, 4:4:4). + /// + /// The two are one type on purpose. They are different diagnoses but the SAME + /// available action: the codec is fixed at Welcome, so a shape this rung cannot + /// decode can only be escaped the way a codec it cannot decode is — a reconnect that + /// takes the codec off the table, after which the host resolves a new shape too. A + /// blunt instrument for the shape case, and the only one the wire offers. + /// + /// The shape case cannot be answered at construction alone: a Windows HDR desktop + /// flips to Main 10 IN-BAND with a new parameter set, so the Welcome's + /// [`crate::video::StreamFormat`] can say 8-bit for a session that becomes 10-bit + /// mid-stream. That is why this is raised from the per-AU path, off the bitstream's + /// own headers, rather than from a negotiated field. + pub shape: Option<&'static str>, +} + +impl NoSoftwareRung { + /// Which diagnosis this is, for the reconnect rule + /// ([`last_rung_verdict`](crate::video::last_rung_verdict)). The two answers differ: + /// a missing CODEC means every hardware rung already failed, a missing SHAPE means + /// none of them was even asked. + pub fn loss(&self) -> RungLoss { + match self.shape { + None => RungLoss::Codec, + Some(_) => RungLoss::Shape, + } + } +} + +impl std::fmt::Display for NoSoftwareRung { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let codec = crate::video::wire_codec_name(self.codec); + match self.shape { + None => write!( + f, + "no software decoder for {codec} — this build decodes H.264 and AV1 on \ + the CPU (there is no permissively licensed software HEVC decoder)" + ), + Some(shape) => write!( + f, + "the software {codec} decoder cannot decode this stream: {shape} \ + (the CPU rung is 8-bit 4:2:0 only)" + ), + } + } +} + +impl std::error::Error for NoSoftwareRung {} // --- software backend --------------------------------------------------------------- pub(crate) struct SoftwareDecoder { - decoder: ffmpeg::decoder::Video, - /// Rebuilt whenever the decoded format/size — or the colour signaling (a mid-stream - /// SDR↔HDR flip) — changes. - sws: Option<(scaling::Context, Pixel, u32, u32, ColorDesc)>, + inner: Inner, + /// Last colour signalling the stream actually stated. Held across AUs the metadata + /// parser could not read (see [`H264Software::colour_of`]) so a stream never silently + /// reverts to the SDR default mid-session; seeded with that default, which is what + /// "unspecified" resolves to anyway (`csc_rows`). + color: ColorDesc, +} + +enum Inner { + H264(H264Software), + Av1(Av1Software), } impl SoftwareDecoder { - pub(crate) fn new(codec_id: ffmpeg::codec::Id) -> Result { - let codec = ffmpeg::decoder::find(codec_id) - .ok_or_else(|| anyhow!("no {codec_id:?} decoder in libavcodec"))?; - let mut ctx = ffmpeg::codec::Context::new_with_codec(codec); - // SAFETY: `as_mut_ptr` yields the `AVCodecContext` behind the `ctx` allocated on the line - // above, which outlives these writes; each store is an in-bounds scalar field write on that - // live context, made before the decoder is opened and reads them. - unsafe { - let raw = ctx.as_mut_ptr(); - (*raw).flags |= ffmpeg::ffi::AV_CODEC_FLAG_LOW_DELAY as i32; - // Slice threading adds no frame delay (frame threading adds thread_count-1). - (*raw).thread_type = ffmpeg::ffi::FF_THREAD_SLICE; - (*raw).thread_count = 0; // auto - } - let decoder = ctx.decoder().video().context("open video decoder")?; - // Every construction site (session open, preference, mid-stream demotion) says - // which decoder actually opened: for AV1 the ID lookup means libdav1d here — - // deliberately (fastest CPU path; the native `av1` decoder has no software - // path at all) — and the name in the log is what keeps that distinguishable - // from the hardware lanes' capability-selected decoders. - tracing::info!(?codec_id, decoder = codec.name(), "software decoder opened"); - Ok(SoftwareDecoder { decoder, sws: None }) + /// Build the CPU rung for a WIRE codec bit. + /// + /// `Err` carrying a [`NoSoftwareRung`] means "there is no such rung", not "the rung + /// failed to start" — the two are different questions for the caller and must not + /// collapse into one string. + pub(crate) fn new(codec: u8) -> Result { + let Some(sw) = SwCodec::for_wire(codec) else { + return Err(NoSoftwareRung { codec, shape: None }.into()); + }; + let inner = match sw { + SwCodec::H264 => Inner::H264(H264Software::new()?), + SwCodec::Av1 => Inner::Av1(Av1Software::new()?), + }; + tracing::info!( + codec = crate::video::wire_codec_name(codec), + decoder = match sw { + SwCodec::H264 => "openh264", + SwCodec::Av1 => "rav1d", + }, + "software decoder opened (CPU, planar output)" + ); + Ok(SoftwareDecoder { + inner, + // "Unspecified" everywhere: `csc_rows` resolves that to BT.709 limited, the + // host's SDR default, which is also what E.2.1 inference produces for a + // stream whose VUI is silent. + color: ColorDesc { + primaries: 2, + transfer: 2, + matrix: 2, + full_range: false, + }, + }) } - pub(crate) fn decode(&mut self, au: &[u8]) -> Result> { - let packet = ffmpeg::Packet::copy(au); - self.decoder - .send_packet(&packet) - .map_err(|e| anyhow!("send_packet: {e}"))?; - let mut frame = AvFrame::empty(); - let mut out = None; - while self.decoder.receive_frame(&mut frame).is_ok() { - out = Some(self.convert_rgba(&frame)?); + pub(crate) fn decode(&mut self, au: &[u8]) -> Result> { + match &mut self.inner { + Inner::H264(h) => h.decode(au, &mut self.color), + Inner::Av1(a) => a.decode(au, &mut self.color), + } + } +} + +// --- H.264 (openh264) ---------------------------------------------------------------- + +struct H264Software { + decoder: openh264::decoder::Decoder, + /// The metadata half: colour signalling, the IDR flag and the display crop, from the + /// same planner every hardware rung submits from. It does NOT drive openh264 — + /// openh264 owns its own parsing — so a plan the narrow envelope refuses costs + /// metadata for that AU, never the picture. + /// + /// Boxed: the planner's DPB dwarfs everything else here, and `Backend` (which holds + /// this by value) is an enum whose other variants are pointer-sized — the same reason + /// the native rungs are boxed there. + planner: Box, + /// The recovery point SEI, folded per picture by the SAME rule the native Vulkan rung + /// uses (`pf-vkdecode`'s watch, unchanged and shared): an intra-refresh session never + /// emits an IDR, so without this the pump's post-loss freeze on THIS rung waits out + /// its 500 ms backstop and then forces the very IDR the wave exists to avoid. + recovery: RecoveryWatch, + /// One warn per session for a stream whose AUs will not plan: the picture is fine + /// (openh264 decodes it), but colour is then whatever the last plannable AU said, + /// and a support engineer must be able to see that from the log rather than infer it + /// from a hue. + plan_warned: bool, +} + +/// Everything the planner tells the software rung about the AU it is ABOUT to submit. +struct AuFacts { + is_idr: bool, + /// `None` = the AU did not plan; the caller keeps the last colour it saw. + color: Option, + recovery: punktfunk_core::reanchor::LocalRecovery, +} + +impl H264Software { + fn new() -> Result { + // Default config: error concealment OFF, logging quiet, one thread. Concealment + // is deliberately not enabled — this rung's contract is that its errors SURFACE + // (the pump turns an `Err` into a keyframe request through the same throttle as + // every other rung), and a decoder quietly inventing macroblocks is precisely + // the "looked clean, wasn't" shape M4's telemetry exists to make impossible. + let decoder = + openh264::decoder::Decoder::new().map_err(|e| anyhow!("openh264 decoder: {e}"))?; + Ok(H264Software { + decoder, + planner: Box::new(H264Planner::new()), + recovery: RecoveryWatch::new(), + plan_warned: false, + }) + } + + fn decode(&mut self, au: &[u8], color: &mut ColorDesc) -> Result> { + // Plan FIRST: the plan describes the AU we are about to decode, and reading it + // after would attribute this picture's colour to the next one on a decoder that + // buffers. + let facts = self.plan_facts(au)?; + if let Some(c) = facts.color { + *color = c; + } + let picture = self + .decoder + .decode(au) + .map_err(|e| anyhow!("openh264 decode: {e}"))?; + let Some(yuv) = picture else { + return Ok(None); + }; + use openh264::formats::YUVSource as _; + let (w, h) = yuv.dimensions(); + let (sy, su, sv) = yuv.strides(); + let frame = CpuPlanarFrame::from_i420( + w as u32, + h as u32, + [yuv.y(), yuv.u(), yuv.v()], + [sy, su, sv], + *color, + facts.is_idr, + facts.recovery, + )?; + Ok(Some(frame)) + } + + /// The IDR flag, the colour and the recovery mark for this AU, from the shared + /// planner. + /// + /// `colour` is `None` when the AU could not be planned — which is NORMAL for the + /// first AUs after a mid-session demotion onto this rung: the parameter sets arrive + /// in-band on the next IDR (which the demotion has already requested), so until it + /// lands the planner has no active SPS and says so. Answering `is_idr = false` there + /// is the conservative direction: it costs the re-anchor gate one frame of patience, + /// where a false `true` would lift a post-loss freeze onto a picture that is still + /// concealed — and the recovery mark is empty for the same reason. + /// + /// `Err` is reserved for the ONE thing that is not a metadata problem: a picture + /// shape this rung cannot decode. It travels as [`NoSoftwareRung`] so the session + /// reconnects instead of erroring per AU forever — see the type's `shape` field for + /// why that answer has to come from here rather than from the Welcome. + fn plan_facts(&mut self, au: &[u8]) -> Result { + match self.planner.plan_au(au) { + Ok(plan) => { + // The envelope, read from the SPS the planner activated for THIS picture + // — so an in-band flip to Main 10 (a Windows HDR desktop) is caught on + // the AU that carries it, not left to openh264 to fail on repeatedly. + if let Some(shape) = unsupported_shape( + plan.picture.chroma_format_idc, + plan.picture.bit_depth_luma_minus8, + ) { + return Err(NoSoftwareRung { + codec: punktfunk_core::quic::CODEC_H264, + shape: Some(shape), + } + .into()); + } + let c = plan.picture.colour; + // The wave's own verdict for this picture. Folded even when openh264 + // then produces nothing: the watch counts `frame_num` increments, so + // skipping a picture would leave the count owing forever. Losing the + // MARK of a picture that never came out only makes the lift late, which + // is the safe direction. + let mark = self.recovery.note_h264( + plan.picture.frame_num, + plan.picture.is_idr, + plan.picture.recovery_point, + ); + Ok(AuFacts { + is_idr: plan.picture.is_idr, + color: Some(ColorDesc { + primaries: c.colour_primaries, + transfer: c.transfer_characteristics, + matrix: c.matrix_coefficients, + full_range: c.video_full_range, + }), + recovery: punktfunk_core::reanchor::LocalRecovery { + sei_here: mark.sei_here, + is_recovery_point: mark.is_recovery_point, + }, + }) + } + Err(e) => { + // `NoActiveParamSet` before the first in-band IDR is expected and says + // nothing; anything else means the stream is outside the envelope the + // hardware rungs plan from, which is worth exactly one line. + if !matches!(e, PlanError::NoActiveParamSet { .. }) && !self.plan_warned { + self.plan_warned = true; + tracing::warn!( + error = %e, + "software rung: AU did not plan — colour signalling and the \ + keyframe flag now follow the last AU that did" + ); + } + Ok(AuFacts { + is_idr: false, + color: None, + recovery: punktfunk_core::reanchor::LocalRecovery::NONE, + }) + } + } + } +} + +/// The CPU rung's picture envelope: 8-bit 4:2:0 and nothing else. `Some(what)` names what +/// falls outside it, for [`NoSoftwareRung::shape`]. +/// +/// Stated once and shared by both legs. Neither decoder is BUILT for anything wider — +/// openh264 has no 4:2:2/4:4:4 or high-bit-depth support at all, and rav1d is compiled +/// here with `bitdepth_8` only — so this is a refusal that reflects the build, not a +/// policy that could drift from it. +fn unsupported_shape(chroma_format_idc: u8, bit_depth_minus8: u8) -> Option<&'static str> { + if bit_depth_minus8 != 0 { + return Some("10-bit or deeper"); + } + if chroma_format_idc != punktfunk_core::quic::CHROMA_IDC_420 { + return Some("chroma other than 4:2:0"); + } + None +} + +// --- AV1 (rav1d) ---------------------------------------------------------------------- + +// rav1d ships dav1d's C ABI as `#[no_mangle] extern "C"` Rust functions over `#[repr(C)]` +// types — there is no linker and no `.so` in sight, but the calling contract is still +// dav1d's, so the FFI discipline below is dav1d's too: every context/picture is owned by +// exactly one value here, and `Drop` closes it exactly once. +use rav1d::include::dav1d::data::Dav1dData; +use rav1d::include::dav1d::dav1d::{Dav1dContext, Dav1dSettings}; +use rav1d::include::dav1d::headers::{Dav1dSequenceHeader, DAV1D_PIXEL_LAYOUT_I420}; +use rav1d::include::dav1d::picture::Dav1dPicture; +use rav1d::src::lib::{ + dav1d_close, dav1d_data_create, dav1d_data_unref, dav1d_default_settings, dav1d_get_picture, + dav1d_open, dav1d_parse_sequence_header, dav1d_picture_unref, dav1d_send_data, +}; +use std::ptr::NonNull; + +struct Av1Software { + /// `None` only between `Drop` taking it and the close returning — every other + /// observer sees a live context. + ctx: Option, +} + +/// An owned `Dav1dData`, unref'd exactly once on drop. +/// +/// The same shape (and the same lesson) as the libavcodec rungs' `AvBuffer`: the send loop +/// below has several fallible exits between allocating the buffer and dav1d taking its +/// reference, and hand-unref'ing on each one is how a leak per failed AU gets written. +/// dav1d zeroes the struct when it takes the reference, so an already-consumed `Dav1dData` +/// drops to a no-op and the double-unref this type prevents cannot happen either. +struct Av1Data(Dav1dData); + +impl Av1Data { + fn create(au: &[u8]) -> Result { + let mut data = Dav1dData::default(); + // SAFETY: `data` is a live local; `dav1d_data_create` either writes an allocated + // buffer of `au.len()` bytes into it and returns its start, or returns null. + let buf = unsafe { dav1d_data_create(NonNull::new(&mut data as *mut Dav1dData), au.len()) }; + if buf.is_null() { + bail!("rav1d: could not allocate {} bytes for an AU", au.len()); + } + // SAFETY: `buf` is the start of the `au.len()`-byte allocation just returned, and + // `au` is a distinct live slice of exactly that length. + unsafe { std::ptr::copy_nonoverlapping(au.as_ptr(), buf, au.len()) }; + Ok(Av1Data(data)) + } +} + +impl Drop for Av1Data { + fn drop(&mut self) { + // SAFETY: `self.0` is a live `Dav1dData` this value solely owns (no `Clone`, and + // `Drop` runs once). `dav1d_data_unref` releases whatever reference is left — none + // when a successful `dav1d_send_data` already took it — and rewrites the struct. + unsafe { dav1d_data_unref(NonNull::new(&mut self.0)) }; + } +} + +// SAFETY: `Dav1dContext` is a refcounted handle dav1d documents as usable from one +// thread at a time; this type owns it exclusively (no `Clone`, no `Sync`) and it lives +// on the pump thread with the rest of the decoder — the same promise every other backend +// in this crate makes for its device handles. +unsafe impl Send for Av1Software {} + +impl Av1Software { + fn new() -> Result { + let mut settings = std::mem::MaybeUninit::::uninit(); + // SAFETY: `dav1d_default_settings` fully initializes the `Dav1dSettings` behind + // the pointer it is given; the storage is a live local that outlives the call. + let mut settings = unsafe { + dav1d_default_settings(NonNull::new_unchecked(settings.as_mut_ptr())); + settings.assume_init() + }; + // No frame delay: a punktfunk stream is zero-reorder and real-time, so the + // throughput a FRAME-threaded decoder buys costs exactly the latency this client + // spends the rest of its budget defending. `max_frame_delay = 1` is the knob that + // says so — dav1d's `get_num_threads` derives `n_fc = min(max_frame_delay, n_tc)`, + // so one frame stays in flight no matter how many threads exist. Same reasoning + // as the old libavcodec rung's `FF_THREAD_SLICE` + `AV_CODEC_FLAG_LOW_DELAY`, and + // `n_threads` is that rung's SLICE half: intra-frame tile/row workers, which add + // no delay. Capped at 8 — this is the rung reached because the GPU already + // failed, and it should not also take the machine over. + settings.max_frame_delay = 1; + settings.n_threads = std::thread::available_parallelism() + .map(|n| n.get().clamp(1, 8)) + .unwrap_or(1) as i32; + // Film grain synthesis is a post-process the hosts never signal and nobody can + // afford on the rung that exists because the GPU already failed. + settings.apply_grain = 0; + let mut ctx: Option = None; + // SAFETY: both pointers are live locals for the duration of the call, which is + // dav1d_open's whole contract: it reads `settings` and writes the context out. + let r = unsafe { + dav1d_open( + NonNull::new(&mut ctx as *mut Option), + NonNull::new(&mut settings as *mut Dav1dSettings), + ) + }; + if r.0 < 0 || ctx.is_none() { + bail!("rav1d (dav1d) decoder open failed: {}", r.0); + } + Ok(Av1Software { ctx }) + } + + fn decode(&mut self, au: &[u8], color: &mut ColorDesc) -> Result> { + let ctx = self.ctx.context("rav1d context closed")?; + if au.is_empty() { + return Ok(None); + } + // The envelope, off the BITSTREAM's own sequence header, before a byte reaches + // the decoder — exactly what the H.264 leg does with `plan_au`, and for the same + // reason. rav1d is compiled `bitdepth_8` only, so a 10-bit frame makes + // `rav1d_submit_frame` refuse with `ENOPROTOOPT`; that refusal is a per-AU error + // the pump would answer with a keyframe request forever, on a stream where every + // following AU is identically 10-bit. This is the shipping case, not a corner: + // AV1 is advertised only where hardware AV1 exists, hardware AV1 + HDR is Main 10, + // and a mid-session hardware failure demotes here. + if let Some(shape) = self.unsupported_sequence(au) { + return Err(NoSoftwareRung { + codec: punktfunk_core::quic::CODEC_AV1, + shape: Some(shape), + } + .into()); + } + // A `Dav1dData` that owns its own copy: `dav1d_data_create` allocates, we fill + // it, and `dav1d_send_data` takes the reference on success. Deliberately not + // `dav1d_data_wrap` over the caller's `au` — that would hand the decoder a + // borrow of a buffer the pump reuses on the next AU. + let mut data = Av1Data::create(au)?; + // dav1d consumes `data` incrementally: a partial send leaves bytes in it and asks + // to be re-sent. A punktfunk AU is one temporal unit and the decoder is drained + // every call, so the loop is bounded by the AU — but it is a LOOP, because + // `EAGAIN` here means "take pictures out first", not "the AU is bad". + // + // Only the NEWEST picture survives, which matches every other backend's + // `decode -> Option` contract (the old libav rung's `while receive_frame` + // did the same). It matters more here than elsewhere because an AV1 temporal unit + // really can carry several shown frames — but this is the rung reached after the + // hardware already failed, and showing the newest is the same answer the pump's + // newest-wins frame queue would give a moment later anyway. + let mut out: Option = None; + loop { + // SAFETY: `ctx` is the live context from `dav1d_open` (not yet closed) and + // `data.0` is a live local dav1d is allowed to read from and write to. Its + // reference is taken by dav1d on success; the guard's `Drop` releases only + // what is left. + let r = unsafe { dav1d_send_data(Some(ctx), NonNull::new(&mut data.0)) }; + let sent = r.0 >= 0; + if !sent && dav1d_errno(r) != Some(libc::EAGAIN) { + // A shape the build cannot decode reaches here only if it slipped past + // the sequence-header check above (an AU whose OBUs carry no sequence + // header of their own). Still typed rather than generic: the pump's + // survivable branch would ask for a keyframe and get the same refusal on + // every AU for the rest of the session — a permanent freeze with no + // fallback, which is the one outcome this rung exists to end. + if dav1d_errno(r) == Some(libc::ENOPROTOOPT) { + return Err(NoSoftwareRung { + codec: punktfunk_core::quic::CODEC_AV1, + shape: Some("10-bit or deeper"), + } + .into()); + } + bail!("rav1d send_data: {}", r.0); + } + match self.take_picture(ctx, color)? { + Some(f) => out = Some(f), + // Nothing more to take and the AU is fully consumed — done. + None if sent && data.0.sz == 0 => break, + // Nothing to take and the decoder still would not accept the rest: it + // has neither produced nor consumed, which is a wedge, not back-pressure. + None if !sent => bail!("rav1d: decoder accepted no data and produced no picture"), + None => {} + } } Ok(out) } - fn convert_rgba(&mut self, frame: &AvFrame) -> Result { - let (fmt, w, h) = (frame.format(), frame.width(), frame.height()); - // SAFETY: `frame.as_ptr()` is the decoder-owned live AVFrame for this call. - let color = unsafe { ColorDesc::from_raw(frame.as_ptr()) }; - let rebuild = !matches!(&self.sws, - Some((_, f, sw, sh, c)) if *f == fmt && *sw == w && *sh == h && *c == color); - if rebuild { - let mut ctx = - scaling::Context::get(fmt, w, h, Pixel::RGBA, w, h, scaling::Flags::POINT) - .context("swscale context")?; - // swscale defaults to BT.601 coefficients — set them from the FRAME's signaling - // (unspecified → BT.709 limited, the host's SDR default; a Windows HDR desktop - // streams BT.2020 in-band). Without this, YUV→RGB decodes with the wrong matrix - // and colours shift. Destination = full-range RGB; the transfer function stays - // baked in (the presenter tags PQ textures so GTK applies the EOTF). - const SWS_CS_ITU709: i32 = 1; - const SWS_CS_ITU601: i32 = 5; - const SWS_CS_BT2020: i32 = 9; - let cs = match color.matrix { - 9 | 10 => SWS_CS_BT2020, - 5 | 6 => SWS_CS_ITU601, - _ => SWS_CS_ITU709, - }; - // SAFETY: `sws_getCoefficients` returns a pointer into libav's own static coefficient - // tables — valid for the process, read-only — and `sws_setColorspaceDetails` takes it - // plus the live `SwsContext` behind `ctx` and plain scalars. - unsafe { - let coeffs = ffmpeg::ffi::sws_getCoefficients(cs); - ffmpeg::ffi::sws_setColorspaceDetails( - ctx.as_mut_ptr(), - coeffs, // inv_table: source (YUV) coefficients per the VUI - color.full_range as i32, // srcRange: 0 = limited/studio (MPEG) - coeffs, // table: destination coefficients (ignored for RGB output) - 1, // dstRange: 1 = full-range RGB - 0, - 1 << 16, - 1 << 16, // brightness, contrast, saturation (defaults) - ); - } - self.sws = Some((ctx, fmt, w, h, color)); - } - let (sws, ..) = self.sws.as_mut().unwrap(); - // Single-pass conversion: swscale writes straight into the Vec the texture will - // wrap. (The old path scaled into a scratch AVFrame and then copied `data(0)` out - // — a second full-frame pass per frame.) 64-byte row alignment keeps swscale on - // aligned SIMD stores; `GdkMemoryTexture` takes the resulting stride explicitly. - const ALIGN: i32 = 64; - use ffmpeg::ffi; - let dst_fmt = ffi::AVPixelFormat::AV_PIX_FMT_RGBA; - // SAFETY: pure size computation from format/dimensions; no pointers involved. - let size = unsafe { ffi::av_image_get_buffer_size(dst_fmt, w as i32, h as i32, ALIGN) }; - if size < 0 { - return Err(averr("av_image_get_buffer_size", size)); - } - let rgba = vec![0u8; size as usize]; - let mut dst_data: [*mut u8; 4] = [ptr::null_mut(); 4]; - let mut dst_linesize: [i32; 4] = [0; 4]; - // SAFETY: fill_arrays only derives plane pointers/strides into `rgba` (sized by - // av_image_get_buffer_size above, same format/align) — no allocation, no - // ownership transfer; `rgba` outlives the scale below. + /// This AU's sequence header against the build's envelope: `Some(what)` names what + /// falls outside it, `None` means "8-bit 4:2:0, or this AU carries no sequence header + /// of its own". + /// + /// An AU without one is the AV1 twin of the H.264 leg's `NoActiveParamSet`: it says + /// nothing, so it decodes against whatever sequence the decoder already holds — which + /// a previous AU was checked for. Punktfunk hosts re-send the sequence header on every + /// key frame, and the demotion onto this rung asks for one immediately, so the first + /// AU this rung ever decodes carries one. + fn unsupported_sequence(&self, au: &[u8]) -> Option<&'static str> { + let mut seq = std::mem::MaybeUninit::::uninit(); + // SAFETY: `out` is a live local this call either fully writes or leaves untouched + // (it writes only on success), and `au` is a live slice of exactly `au.len()` + // bytes. Nothing is allocated or referenced: dav1d fills the struct by value. let r = unsafe { - ffi::av_image_fill_arrays( - dst_data.as_mut_ptr(), - dst_linesize.as_mut_ptr(), - rgba.as_ptr(), - dst_fmt, - w as i32, - h as i32, - ALIGN, + dav1d_parse_sequence_header( + NonNull::new(seq.as_mut_ptr()), + NonNull::new(au.as_ptr().cast_mut()), + au.len(), ) }; - if r < 0 { - return Err(averr("av_image_fill_arrays", r)); + if r.0 < 0 { + return None; // no sequence header here (ENOENT), or an AU we cannot read } - // SAFETY: src pointers/strides belong to the decoder-owned `frame` (alive for the - // call); dst pointers were just filled over `rgba`, and sws_scale writes rows - // [0, h) only — exactly the buffer fill_arrays sized. - let r = unsafe { - ffi::sws_scale( - sws.as_mut_ptr(), - (*frame.as_ptr()).data.as_ptr() as *const *const u8, - (*frame.as_ptr()).linesize.as_ptr(), - 0, - h as i32, - dst_data.as_ptr(), - dst_linesize.as_ptr(), - ) - }; - if r < 0 { - return Err(averr("sws_scale", r)); + // SAFETY: the call returned success, which is its contract for having written + // the whole `Dav1dSequenceHeader`. + let seq = unsafe { seq.assume_init() }; + if seq.hbd != 0 { + return Some("10-bit or deeper"); } - Ok(CpuFrame { - width: w, - height: h, - stride: dst_linesize[0] as usize, - rgba, - color, - // `is_key()` reads the same intra flag `frame_is_keyframe` derives from pict_type - // for the hardware paths; ffmpeg-next handles the FFmpeg-version binding split. - keyframe: frame.is_key(), - }) + if seq.layout != DAV1D_PIXEL_LAYOUT_I420 { + return Some("chroma other than 4:2:0"); + } + None } + + /// One picture out of the decoder, converted. `None` = nothing ready yet. + fn take_picture( + &self, + ctx: Dav1dContext, + color: &mut ColorDesc, + ) -> Result> { + let mut pic = Dav1dPicture::default(); + // SAFETY: `ctx` is live and `pic` is a live local dav1d writes the picture into. + let r = + unsafe { dav1d_get_picture(Some(ctx), NonNull::new(&mut pic as *mut Dav1dPicture)) }; + if dav1d_errno(r) == Some(libc::EAGAIN) { + return Ok(None); + } + if r.0 < 0 { + bail!("rav1d get_picture: {}", r.0); + } + // From here the picture is OURS and must be unref'd on every exit — including the + // refusals below, which is why the conversion is a closure and the unref is not + // in a branch. + let converted = Self::convert(&pic, color); + // SAFETY: `pic` is the live picture `dav1d_get_picture` just wrote; this releases + // exactly the one reference it handed over, once. + unsafe { dav1d_picture_unref(NonNull::new(&mut pic as *mut Dav1dPicture)) }; + converted.map(Some) + } + + fn convert(pic: &Dav1dPicture, color: &mut ColorDesc) -> Result { + // 8-bit 4:2:0 only, stated as a refusal rather than assumed — and as the SAME + // typed refusal the H.264 leg raises, so a shape this rung cannot decode + // reconnects instead of erroring once per AU for the rest of the session. + // Treating a 4:4:4 picture's planes as 4:2:0 would decode correctly and display + // wrong, which is the class this program exists to refuse. + // + // A BELT, not the gate: `unsupported_sequence` refuses these shapes before the + // AU is submitted, and a `bitdepth_8`-only build cannot produce a 10-bit picture + // anyway (`rav1d_submit_frame` refuses the frame setup). Kept because it costs + // two comparisons and because the day this build gains `bitdepth_16` the layout + // half stops being redundant. + let shape = if pic.p.bpc != 8 { + Some("10-bit or deeper") + } else if pic.p.layout != DAV1D_PIXEL_LAYOUT_I420 { + Some("chroma other than 4:2:0") + } else { + None + }; + if let Some(shape) = shape { + return Err(NoSoftwareRung { + codec: punktfunk_core::quic::CODEC_AV1, + shape: Some(shape), + } + .into()); + } + let (w, h) = (pic.p.w.max(0) as u32, pic.p.h.max(0) as u32); + // Colour rides the SEQUENCE header, which AV1 re-sends whenever it changes — the + // same per-picture contract the H.264 leg gets from the SPS, so an in-band + // SDR↔HDR flip is followed rather than latched. + if let Some(seq) = pic.seq_hdr { + // SAFETY: `seq_hdr` belongs to the picture we hold a reference to, so it is + // live for this call; these are plain scalar field reads. + let seq = unsafe { seq.as_ref() }; + *color = ColorDesc { + primaries: seq.pri as u8, + transfer: seq.trc as u8, + matrix: seq.mtrx as u8, + full_range: seq.color_range != 0, + }; + } + let keyframe = pic.frame_hdr.is_some_and(|f| { + // SAFETY: same as `seq_hdr` above — owned by the live picture, scalar read. + unsafe { f.as_ref() }.frame_type == rav1d::include::dav1d::headers::DAV1D_FRAME_TYPE_KEY + }); + let (_, ch) = CpuPlanarFrame::chroma_dims(w, h); + // dav1d gives ONE chroma stride for both planes (`stride[1]`), which is why the + // triple below repeats it rather than looking for a third. + let strides = [ + pic.stride[0].max(0) as usize, + pic.stride[1].max(0) as usize, + pic.stride[1].max(0) as usize, + ]; + let sizes = [ + h as usize * strides[0], + ch as usize * strides[1], + ch as usize * strides[2], + ]; + let mut planes: [&[u8]; 3] = [&[], &[], &[]]; + for i in 0..3 { + let p = pic.data[i].with_context(|| format!("rav1d: plane {i} is null"))?; + // SAFETY: dav1d's picture contract is that plane `i` spans + // `height_of_plane * |stride|` bytes from `data[i]`, and the picture holds a + // reference to that allocation for as long as we do (unref'd by the caller, + // after this conversion returns). Negative strides (bottom-up pictures) are + // rejected above by the `max(0)` collapsing them to a zero size, which the + // copy below then refuses. + planes[i] = unsafe { std::slice::from_raw_parts(p.as_ptr().cast::(), sizes[i]) }; + } + // No local-recovery answer from this leg: AV1 has no recovery point SEI, and its + // intra-refresh equivalent (`frame_refs_short_signaling` / S-frames) is not + // something a punktfunk host emits — so the pump's re-anchor behaviour on AV1 is + // exactly the wire's, as it is on every lane but H.264/H.265. + CpuPlanarFrame::from_i420( + w, + h, + planes, + strides, + *color, + keyframe, + punktfunk_core::reanchor::LocalRecovery::NONE, + ) + } +} + +impl Drop for Av1Software { + fn drop(&mut self) { + if self.ctx.is_none() { + return; + } + // SAFETY: `self.ctx` is the one context `dav1d_open` produced and this is its + // sole owner, so this runs exactly once; `dav1d_close` takes it through the + // `&mut` and leaves `None`. + unsafe { dav1d_close(NonNull::new(&mut self.ctx as *mut Option)) }; + } +} + +/// The errno behind a `Dav1dResult`, or `None` for success. +/// +/// rav1d returns the NEGATED errno as a plain `c_int`, and the codes are `libc`'s own +/// (`Rav1dError::ENOPROTOOPT = libc::ENOPROTOOPT as u8`) — so they are matched against +/// `libc`'s rather than written out. Not a nicety: `EAGAIN` is 11 everywhere but +/// `ENOPROTOOPT` is **92 on Linux and 123 on Windows**, and a literal would therefore be +/// right on exactly one platform. The typed enum this would rather match on +/// (`Rav1dError`) lives in a `pub(crate)` module — rav1d re-exports only `Dav1dResult` — +/// so the errno is the only handle the crate actually offers. +fn dav1d_errno(r: rav1d::Dav1dResult) -> Option { + (r.0 < 0).then_some(-r.0) } #[cfg(test)] mod tests { use super::*; + use crate::video::csc_rows; - /// The wire → `ColorDesc` plumbing: an HDR10 stream's VUI (BT.2020 primaries, PQ - /// transfer, BT.2020-NCL matrix, limited range) must arrive on the decoded frame — - /// this is what the Windows host emits in-band for an HDR desktop, and mis-rendering - /// it as BT.709 is the washed-out-colors bug. Fixture: one 64×64 Main10 IDR - /// (`tests/pq-frame.h265`, x265 with explicit VUI). - #[test] - fn software_decode_carries_pq_signaling() { - let au = include_bytes!("../tests/pq-frame.h265"); - let mut dec = SoftwareDecoder::new(ffmpeg::codec::Id::HEVC).expect("hevc decoder"); - let mut got = dec.decode(au).expect("decode"); - if got.is_none() { - // Low-delay decoders may still hold the frame until a flush — send EOF. - dec.decoder.send_eof().ok(); - let mut frame = AvFrame::empty(); - if dec.decoder.receive_frame(&mut frame).is_ok() { - got = Some(dec.convert_rgba(&frame).expect("convert")); - } - } - let f = got.expect("no frame decoded from the PQ fixture"); - assert_eq!( - f.color, - ColorDesc { - primaries: 9, - transfer: 16, - matrix: 9, - full_range: false - } - ); - assert!(f.color.is_pq()); - assert_eq!((f.width, f.height), (64, 64)); + /// The nine bars every fixture encodes, in x order: eight fully-saturated + /// primaries/secondaries plus black and white, and then the one that carries the + /// RANGE axis. + /// + /// ⚠ `(192, 128, 64)` is not decoration. On saturated bars a limited↔full mismatch + /// only pushes values outside [0, 1], where the shader clamps — so the 709-FULL + /// fixture decoded with the WRONG range comes back with max error **0** over the + /// eight, and this test could not fail on range at all. Measured on these fixtures: + /// the mid-tone gives max error 11 under the wrong range. A 50% grey does not do the + /// job either (3, inside the ±4 tolerance) — it has to be OFF-neutral, so the chroma + /// scale is exercised and not just the luma one. + const BARS: [(u8, u8, u8); 9] = [ + (255, 255, 255), + (255, 255, 0), + (0, 255, 255), + (0, 255, 0), + (255, 0, 255), + (255, 0, 0), + (0, 0, 255), + (0, 0, 0), + (192, 128, 64), + ]; + + /// The presenter's planar CSC shader, on the CPU: sample the three planes and apply + /// `csc_rows` exactly as `planar_csc.frag` does (`rgb[i] = dot(r[i].xyz, yuv) + + /// r[i].w`, then clamp). 8-bit, no MSB packing — the software rung's only shape. + /// + /// This is a MODEL of the shader, and it is the honest one: `csc_rows` is the single + /// coefficient implementation the shader's push constants are filled from (and the + /// Windows client's constant buffer, and the Apple client's Swift port), so what this + /// exercises end to end is exactly what changes colour on screen — the decoder's + /// plane layout, and the `ColorDesc` it read out of the bitstream. Sampling is + /// nearest at bar centres, which is where the shader's quarter-texel 4:2:0 siting + /// correction and its linear filter both make no difference. + fn shader_rgb(f: &CpuPlanarFrame, x: u32, y: u32) -> [u8; 3] { + let rows = csc_rows(f.color, 8, false); + let (cw, _) = CpuPlanarFrame::chroma_dims(f.width, f.height); + let luma = f.plane(0)[(y * f.width + x) as usize]; + let (cx, cy) = (x / 2, y / 2); + let cb = f.plane(1)[(cy * cw + cx) as usize]; + let cr = f.plane(2)[(cy * cw + cx) as usize]; + let yuv = [luma as f32 / 255.0, cb as f32 / 255.0, cr as f32 / 255.0]; + core::array::from_fn(|i| { + let v = rows[i][0] * yuv[0] + rows[i][1] * yuv[1] + rows[i][2] * yuv[2] + rows[i][3]; + (v.clamp(0.0, 1.0) * 255.0).round() as u8 + }) } - /// Golden colour fixtures: one 256×64 LOSSLESS x265 IDR of 8 fully-saturated colour bars per - /// signaling variant (generated offline with ffmpeg/libx265; the RGB→YUV conversion matched - /// to the VUI each fixture declares, so the original RGB is recoverable ±1 code). Decoding - /// through the real CPU path (`SoftwareDecoder` → per-frame `ColorDesc` → swscale with the - /// signaled matrix/range) must reproduce the bars — the end-to-end guard for the - /// signaling-driven CSC across BT.601/709 × limited/full. A hardcoded-709 regression fails - /// the 601 fixture by tens of code points; a range mix-up fails the full-range one. + fn decode_one(codec: u8, au: &[u8]) -> CpuPlanarFrame { + let mut dec = SoftwareDecoder::new(codec).expect("software decoder"); + dec.decode(au) + .expect("decode") + .expect("no frame out of the fixture") + } + + /// **M8's exit criterion.** Three lossless-ish H.264 colour-bar fixtures whose VUIs + /// differ ONLY in matrix and range (see `tests/gen-bars.sh` for the recipe): decode + /// each through the real CPU rung, then convert with the real `csc_rows`, and require + /// the original RGB back. + /// + /// What it would have caught, one failure per axis: + /// + /// * **The BT.601 default** — the bug the deleted `convert_rgba` carried explicit + /// correction code for. swscale converts with BT.601 coefficients unless told + /// otherwise, so a rung that dropped the signalling (or hardcoded one matrix) + /// renders the 601 fixture with 709 coefficients or vice versa. On the saturated + /// bars that is tens of code points — e.g. pure red's green channel goes from 0 to + /// ~+40 — far outside the ±4 tolerance (measured on these fixtures: max error 22 + /// for 709 read as 601, 39 the other way). + /// * **Range** — the 709-full fixture differs from 709-limited by the 16..235 vs + /// 0..255 expansion only. ⚠ On the eight saturated bars this axis CANNOT fail: + /// every one of them is at an extreme, so a mismatch only pushes values outside + /// [0, 1] where the shader clamps, and the fixture decodes with max error **0** + /// under the wrong range. The ninth bar, `(192, 128, 64)`, is what makes the axis + /// testable (max error 11 wrong-range, ~1 right) — see [`BARS`]. + /// * **Plane order and stride** — a Cb/Cr swap turns red into blue, and a stride + /// mistake shears the bars sideways, so both show up as a wrong bar rather than a + /// wrong shade. + /// + /// It is deliberately NOT a "did it decode" test: every assertion is a pixel value + /// that depends on the colour signalling surviving the whole path. #[test] - fn software_decode_reproduces_golden_bars() { - const BARS: [(u8, u8, u8); 8] = [ - (255, 255, 255), - (255, 255, 0), - (0, 255, 255), - (0, 255, 0), - (255, 0, 255), - (255, 0, 0), - (0, 0, 255), - (0, 0, 0), - ]; + fn software_h264_reproduces_the_golden_bars_in_both_ranges() { let fixtures: [(&str, &[u8], ColorDesc); 3] = [ ( "601-limited", - include_bytes!("../tests/bars-601-limited.h265"), + include_bytes!("../tests/bars-601-limited.h264"), ColorDesc { primaries: 1, transfer: 1, @@ -228,7 +822,7 @@ mod tests { ), ( "709-limited", - include_bytes!("../tests/bars-709-limited.h265"), + include_bytes!("../tests/bars-709-limited.h264"), ColorDesc { primaries: 1, transfer: 1, @@ -238,39 +832,264 @@ mod tests { ), ( "709-full", - include_bytes!("../tests/bars-709-full.h265"), + include_bytes!("../tests/bars-709-full.h264"), ColorDesc { primaries: 1, transfer: 1, matrix: 1, - full_range: true, // the PUNKTFUNK_444_FULLRANGE experiment's signaling + full_range: true, }, ), ]; for (name, au, want_color) in fixtures { - let mut dec = SoftwareDecoder::new(ffmpeg::codec::Id::HEVC).expect("hevc decoder"); - let mut got = dec.decode(au).expect("decode"); - if got.is_none() { - dec.decoder.send_eof().ok(); - let mut frame = AvFrame::empty(); - if dec.decoder.receive_frame(&mut frame).is_ok() { - got = Some(dec.convert_rgba(&frame).expect("convert")); - } - } - let f = got.unwrap_or_else(|| panic!("{name}: no frame decoded")); - assert_eq!(f.color, want_color, "{name}: signaling"); - assert_eq!((f.width, f.height), (256, 64), "{name}: dims"); + let f = decode_one(punktfunk_core::quic::CODEC_H264, au); + assert_eq!(f.color, want_color, "{name}: signalling"); + assert_eq!((f.width, f.height), (288, 64), "{name}: dims"); + assert!(f.keyframe, "{name}: the fixture is a single IDR"); for (i, (r, g, b)) in BARS.iter().enumerate() { - let (cx, cy) = (i * 32 + 16, 32usize); - let o = cy * f.stride + cx * 4; - let px = &f.rgba[o..o + 3]; + let px = shader_rgb(&f, i as u32 * 32 + 16, 32); for (got, want) in px.iter().zip([r, g, b]) { assert!( - got.abs_diff(*want) <= 3, + got.abs_diff(*want) <= 4, "{name} bar {i}: got {px:?}, want ({r},{g},{b})" ); } } } } + + /// The same three fixtures, but asserting the thing a "it decoded" test cannot: the + /// 601 and 709 pictures are DIFFERENT pixels, so a rung that ignored the signalling + /// and converted both with one matrix would still pass a self-consistency check. + /// + /// Guards the fixtures themselves as much as the code — if a regeneration ever + /// produced two identical bitstreams the colour test above would go vacuously green. + #[test] + fn the_601_and_709_fixtures_really_do_carry_different_luma() { + let f601 = decode_one( + punktfunk_core::quic::CODEC_H264, + include_bytes!("../tests/bars-601-limited.h264"), + ); + let f709 = decode_one( + punktfunk_core::quic::CODEC_H264, + include_bytes!("../tests/bars-709-limited.h264"), + ); + // Pure red: Y = 0.299·255 ≈ 76 under 601, 0.2126·255 ≈ 54 under 709 (both then + // range-compressed to 16..235). Same displayed colour, different code points. + let (x, y) = (5 * 32 + 16, 32); + let a = f601.plane(0)[(y * f601.width + x) as usize]; + let b = f709.plane(0)[(y * f709.width + x) as usize]; + assert!( + a.abs_diff(b) > 10, + "601 luma {a} vs 709 luma {b} — the fixtures do not differ, so the colour \ + test above proves nothing" + ); + // ...and after the CSC both land on the same red. + for (f, name) in [(&f601, "601"), (&f709, "709")] { + let px = shader_rgb(f, x, y); + assert!( + px[0].abs_diff(255) <= 4 && px[1] <= 4 && px[2] <= 4, + "{name}: red bar came out {px:?}" + ); + } + } + + /// HEVC has no CPU rung and must say so with the TYPE the session layer keys its + /// reconnect off — not with a string, and not by quietly producing nothing. + #[test] + fn hevc_is_refused_with_the_typed_no_rung_error() { + let err = SoftwareDecoder::new(punktfunk_core::quic::CODEC_HEVC) + .err() + .expect("HEVC must not build a software decoder"); + let typed = err + .downcast_ref::() + .expect("the refusal must survive as NoSoftwareRung through anyhow"); + assert_eq!(typed.codec, punktfunk_core::quic::CODEC_HEVC); + assert_eq!(typed.shape, None, "the CODEC is missing, not a shape"); + assert!(err.to_string().contains("HEVC"), "{err}"); + // And the two codecs that DO have one still build. + assert!(SoftwareDecoder::new(punktfunk_core::quic::CODEC_H264).is_ok()); + assert!(SoftwareDecoder::new(punktfunk_core::quic::CODEC_AV1).is_ok()); + } + + /// A picture shape the CPU rung cannot decode must raise the SAME typed refusal as a + /// missing codec, because it has the same available answer (reconnect) and because + /// the alternative — an `Err` per AU forever, or 8-bit maths over 10-bit samples — is + /// respectively a frozen screen and a wrong one. + /// + /// Exercised as the pure rule plus the two shapes a punktfunk host can actually + /// resolve: Main 10 (an HDR desktop, flipped IN-BAND, which is why the check reads + /// the ACTIVE SPS and not the Welcome) and 4:4:4 (the "Full chroma" opt-in). + #[test] + fn a_shape_the_cpu_rung_cannot_decode_is_the_same_typed_refusal() { + use punktfunk_core::quic::{CHROMA_IDC_420, CHROMA_IDC_444}; + // 8-bit 4:2:0 is the whole envelope. + assert_eq!(unsupported_shape(CHROMA_IDC_420, 0), None); + assert_eq!( + unsupported_shape(CHROMA_IDC_420, 2), + Some("10-bit or deeper") + ); + assert_eq!( + unsupported_shape(CHROMA_IDC_444, 0), + Some("chroma other than 4:2:0") + ); + // Depth is reported FIRST when both are wrong: it is the one that silently + // mis-scales rather than merely mis-siting, so it is the more useful diagnosis. + assert_eq!( + unsupported_shape(CHROMA_IDC_444, 2), + Some("10-bit or deeper") + ); + // And the refusal reaches a caller as the type the session keys its reconnect + // off, with a message that says which stream, not just "decode failed". + let e: anyhow::Error = NoSoftwareRung { + codec: punktfunk_core::quic::CODEC_AV1, + shape: Some("10-bit or deeper"), + } + .into(); + let typed = e.downcast_ref::().expect("typed"); + assert_eq!(typed.shape, Some("10-bit or deeper")); + assert!(e.to_string().contains("AV1"), "{e}"); + assert!(e.to_string().contains("8-bit 4:2:0 only"), "{e}"); + } + + /// The 709-full fixture must be ABLE to fail on the range axis. It could not before + /// the M8 review — every bar was saturated, so a limited↔full mismatch only pushed + /// values past the shader's clamp and the decode came back byte-perfect with the + /// WRONG range honoured. + /// + /// Guards the fixture, not the code: if `BARS` ever loses its mid-tone (or a + /// regeneration drops the ninth bar), the range half of the test above goes vacuous + /// and this is what says so. + #[test] + fn the_full_range_fixture_is_decoded_wrong_by_the_wrong_range() { + let f = decode_one( + punktfunk_core::quic::CODEC_H264, + include_bytes!("../tests/bars-709-full.h264"), + ); + let wrong = ColorDesc { + full_range: false, + ..f.color + }; + let rows = csc_rows(wrong, 8, false); + let (cw, _) = CpuPlanarFrame::chroma_dims(f.width, f.height); + let mut worst = 0u8; + for (i, (r, g, b)) in BARS.iter().enumerate() { + let (x, y) = (i as u32 * 32 + 16, 32u32); + let luma = f.plane(0)[(y * f.width + x) as usize]; + let cb = f.plane(1)[((y / 2) * cw + x / 2) as usize]; + let cr = f.plane(2)[((y / 2) * cw + x / 2) as usize]; + let yuv = [luma as f32 / 255.0, cb as f32 / 255.0, cr as f32 / 255.0]; + let px: [u8; 3] = core::array::from_fn(|c| { + let v = + rows[c][0] * yuv[0] + rows[c][1] * yuv[1] + rows[c][2] * yuv[2] + rows[c][3]; + (v.clamp(0.0, 1.0) * 255.0).round() as u8 + }); + for (got, want) in px.iter().zip([r, g, b]) { + worst = worst.max(got.abs_diff(*want)); + } + } + assert!( + worst > 4, + "decoding the FULL-range fixture as LIMITED was off by only {worst}, inside \ + the ±4 tolerance — the fixture no longer tests the range axis at all" + ); + } + + /// **B1.** A 10-bit AV1 stream must be REFUSED with the typed error, not errored on + /// per AU forever. + /// + /// This is the shipping case, not a corner: AV1 is advertised only where hardware AV1 + /// exists, hardware AV1 + HDR is Main 10, and a mid-session hardware failure demotes + /// onto this rung. rav1d is built `bitdepth_8`, so its frame setup refuses with + /// `ENOPROTOOPT`; before the review that surfaced as a generic `anyhow` the pump read + /// as survivable — keyframe requested, next AU identically 10-bit, screen frozen for + /// the rest of the session with no fallback. + /// + /// The fixture is a whole 10-bit AV1 temporal unit (SVT-AV1, 64x64 red, one key + /// frame) inline rather than on disk: 38 bytes, and what is being tested is the + /// SEQUENCE HEADER inside it. + #[test] + fn a_10bit_av1_stream_is_refused_with_the_typed_no_rung_error() { + const TU_10BIT: [u8; 38] = [ + 0x12, 0x00, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x02, 0xaf, 0xff, 0x8d, 0x5f, 0x38, 0x08, + 0x32, 0x16, 0x10, 0x00, 0xba, 0x02, 0x0b, 0x2c, 0x51, 0x41, 0x00, 0x00, 0x08, 0x00, + 0x95, 0xd1, 0xe2, 0x7e, 0xac, 0x4f, 0x04, 0xad, 0xa4, 0x70, + ]; + let mut dec = SoftwareDecoder::new(punktfunk_core::quic::CODEC_AV1).expect("av1 decoder"); + let err = dec + .decode(&TU_10BIT) + .err() + .expect("a 10-bit AV1 AU must not decode on an 8-bit-only build"); + let typed = err.downcast_ref::().expect( + "the refusal must survive as NoSoftwareRung through anyhow — a generic \ + error here is the permanent freeze this test exists to prevent", + ); + assert_eq!(typed.codec, punktfunk_core::quic::CODEC_AV1); + assert_eq!(typed.shape, Some("10-bit or deeper")); + // ...and the session layer's rule reads it as a SHAPE loss, so the retry is not + // narrowed to codecs with a CPU rung. + assert_eq!(typed.loss(), crate::video::RungLoss::Shape); + // Every following AU raises the same refusal rather than the decoder wedging: + // the pump breaks out on the first one, but a stuck loop here is the failure + // mode, so prove it stays a refusal. + assert!(dec + .decode(&TU_10BIT) + .err() + .and_then(|e| e.downcast_ref::().copied()) + .is_some()); + } + + /// The AV1 leg decodes a real stream and reports the sequence header's own colour. + /// Fixture: the vendored cros-codecs AV1 vector (IVF), whose first temporal unit is a + /// key frame — enough to prove the rav1d FFI (open → send → get → unref → close), + /// the I420 plane copy and the colour read, none of which the H.264 leg exercises. + #[test] + fn software_av1_decodes_and_reports_its_sequence_colour() { + const IVF: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1" + ); + // IVF: 32-byte file header, then per-frame [u32 size][u64 pts][payload]. + let mut off = 32usize; + let mut dec = SoftwareDecoder::new(punktfunk_core::quic::CODEC_AV1).expect("av1 decoder"); + let mut first = None; + let (mut units, mut frames) = (0u32, 0u32); + // Drive the WHOLE vector, not just the first unit: the send loop runs once per + // temporal unit and its `Dav1dData` guard drops on every path through it, so a + // leak or a wedge shows up as the run failing rather than as a slow drift nobody + // reproduces. + while off + 12 <= IVF.len() { + let sz = u32::from_le_bytes(IVF[off..off + 4].try_into().unwrap()) as usize; + off += 12; + if off + sz > IVF.len() { + break; + } + units += 1; + if let Some(f) = dec.decode(&IVF[off..off + sz]).expect("av1 decode") { + frames += 1; + if first.is_none() { + first = Some(f); + } + } + off += sz; + } + assert!( + units > 100, + "expected the full 25 fps vector, got {units} units" + ); + assert_eq!(frames, units, "every temporal unit here shows a picture"); + let f = first.expect("no AV1 frame decoded"); + assert_eq!((f.width, f.height), (320, 240)); + assert!(f.keyframe, "the first temporal unit is a key frame"); + // The vector signals nothing, so E.2.1-equivalent "unspecified" (2) must come + // through UNTOUCHED — `csc_rows` is what resolves it to the BT.709 SDR default, + // and a decoder that resolved it early would make an in-band HDR flip invisible. + assert_eq!(f.color.matrix, 2, "unspecified matrix must survive as 2"); + assert!(!f.color.full_range); + // Planes are tightly packed at the picture's own size — the presenter uploads + // them with no stride, so this invariant is load-bearing, not cosmetic. + assert_eq!(f.plane(0).len(), (f.width * f.height) as usize); + let (cw, ch) = CpuPlanarFrame::chroma_dims(f.width, f.height); + assert_eq!(f.plane(1).len(), (cw * ch) as usize); + assert_eq!(f.plane(2).len(), (cw * ch) as usize); + } } diff --git a/crates/pf-client-core/src/video_vaapi.rs b/crates/pf-client-core/src/video_vaapi.rs deleted file mode 100644 index 1aa79ec8..00000000 --- a/crates/pf-client-core/src/video_vaapi.rs +++ /dev/null @@ -1,287 +0,0 @@ -//! VAAPI (libavcodec hwaccel) decode backend → DRM-PRIME dmabuf for the presenter. Linux-only. - -use crate::video::{ - averr, drm_fourcc_for, frame_is_keyframe, DmabufFrame, DmabufPlane, DrmFrameGuard, - AVERROR_EAGAIN, -}; -use crate::video_color::ColorDesc; -use crate::video_libav::AvBuffer; -use anyhow::{anyhow, bail, Context, Result}; -use ffmpeg_next as ffmpeg; -use std::ptr; - -/// libavcodec offers the formats it can decode into; pick the VAAPI hw surface. Falling -/// back to the first (software) entry would silently decode on the CPU *and* break our -/// dmabuf mapping — return NONE instead so the error surfaces and the session demotes -/// to the software backend explicitly. -#[cfg(target_os = "linux")] -unsafe extern "C" fn pick_vaapi( - _ctx: *mut ffmpeg::ffi::AVCodecContext, - mut list: *const ffmpeg::ffi::AVPixelFormat, -) -> ffmpeg::ffi::AVPixelFormat { - // SAFETY: libav calls this `get_format` callback with a list it owns, terminated by - // `AV_PIX_FMT_NONE` — the walk stops at that terminator, so it stays inside the array, and it - // only reads. - unsafe { - while *list != ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_NONE { - if *list == ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_VAAPI { - return ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_VAAPI; - } - list = list.add(1); - } - } - ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_NONE -} - -#[cfg(target_os = "linux")] -pub(crate) struct VaapiDecoder { - ctx: *mut ffmpeg::ffi::AVCodecContext, - /// The VAAPI hwdevice, owned. Nothing reads this field after construction — the codec context - /// took its own ref via `av_buffer_ref` — it exists so the device outlives the decoder and is - /// unref'd exactly once when it drops. Declared after `ctx` so it still releases AFTER the - /// `Drop` below frees packet/frame/context, which is the order the hand-written unref had. - /// `dead_code` is answered here rather than by removing the field (that would free the device - /// early) or by an underscore name (that would hide what it is). - #[allow(dead_code)] - hw_device: AvBuffer, - packet: *mut ffmpeg::ffi::AVPacket, - frame: *mut ffmpeg::ffi::AVFrame, - /// The selected decoder's registry name (`(*codec).name`) — `"av1"` vs `"libdav1d"` - /// is the difference between hardware decode and a silent CPU fallback, so every - /// log a field report leans on carries it. - name: String, -} - -// SAFETY: the three raw pointers (`ctx`, `packet`, `frame`) are allocations this decoder makes in -// its constructor and frees exactly once in `Drop`; nothing else holds them, and `hw_device` is an -// owning `AvBuffer` whose refcount is atomic. `Send` only permits MOVING that ownership to another -// thread, which libav supports — a codec context may be used from a thread other than the one that -// created it, provided use is serialised, and `&mut self` on every method is that serialisation. -// Deliberately NOT `Sync`: two threads holding `&VaapiDecoder` could call into libav concurrently -// on one context, which libav does not allow. -#[cfg(target_os = "linux")] -unsafe impl Send for VaapiDecoder {} - -#[cfg(target_os = "linux")] -impl VaapiDecoder { - pub(crate) fn new(codec_id: ffmpeg::codec::Id) -> Result { - use ffmpeg::ffi; - // SAFETY: a self-contained builder — every allocation below is made here, each result is - // checked before the next call uses it, and the ones that survive are moved into the - // returned `VaapiDecoder`, which frees each exactly once in `Drop`. - unsafe { - let mut hw_device: *mut ffi::AVBufferRef = ptr::null_mut(); - let r = ffi::av_hwdevice_ctx_create( - &mut hw_device, - ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI, - ptr::null(), - ptr::null_mut(), - 0, - ); - if r < 0 { - bail!("no VAAPI device ({})", ffmpeg::Error::from(r)); - } - // Owned from here: every `bail!` below drops it, so none of them unref by hand. - let hw_device = AvBuffer::from_raw(hw_device) - .context("av_hwdevice_ctx_create(VAAPI) gave no device")?; - // NOT `avcodec_find_decoder`: the ID lookup returns the registry's FIRST - // decoder, and for AV1 that is libdav1d (upstream orders the hwaccel-only - // native decoder last) — a software decoder that silently ignores - // `hw_device_ctx` and fails every frame's VAAPI-format guard mid-stream. - // Select by capability instead: the first decoder that can drive - // AV_PIX_FMT_VAAPI via hw_device_ctx, or fail here at open. - let codec = - crate::video::find_hw_decoder(codec_id, ffi::AVPixelFormat::AV_PIX_FMT_VAAPI)?; - let name = crate::video::codec_name(codec); - let ctx = ffi::avcodec_alloc_context3(codec); - (*ctx).hw_device_ctx = ffi::av_buffer_ref(hw_device.as_ptr()); - (*ctx).get_format = Some(pick_vaapi); - (*ctx).flags |= ffi::AV_CODEC_FLAG_LOW_DELAY as i32; - (*ctx).thread_count = 1; // hwaccel: threads only add latency - - // The presenter holds mapped surfaces PAST receive_frame (the paintable's - // current texture + the newest frame in flight each pin one until GDK's - // release func) — surfaces libavcodec doesn't know are missing from its - // fixed-size VAAPI pool. Without headroom the decoder can recycle a surface - // the renderer is still sampling (intermittent block corruption) or fail - // allocation under scheduling jitter. - (*ctx).extra_hw_frames = 4; - let r = ffi::avcodec_open2(ctx, codec, ptr::null_mut()); - if r < 0 { - let mut ctx = ctx; - ffi::avcodec_free_context(&mut ctx); - bail!("avcodec_open2: {}", ffmpeg::Error::from(r)); - } - Ok(VaapiDecoder { - ctx, - hw_device, - packet: ffi::av_packet_alloc(), - frame: ffi::av_frame_alloc(), - name, - }) - } - } - - /// The selected decoder's registry name (e.g. `"av1"`) — see the field doc. - pub(crate) fn name(&self) -> &str { - &self.name - } - - pub(crate) fn decode(&mut self, au: &[u8]) -> Result> { - use ffmpeg::ffi; - // SAFETY: `packet`/`frame`/`ctx` are this decoder's own allocations, live for its whole - // lifetime; `au` outlives the synchronous `send_packet` that copies out of it, and every - // libav return is checked before the result is used. - unsafe { - let r = ffi::av_new_packet(self.packet, au.len() as i32); - if r < 0 { - return Err(averr("av_new_packet", r)); - } - ptr::copy_nonoverlapping(au.as_ptr(), (*self.packet).data, au.len()); - let r = ffi::avcodec_send_packet(self.ctx, self.packet); - ffi::av_packet_unref(self.packet); - if r < 0 { - return Err(averr("send_packet", r)); - } - let mut out = None; - loop { - let r = ffi::avcodec_receive_frame(self.ctx, self.frame); - if r == AVERROR_EAGAIN { - break; - } - if r < 0 { - return Err(averr("receive_frame", r)); - } - out = Some(self.map_dmabuf()?); // newest wins; older guards drop here - ffi::av_frame_unref(self.frame); - } - Ok(out) - } - } - - /// Map the VAAPI surface to DRM PRIME (zero copy) and lift the descriptor into a - /// `DmabufFrame`. The mapped frame keeps the surface alive via its buffer refs. - /// - /// FFmpeg's VAAPI export uses `VA_EXPORT_SURFACE_SEPARATE_LAYERS`, so an NV12 surface - /// comes back as TWO layers (`R8` luma + `GR88` chroma), each one plane — NOT a single - /// `NV12` layer. The previous code took `layers[0]` only: GTK then saw an `R8` - /// single-plane texture with the chroma dropped, painting the screen green. The fix: - /// derive the COMBINED fourcc from the decoder's software pixel format (NV12 → - /// `DRM_FORMAT_NV12`) and flatten every plane across every layer in order (Y then UV). - fn map_dmabuf(&mut self) -> Result { - use ffmpeg::ffi; - // SAFETY: `self.frame` is this decoder's own `AVFrame`, holding a decoded VAAPI surface — - // the format check below is what proves that before anything reads the hardware layout. - unsafe { - if (*self.frame).format != ffi::AVPixelFormat::AV_PIX_FMT_VAAPI as i32 { - bail!("decoder returned a software frame (no VAAPI surface)"); - } - // The real pixel layout lives on the hardware frames context, not the - // DRM-PRIME layer formats (those are the per-plane R8/GR88 component formats). - let sw_format = { - let hwfc = (*self.frame).hw_frames_ctx; - if hwfc.is_null() { - bail!("VAAPI frame without a hardware frames context"); - } - (*((*hwfc).data as *const ffi::AVHWFramesContext)).sw_format - }; - let fourcc = drm_fourcc_for(sw_format) - .ok_or_else(|| anyhow!("unsupported VAAPI output format {sw_format:?}"))?; - - let drm = ffi::av_frame_alloc(); - (*drm).format = ffi::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32; - let r = ffi::av_hwframe_map(drm, self.frame, ffi::AV_HWFRAME_MAP_READ as i32); - if r < 0 { - let mut drm = drm; - ffi::av_frame_free(&mut drm); - return Err(averr("av_hwframe_map", r)); - } - let desc = (*drm).data[0] as *const ffi::AVDRMFrameDescriptor; - let guard = DrmFrameGuard(drm); - let d = &*desc; - if d.nb_layers < 1 || d.nb_objects < 1 { - bail!("DRM descriptor without layers/objects"); - } - - // Flatten planes across ALL layers, in declared order — the combined fourcc's - // plane order (Y, then UV for NV12) matches the layer order FFmpeg emits. - let mut planes = Vec::new(); - for layer in &d.layers[..d.nb_layers as usize] { - for p in &layer.planes[..layer.nb_planes as usize] { - let obj = &d.objects[p.object_index as usize]; - planes.push(DmabufPlane { - fd: obj.fd, - offset: p.offset as u32, - stride: p.pitch as u32, - }); - } - } - - // The whole surface shares one tiling modifier (one BO on radeonsi); GTK takes - // a single modifier for the texture. - let modifier = d.objects[0].format_modifier; - - log_descriptor_once(d, sw_format, fourcc, modifier, &self.name); - - Ok(DmabufFrame { - width: (*self.frame).width as u32, - height: (*self.frame).height as u32, - fourcc, - modifier, - planes, - // SAFETY: `self.frame` is the live decoded AVFrame (unref'd only after - // this returns); plain CICP field reads. - color: ColorDesc::from_raw(self.frame), - keyframe: frame_is_keyframe(self.frame), - guard, - }) - } - } -} - -/// One-time dump of the DRM descriptor layout (objects, layers, planes, modifier) — so a -/// new client/driver combination's real layout is visible in the logs without a debugger. -#[cfg(target_os = "linux")] -fn log_descriptor_once( - d: &ffmpeg_next::ffi::AVDRMFrameDescriptor, - sw: ffmpeg_next::ffi::AVPixelFormat, - fourcc: u32, - modifier: u64, - decoder: &str, -) { - use std::sync::atomic::{AtomicBool, Ordering}; - static ONCE: AtomicBool = AtomicBool::new(true); - if !ONCE.swap(false, Ordering::Relaxed) { - return; - } - let layers: Vec<(u32, i32)> = d.layers[..d.nb_layers.max(0) as usize] - .iter() - .map(|l| (l.format, l.nb_planes)) - .collect(); - tracing::info!( - sw_format = ?sw, - chosen_fourcc = format_args!("{:#010x}", fourcc), - nb_objects = d.nb_objects, - nb_layers = d.nb_layers, - ?layers, - modifier = format_args!("{:#018x}", modifier), - decoder, - "VAAPI dmabuf descriptor layout (first frame)" - ); -} - -#[cfg(target_os = "linux")] -impl Drop for VaapiDecoder { - fn drop(&mut self) { - use ffmpeg::ffi; - // SAFETY: each pointer is this decoder's own allocation and nothing else holds it; `Drop` - // runs exactly once, and each free nulls the pointer through its `&mut`, so none can be - // released twice. Freed packet-then-frame-then-context, the order libav documents. - unsafe { - ffi::av_packet_free(&mut self.packet); - ffi::av_frame_free(&mut self.frame); - ffi::avcodec_free_context(&mut self.ctx); - // `hw_device` is an `AvBuffer` and unrefs itself when the field drops, right after this. - } - } -} diff --git a/crates/pf-client-core/src/video_vaapi_native.rs b/crates/pf-client-core/src/video_vaapi_native.rs new file mode 100644 index 00000000..bc8cb0e0 --- /dev/null +++ b/crates/pf-client-core/src/video_vaapi_native.rs @@ -0,0 +1,2441 @@ +//! Native VAAPI decode — M6 of the native-decode program, and the FFmpeg-free +//! replacement for `video_vaapi`, the libavcodec VAAPI rung M10 deleted. +//! +//! `pf-vaadec` turns one pf-bitstream `AuPlan` into the buffers a +//! `vaRenderPicture` call carries; this module is everything libva-shaped around +//! that: the display, the config and context, the surface pool, the submission, and +//! the DRM-PRIME export the presenter imports. Its output is +//! [`DecodedImage::NativeDmabuf`] — physically identical to what the libavcodec VAAPI +//! rung delivered, and deliberately a different variant so that while both existed no +//! log could confuse them (see that variant's docs). +//! +//! # libva is dlopen'd, never linked +//! +//! Everything here resolves `libva.so.2` and `libva-drm.so.2` at runtime. Three +//! things follow, and all three are the point: +//! +//! * `pf-client-core` gains no build-time libva dependency, so the **pf-lxcheck2 +//! container compiles and clippies this whole rung** even though it has no +//! `libva-dev`. On a program where `cfg(windows)` code could only ever be checked +//! on a box, that is the difference between a defect found on a laptop and one +//! found on hardware. +//! * A machine without libva gets a clean refusal at construction — the ladder +//! falls through exactly as it does for any other unavailable rung — instead of a +//! packaging dependency or a link error. +//! * Nothing in the shipped packages needs to change to try it. +//! +//! # The surface pool, and why it is not the slot map +//! +//! VAAPI has no DPB slots: `VAPictureH264::picture_id` is a `VASurfaceID`, and +//! `vaBeginPicture` takes the target surface itself. The slot ledger +//! ([`pf_vaadec::SlotMap`], borrowed from the Vulkan rung) is our own indirection +//! from a stable `PicId` to a small integer, and a slot is emphatically NOT a +//! surface index. +//! +//! It cannot be, because [`pf_vaadec::SlotMap::assign`] hands out the lowest free +//! slot and a slot freed by this access unit's own removals is free by then — +//! measured at **225 of the vendored vector's 250 access units** (pf-vaadec's +//! `the_setup_picture_routinely_inherits_a_just_freed_slot`). A surface bound by +//! slot index would therefore decode, on nine frames in ten, straight into the +//! surface holding the picture that was just displayed. Under zero-copy the +//! presenter is still sampling that surface: it holds the frame until its fence has +//! been waited, which is exactly what "zero-copy" costs. +//! +//! So the pool follows pf-vkdecode's image model. Surfaces outnumber slots by +//! [`pf_vaadec::config::PRESENTER_HEADROOM`], the decode target is taken from a free +//! list at activation time and bound to its slot afterwards, and a surface the +//! presenter holds simply stays off the free list until its release token comes +//! back. A surface is free when no live picture is bound to it AND no consumer holds +//! it — two conditions, tracked separately, because they end at different times. + +use std::os::fd::AsRawFd as _; +use std::os::fd::FromRawFd as _; +use std::os::fd::OwnedFd; +use std::os::raw::c_char; +use std::os::raw::c_int; +use std::os::raw::c_uint; +use std::os::raw::c_void; +use std::sync::mpsc; + +use anyhow::anyhow; +use anyhow::bail; +use anyhow::Context as _; +use anyhow::Result; + +use crate::video::DecodeHealth; +use crate::video::DmabufFrame; +use crate::video::DmabufPlane; +use crate::video::DrmFrameGuard; +use crate::video::StreamFormat; +use crate::video_color::ColorDesc; + +/// `PUNKTFUNK_DECODER=native-vaapi` — the pin that selects this rung. +/// +/// ⚠ This rung has decoded nothing on any hardware (`video::native_evidence`). Since M10 +/// deleted libavcodec's VAAPI hwaccel it is the only VAAPI there is, so `auto` reaches it +/// in the vendor order and the session log says at `warn` that nothing has run it. +/// +/// The PIN is what makes the missing evidence generatable: it skips the vendor order, so a +/// lab run can reach this rung on a box where `auto` puts Vulkan first. A rule that gated +/// the pin too would be a rule no hardware run could ever satisfy. +pub(crate) const DECODER_PIN: &str = "native-vaapi"; + +// --------------------------------------------------------------------------- +// libva, resolved at runtime +// --------------------------------------------------------------------------- + +/// `VADisplay` — an opaque driver handle. +type VaDisplay = *mut c_void; +type VaStatus = c_int; +type VaSurfaceId = c_uint; +type VaConfigId = c_uint; +type VaContextId = c_uint; +type VaBufferId = c_uint; + +const VA_STATUS_SUCCESS: VaStatus = 0; +/// `VA_INVALID_ID` — also the "no surface" sentinel in a slot table. +const VA_INVALID_ID: c_uint = 0xffff_ffff; +/// `VA_PROGRESSIVE` — the only picture structure this rung's envelope contains. +const VA_PROGRESSIVE: c_uint = 0x0001; + +/// `VAGenericValue` — 16 bytes, value at offset 8, align 8 (measured). +/// +/// The C type's `value` is a union of `int`/`float`/`void*`/function pointer. Two +/// consequences are written out here rather than left to a Rust `union` declaration: +/// +/// * the union holds a pointer, so it is **eight-byte aligned** — which is why the +/// enum ahead of it is followed by four bytes of padding, and why the whole thing +/// is 16 bytes and not 12. The compile-time assertion below caught exactly that +/// mistake in this file; +/// * a Rust union initialised through its `i32` arm leaves the other four bytes +/// **uninitialised**, and those are the bytes a driver reading the pointer arm +/// would see. Naming the remainder and writing zero means everything crossing the +/// FFI boundary was written by us. +#[repr(C, align(8))] +#[derive(Clone, Copy)] +struct VaGenericValue { + kind: c_int, + _pad: u32, + /// The union's integer arm, first in the union — where `VAGenericValue.i` lives. + i: i32, + /// The rest of the union. Always zero. + _rest: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct VaSurfaceAttrib { + kind: c_int, + flags: c_uint, + value: VaGenericValue, +} + +/// `VASurfaceAttribPixelFormat` / `VAGenericValueTypeInteger` / +/// `VA_SURFACE_ATTRIB_SETTABLE` — measured by `pf-vaadec/layout-probe.c`. +const VA_SURFACE_ATTRIB_PIXEL_FORMAT: c_int = 1; +const VA_GENERIC_VALUE_TYPE_INTEGER: c_int = 1; +const VA_SURFACE_ATTRIB_SETTABLE: c_uint = 0x0002; + +// The layouts these calls pass by value, measured (`pf-vaadec/layout-probe.c`). +const _: () = { + assert!(size_of::() == 16); + assert!(std::mem::offset_of!(VaGenericValue, i) == 8); + assert!(size_of::() == 24); + assert!(std::mem::offset_of!(VaSurfaceAttrib, flags) == 4); + assert!(std::mem::offset_of!(VaSurfaceAttrib, value) == 8); +}; + +/// The libva entry points this rung calls, resolved from `libva.so.2` and +/// `libva-drm.so.2` at runtime (the same pattern the host's NVML and CUDA loaders +/// use — no link-time dependency, absent library = clean refusal). +struct Libva { + _va: libloading::Library, + _drm: libloading::Library, + get_display_drm: unsafe extern "C" fn(c_int) -> VaDisplay, + initialize: unsafe extern "C" fn(VaDisplay, *mut c_int, *mut c_int) -> VaStatus, + terminate: unsafe extern "C" fn(VaDisplay) -> VaStatus, + error_str: unsafe extern "C" fn(VaStatus) -> *const c_char, + query_config_entrypoints: + unsafe extern "C" fn(VaDisplay, c_int, *mut c_int, *mut c_int) -> VaStatus, + max_entrypoints: unsafe extern "C" fn(VaDisplay) -> c_int, + create_config: unsafe extern "C" fn( + VaDisplay, + c_int, + c_int, + *mut c_void, + c_int, + *mut VaConfigId, + ) -> VaStatus, + destroy_config: unsafe extern "C" fn(VaDisplay, VaConfigId) -> VaStatus, + create_surfaces: unsafe extern "C" fn( + VaDisplay, + c_uint, + c_uint, + c_uint, + *mut VaSurfaceId, + c_uint, + *mut VaSurfaceAttrib, + c_uint, + ) -> VaStatus, + destroy_surfaces: unsafe extern "C" fn(VaDisplay, *mut VaSurfaceId, c_int) -> VaStatus, + create_context: unsafe extern "C" fn( + VaDisplay, + VaConfigId, + c_int, + c_int, + c_int, + *mut VaSurfaceId, + c_int, + *mut VaContextId, + ) -> VaStatus, + destroy_context: unsafe extern "C" fn(VaDisplay, VaContextId) -> VaStatus, + create_buffer: unsafe extern "C" fn( + VaDisplay, + VaContextId, + c_uint, + c_uint, + c_uint, + *mut c_void, + *mut VaBufferId, + ) -> VaStatus, + destroy_buffer: unsafe extern "C" fn(VaDisplay, VaBufferId) -> VaStatus, + begin_picture: unsafe extern "C" fn(VaDisplay, VaContextId, VaSurfaceId) -> VaStatus, + render_picture: + unsafe extern "C" fn(VaDisplay, VaContextId, *mut VaBufferId, c_int) -> VaStatus, + end_picture: unsafe extern "C" fn(VaDisplay, VaContextId) -> VaStatus, + sync_surface: unsafe extern "C" fn(VaDisplay, VaSurfaceId) -> VaStatus, + /// `vaExportSurfaceHandle(dpy, surface_id, mem_type, flags, descriptor)` — five + /// parameters, and the descriptor's type is decided by `mem_type`. + export_surface_handle: + unsafe extern "C" fn(VaDisplay, VaSurfaceId, c_uint, c_uint, *mut c_void) -> VaStatus, +} + +impl Libva { + fn load() -> Result { + // SAFETY: `Library::new` runs the trusted system libva's initialisers, and each + // `lib.get` resolves a documented libva symbol to the matching `unsafe extern "C"` + // signature transcribed from `va.h` / `va_drm.h` (by-value integers and pointers + // throughout, no callbacks). Both `Library` handles are stored in the returned + // struct, so every resolved pointer outlives its uses. + unsafe { + let va = libloading::Library::new("libva.so.2") + .context("libva.so.2 (no VAAPI runtime on this system)")?; + let drm = libloading::Library::new("libva-drm.so.2") + .context("libva-drm.so.2 (no VAAPI DRM backend on this system)")?; + // Each symbol is resolved AT the field's own type — `Library::get` is + // generic, so the struct's declared signature is what `dlsym`'s pointer + // is read as. No `transmute` anywhere: a mistyped entry point is then a + // mismatch the reader can see next to the declaration rather than a cast + // that accepts anything. Bound with `let` (not inline in the literal) so + // each borrow of the `Library` ends before it is moved into the struct. + macro_rules! get { + ($lib:expr, $name:literal) => { + *$lib + .get(concat!($name, "\0").as_bytes()) + .map_err(|e| anyhow!(concat!("dlsym ", $name, ": {}"), e))? + }; + } + let get_display_drm = get!(drm, "vaGetDisplayDRM"); + let initialize = get!(va, "vaInitialize"); + let terminate = get!(va, "vaTerminate"); + let error_str = get!(va, "vaErrorStr"); + let query_config_entrypoints = get!(va, "vaQueryConfigEntrypoints"); + let max_entrypoints = get!(va, "vaMaxNumEntrypoints"); + let create_config = get!(va, "vaCreateConfig"); + let destroy_config = get!(va, "vaDestroyConfig"); + let create_surfaces = get!(va, "vaCreateSurfaces"); + let destroy_surfaces = get!(va, "vaDestroySurfaces"); + let create_context = get!(va, "vaCreateContext"); + let destroy_context = get!(va, "vaDestroyContext"); + let create_buffer = get!(va, "vaCreateBuffer"); + let destroy_buffer = get!(va, "vaDestroyBuffer"); + let begin_picture = get!(va, "vaBeginPicture"); + let render_picture = get!(va, "vaRenderPicture"); + let end_picture = get!(va, "vaEndPicture"); + let sync_surface = get!(va, "vaSyncSurface"); + let export_surface_handle = get!(va, "vaExportSurfaceHandle"); + Ok(Libva { + get_display_drm, + initialize, + terminate, + error_str, + query_config_entrypoints, + max_entrypoints, + create_config, + destroy_config, + create_surfaces, + destroy_surfaces, + create_context, + destroy_context, + create_buffer, + destroy_buffer, + begin_picture, + render_picture, + end_picture, + sync_surface, + export_surface_handle, + _va: va, + _drm: drm, + }) + } + } + + /// libva's own text for a status code, so a driver's reason reaches the log + /// instead of a bare number. + fn err(&self, what: &str, status: VaStatus) -> anyhow::Error { + // SAFETY: `vaErrorStr` is documented total — it returns a pointer into libva's + // static string table for any input, valid while the library is loaded, which + // `&self` proves. + let text = unsafe { + let p = (self.error_str)(status); + if p.is_null() { + String::new() + } else { + std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned() + } + }; + if text.is_empty() { + anyhow!("{what} failed ({status})") + } else { + anyhow!("{what} failed: {text} ({status})") + } + } + + fn check(&self, what: &str, status: VaStatus) -> Result<()> { + if status == VA_STATUS_SUCCESS { + Ok(()) + } else { + Err(self.err(what, status)) + } + } +} + +// --------------------------------------------------------------------------- +// The display +// --------------------------------------------------------------------------- + +/// An initialised `VADisplay` over a DRM render node. +struct Display { + va: Libva, + display: VaDisplay, + /// The render node. libva does NOT dup the fd it is given, so the display is + /// only valid while this is open — it is dropped after `vaTerminate`. + node: Option, + /// Which node, for the field report that asks "which GPU decoded?". + path: String, + version: (c_int, c_int), +} + +// SAFETY: the display is created and used from ONE thread (the pump), and `Send` only +// permits MOVING that ownership. libva is not safe for concurrent calls on one +// display, which is why `Sync` is deliberately absent: every path into it goes through +// `&mut NativeVaapiDecoder`, and that is the serialisation. +unsafe impl Send for Display {} + +impl Display { + /// Open a render node and initialise libva on it. + /// + /// `PUNKTFUNK_VAAPI_DEVICE` pins a node explicitly. Otherwise nodes are tried in + /// name order and the first that initialises wins — the rule libavcodec's VAAPI + /// device creation uses when given no device string, so a box that got hardware + /// decode from the libavcodec rung this replaced gets the same GPU here. + /// + /// ⚠ On a multi-GPU box that is not necessarily the PRESENTER's GPU, and a dmabuf + /// exported from one GPU and imported into another either fails outright or + /// copies. The libavcodec rung had the same property; the env pin is the + /// escape hatch, and the chosen node is logged so a field report can name it. + fn open(va: Libva) -> Result { + if let Some(pin) = std::env::var_os("PUNKTFUNK_VAAPI_DEVICE") { + let path = pin.to_string_lossy().into_owned(); + let (display, node, version) = Display::probe(&va, &path) + .with_context(|| format!("PUNKTFUNK_VAAPI_DEVICE={path}"))?; + return Ok(Display { + va, + display, + node: Some(node), + path, + version, + }); + } + let mut nodes: Vec = std::fs::read_dir("/dev/dri") + .context("/dev/dri (no DRM devices on this machine)")? + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .filter(|p| { + p.file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.starts_with("renderD")) + }) + .collect(); + nodes.sort(); + let mut tried: Vec = Vec::new(); + for node in &nodes { + let path = node.to_string_lossy().into_owned(); + match Display::probe(&va, &path) { + Ok((display, node, version)) => { + return Ok(Display { + va, + display, + node: Some(node), + path, + version, + }) + } + Err(e) => { + tracing::debug!(node = %path, reason = %format!("{e:#}"), "not a VAAPI device"); + tried.push(path); + } + } + } + bail!( + "no render node initialised a VAAPI display ({})", + if tried.is_empty() { + "/dev/dri has no renderD* nodes".to_string() + } else { + format!("tried {}", tried.join(", ")) + } + ) + } + + /// Try ONE node, borrowing the loaded library — so a box with several GPUs + /// dlopens libva once rather than once per node, and a failure carries only its + /// reason. + fn probe(va: &Libva, path: &str) -> Result<(VaDisplay, OwnedFd, (c_int, c_int))> { + let node = OwnedFd::from( + std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(path) + .with_context(|| format!("open {path}"))?, + ); + // SAFETY: `vaGetDisplayDRM` takes the render node's fd by value and returns an + // opaque display or null; `vaInitialize` writes the two version ints through + // the out-pointers, which are locals live across the call. The fd stays open in + // `node` for as long as the display exists — libva does not dup it. + unsafe { + let display = (va.get_display_drm)(node.as_raw_fd()); + if display.is_null() { + bail!("vaGetDisplayDRM({path}) returned no display"); + } + let (mut major, mut minor) = (0, 0); + let status = (va.initialize)(display, &mut major, &mut minor); + if status != VA_STATUS_SUCCESS { + let e = va.err("vaInitialize", status); + // The display is unusable but still allocated; terminate it so the + // driver's own state goes with the attempt. + (va.terminate)(display); + // No path in the context: every caller already names the node it + // asked about, and on a box where nothing initialises that printed + // the node twice on every line. + return Err(e); + } + Ok((display, node, (major, minor))) + } + } +} + +impl Display { + /// Does this device decode that profile? + /// + /// Asked BEFORE `vaCreateConfig` so an unsupported profile is a clean refusal + /// naming the profile, not a driver status code — the ladder falls through + /// either way, but only one of them tells a field report why. + fn require_entrypoint(&self, profile: c_int) -> Result<()> { + // SAFETY: `vaMaxNumEntrypoints` returns the array size this display needs; + // the vector is allocated to exactly that and `count` is a local written + // through by the call. + unsafe { + let max = (self.va.max_entrypoints)(self.display); + if max <= 0 { + bail!("vaMaxNumEntrypoints returned {max}"); + } + let mut entrypoints = vec![0 as c_int; max as usize]; + let mut count: c_int = 0; + self.va.check( + "vaQueryConfigEntrypoints", + (self.va.query_config_entrypoints)( + self.display, + profile, + entrypoints.as_mut_ptr(), + &mut count, + ), + )?; + let vld = pf_vaadec::VA_ENTRYPOINT_VLD as c_int; + if !entrypoints[..count.clamp(0, max) as usize].contains(&vld) { + bail!("this device has no VLD decode entrypoint for VAProfile {profile}"); + } + } + Ok(()) + } + + /// `vaCreateBuffer` with the data copied in — libva's documented behaviour for a + /// non-null `data` pointer, and what makes the caller's structs free to die + /// straight after. + /// + /// `size` is ONE element's size and `count` is how many follow, because that is + /// how `vaCreateBuffer` is declared and the two are not interchangeable. Every + /// H.264 and H.265 buffer here passes `count = 1`; **AV1's tile-parameter buffer + /// is the one exception** — libavcodec's `vaapi_av1.c` sends a whole tile group's + /// records in a single buffer beside that group's one data buffer, and a driver + /// reads `num_elements` records out of it. + fn create_buffer( + &self, + context: VaContextId, + kind: u32, + size: usize, + count: usize, + data: *const c_void, + ) -> Result { + let mut id: VaBufferId = VA_INVALID_ID; + // SAFETY: a live display and context; `data` points at `size * count` readable + // bytes for the duration of the call (the caller's live struct or slice), and + // `id` is a local written through. libva copies the payload before returning. + self.va.check("vaCreateBuffer", unsafe { + (self.va.create_buffer)( + self.display, + context, + kind as c_uint, + size as c_uint, + count as c_uint, + data.cast_mut(), + &mut id, + ) + })?; + Ok(id) + } + + /// Destroy every buffer of a submission. + /// + /// ⚠ Not optional and not automatic. `va.h` is explicit — *"The user must call + /// vaDestroyBuffer() to destroy a buffer"*, and *"a buffer can be re-used and + /// sent to the server by another Begin/Render/End sequence if vaDestroyBuffer() + /// is not called"*. The libva 0.x behaviour where `vaEndPicture` consumed them is + /// long gone; leaking two-plus buffers per picture at 60 fps exhausts the + /// driver's buffer store in minutes. + fn destroy_buffers(&self, buffers: &[VaBufferId]) { + for &b in buffers { + if b == VA_INVALID_ID { + continue; + } + // SAFETY: each id came from `create_buffer` on this display and is + // destroyed exactly once — the submission's list is consumed here. + unsafe { (self.va.destroy_buffer)(self.display, b) }; + } + } +} + +impl Drop for Display { + fn drop(&mut self) { + // SAFETY: `self.display` was initialised in `open_node` and nothing else + // terminates it; `Drop` runs once. The node fd is dropped AFTER this, which is + // the order libva requires — it holds the fd, it does not own it. + unsafe { (self.va.terminate)(self.display) }; + self.node = None; + } +} + +// --------------------------------------------------------------------------- +// The stream shape a session is built for +// --------------------------------------------------------------------------- + +/// Everything about a stream that sizes or configures a session. Any change rebuilds +/// the whole thing: the config, the context, the surface pool and the slot map all +/// derive from it, and a half-rebuilt session hands out surfaces the pool does not +/// have. (M5's review found the depth/chroma half of this missing on the D3D11 rung — +/// the Windows host flips an HDR desktop to PQ in-band with a NEW SPS at unchanged +/// size, so a shape keyed on size alone decodes 10-bit samples into an 8-bit pool.) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct StreamShape { + coded_width: u32, + coded_height: u32, + display_width: u32, + display_height: u32, + max_dpb_frames: usize, + chroma_format_idc: u8, + bit_depth: u8, +} + +/// Which codec, and the planner that plans it. +enum Planner { + H264(Box), + H265(Box), + Av1(Box), +} + +impl Planner { + fn name(&self) -> &'static str { + match self { + Planner::H264(_) => "native-vaapi h264", + Planner::H265(_) => "native-vaapi h265", + Planner::Av1(_) => "native-vaapi av1", + } + } +} + +// --------------------------------------------------------------------------- +// Surface release: the consumer's half of the zero-copy contract +// --------------------------------------------------------------------------- + +/// What a shipped frame hands back when the consumer is done with it. +/// +/// `generation` is what makes a renegotiation safe: a token from a retired pool +/// names a surface index that no longer exists, and freeing that index in the NEW +/// pool would hand a live surface to the decoder as if it were spare. +#[derive(Debug, Clone, Copy)] +struct VaRelease { + surface: usize, + generation: u64, +} + +/// Holds one shipped picture's surface out of the decoder's free list, and owns the +/// fds exported for it. +/// +/// The presenter DUPS every fd it imports (`pf-presenter`'s dmabuf import says so in +/// as many words) and drops the frame — and so this guard — only after the fence for +/// its sampling submission has been waited. So "guard dropped" means "the GPU is done +/// reading", which is exactly when the surface may be decoded into again. +pub struct VaFrameGuard { + /// The exported PRIME fds, closed by this field's own drop. One per OBJECT, not + /// per plane: several planes routinely name one object, and closing a shared fd + /// twice would close an unrelated file. + _fds: Vec, + tx: mpsc::Sender, + release: VaRelease, +} + +impl Drop for VaFrameGuard { + fn drop(&mut self) { + // A dead channel means the decoder is gone; there is nothing to release to. + let _ = self.tx.send(self.release); + } +} + +// --------------------------------------------------------------------------- +// The session +// --------------------------------------------------------------------------- + +/// The live config, context and surface pool for one [`StreamShape`]. +struct Session { + shape: StreamShape, + config: VaConfigId, + context: VaContextId, + /// The pool. Indices into this are what everything else here refers to. + surfaces: Vec, + /// A consumer holds this surface. Cleared when its release token returns. + held: Vec, + /// DPB slot → pool index, rebound at ACTIVATION (module docs). `None` for a slot + /// holding no picture. + slot_surface: Vec>, + /// Decoded pictures the planner has not output yet, `(PicId, pool index)`. + /// Separate from the slot binding because the two end at different times: a + /// non-reference picture leaves the DPB immediately but still owes an output. + pending: Vec<(u64, usize)>, + slots: pf_vaadec::SlotMap, + /// The surface fourcc the pool was created with (NV12 or P010). + fourcc: u32, + /// Bumped on every rebuild; stamped into release tokens. + generation: u64, +} + +impl Session { + /// A surface bound to no live picture, owed no output, and held by no consumer. + /// + /// All three conditions, because they end at different moments: a picture leaves + /// the DPB when the planner removes it, stops being pending when it is output, + /// and stops being held when the presenter's fence has been waited — and the + /// display is usually the LAST of the three. + fn free_surface(&self) -> Option { + (0..self.surfaces.len()).find(|i| { + !self.held[*i] + && !self.slot_surface.contains(&Some(*i)) + && !self.pending.iter().any(|(_, p)| p == i) + }) + } + + /// Re-derive the slot bindings from the ledger: a slot the planner released + /// binds nothing. + /// + /// Done by reading the ledger rather than by tracking `removed` here, so there is + /// exactly one source of truth for which slots are live. `plan_to_va` has already + /// applied this AU's removals by the time it returns. + fn sync_slot_bindings(&mut self) { + let mut live = vec![false; self.slot_surface.len()]; + for (slot, _) in self.slots.held() { + if let Some(l) = live.get_mut(usize::from(slot)) { + *l = true; + } + } + for (slot, bound) in self.slot_surface.iter_mut().enumerate() { + if !live[slot] { + *bound = None; + } + } + } + + /// Slot → `VASurfaceID`, for the pictures the DPB holds RIGHT NOW. + /// + /// Built before the conversion, because references resolve against the + /// pre-removal state. An unbound slot reads [`VA_INVALID_ID`], never 0 — a zero + /// there is a plausible surface id, and the conversion only ever indexes slots + /// the ledger says are live, so the sentinel exists to make a bug in that + /// argument visible rather than silent. + fn surface_table(&self) -> Vec { + self.slot_surface + .iter() + .map(|b| b.map_or(VA_INVALID_ID, |i| self.surfaces[i])) + .collect() + } + + /// Release every libva object this session owns, in creation-reverse order. + /// Called explicitly (a `Drop` here could not reach the display). + fn destroy(mut self, d: &Display) { + // SAFETY: every handle was created on this display by `build` and is + // destroyed exactly once — `destroy` consumes `self`. Surfaces are freed + // after the context that referenced them, which is the order libva documents. + unsafe { + (d.va.destroy_context)(d.display, self.context); + (d.va.destroy_surfaces)( + d.display, + self.surfaces.as_mut_ptr(), + self.surfaces.len() as c_int, + ); + (d.va.destroy_config)(d.display, self.config); + } + } + + /// Build a config, a surface pool and a context for one stream shape. + fn build(d: &Display, codec: pf_vaadec::Codec, shape: StreamShape) -> Result { + let profile = pf_vaadec::profile_for(codec, shape.chroma_format_idc, shape.bit_depth) + .map_err(|e| anyhow!("{e}"))?; + let rt_format = pf_vaadec::rt_format(shape.chroma_format_idc, shape.bit_depth) + .map_err(|e| anyhow!("{e}"))?; + let fourcc = match shape.bit_depth { + 8 => pf_vaadec::VA_FOURCC_NV12, + 10 => pf_vaadec::VA_FOURCC_P010, + other => bail!("no VAAPI surface format for {other}-bit output"), + }; + d.require_entrypoint(profile.value)?; + + // `VAConfigAttribRTFormat` (= 0, measured) is set explicitly rather than left + // to the driver's default: on a Main 10 stream the default is the 8-bit + // format, and a decoder writing 10-bit samples into an 8-bit surface is the + // silent-narrowing failure this program refuses everywhere else. + let mut attrib = VaConfigAttrib { + kind: VA_CONFIG_ATTRIB_RT_FORMAT, + value: rt_format, + }; + let mut config: VaConfigId = VA_INVALID_ID; + // SAFETY: a live display; `attrib` and `config` are locals that outlive the + // call, and the count matches the slice length. + d.va.check("vaCreateConfig", unsafe { + (d.va.create_config)( + d.display, + profile.value, + pf_vaadec::VA_ENTRYPOINT_VLD as c_int, + (&mut attrib as *mut VaConfigAttrib).cast::(), + 1, + &mut config, + ) + })?; + + // From here every early return must destroy what has been created, so the + // fallible tail is written as a closure and unwound once. + let built = (|| -> Result { + let count = pf_vaadec::surface_count(shape.max_dpb_frames); + let mut surfaces: Vec = vec![VA_INVALID_ID; count]; + let mut pixel = VaSurfaceAttrib { + kind: VA_SURFACE_ATTRIB_PIXEL_FORMAT, + flags: VA_SURFACE_ATTRIB_SETTABLE, + value: VaGenericValue { + kind: VA_GENERIC_VALUE_TYPE_INTEGER, + _pad: 0, + // The fourcc is an i32 in libva's integer arm; the top bit is + // clear for every fourcc here, so the cast is value-preserving. + i: fourcc as i32, + _rest: 0, + }, + }; + // Surfaces are allocated at the CODED size. The conformance window is a + // display-time crop, and a pool sized to the display region would be + // short by the codec's granule padding — the scar that smears rows. + // SAFETY: live display; the surface array and the attribute are locals + // that outlive the call and the counts match their lengths. + d.va.check("vaCreateSurfaces", unsafe { + (d.va.create_surfaces)( + d.display, + rt_format, + shape.coded_width, + shape.coded_height, + surfaces.as_mut_ptr(), + count as c_uint, + &mut pixel, + 1, + ) + })?; + + let mut context: VaContextId = VA_INVALID_ID; + // SAFETY: live display and the config/surfaces just created; `context` is + // a local that outlives the call. libva copies the surface array. + let status = unsafe { + (d.va.create_context)( + d.display, + config, + shape.coded_width as c_int, + shape.coded_height as c_int, + VA_PROGRESSIVE as c_int, + surfaces.as_mut_ptr(), + count as c_int, + &mut context, + ) + }; + if let Err(e) = d.va.check("vaCreateContext", status) { + // SAFETY: destroying the surfaces this closure just created, on the + // unwind path, before they are moved into a Session. + unsafe { + (d.va.destroy_surfaces)( + d.display, + surfaces.as_mut_ptr(), + surfaces.len() as c_int, + ) + }; + return Err(e); + } + + let slots = pf_vaadec::SlotMap::new(shape.max_dpb_frames); + let slot_count = slots.capacity(); + tracing::info!( + node = %d.path, + va = format_args!("{}.{}", d.version.0, d.version.1), + profile = profile.name, + coded = format_args!("{}x{}", shape.coded_width, shape.coded_height), + display = format_args!("{}x{}", shape.display_width, shape.display_height), + bit_depth = shape.bit_depth, + surfaces = count, + dpb_slots = slot_count, + "native VAAPI decode session built" + ); + Ok(Session { + shape, + config, + context, + surfaces, + held: vec![false; count], + slot_surface: vec![None; slot_count], + pending: Vec::new(), + slots, + fourcc, + generation: 0, + }) + })(); + if built.is_err() { + // SAFETY: destroying the config created above, on the unwind path; no + // Session took ownership of it. + unsafe { (d.va.destroy_config)(d.display, config) }; + } + built + } +} + +/// `VAConfigAttrib` — 8 bytes, `{type, value}` at 0 and 4 (measured). +#[repr(C)] +#[derive(Clone, Copy)] +struct VaConfigAttrib { + kind: c_int, + value: c_uint, +} + +/// `VAConfigAttribRTFormat` — measured, and 0 is a real enumerator here rather than +/// a "left unset", which is why it is named. +const VA_CONFIG_ATTRIB_RT_FORMAT: c_int = 0; + +/// `VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2` — the export memory type that yields a +/// [`pf_vaadec::VaDrmPrimeSurfaceDescriptor`]. Measured; the older +/// `..._DRM_PRIME` (0x2000_0000) hands back a different, smaller structure. +const VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2: c_uint = 0x4000_0000; + +const _: () = { + assert!(size_of::() == 8); + assert!(std::mem::offset_of!(VaConfigAttrib, value) == 4); +}; + +// --------------------------------------------------------------------------- +// The decoder +// --------------------------------------------------------------------------- + +/// The native VAAPI rung. +pub(crate) struct NativeVaapiDecoder { + display: Display, + planner: Planner, + session: Option, + health: DecodeHealth, + /// A concealed AU asks the pump for a re-anchor, through the same one throttle + /// every other ask uses. Drained by [`Self::take_recovery_request`]. + recovery_request: bool, + generation: u64, + release_tx: mpsc::Sender, + release_rx: mpsc::Receiver, + /// Pool-index releases that arrived for a RETIRED generation, so the count of + /// what is still outstanding is honest in the log. + stale_releases: u64, +} + +impl NativeVaapiDecoder { + /// Build the rung, refusing anything this device or this crate cannot decode + /// BEFORE the ladder commits to it. + /// + /// The refusal is at construction on purpose, and it is M3 WP-2's lesson: a + /// backend that accepts a session and then refuses its first access unit has + /// already cost the ladder its fall-through — the refusal arrives as a decode + /// error, burns the demotion streak, and lands the session on a rung far below + /// the one it would have had. So the negotiated [`StreamFormat`] is probed here, + /// where "no" simply means the next rung is tried. + pub(crate) fn new(codec: pf_vaadec::Codec, stream: StreamFormat) -> Result { + let depth = stream.bit_depth; + pf_vaadec::profile_for(codec, stream.chroma_format_idc, depth) + .map_err(|e| anyhow!("{e}")) + .context("the negotiated stream shape has no VAAPI decode profile")?; + let va = Libva::load().context("libva")?; + let display = Display::open(va)?; + let planner = match codec { + pf_vaadec::Codec::H264 => Planner::H264(Box::new(pf_vaadec::H264Planner::new())), + pf_vaadec::Codec::H265 => Planner::H265(Box::new(pf_vaadec::H265Planner::new())), + pf_vaadec::Codec::Av1 => Planner::Av1(Box::new(pf_vaadec::Av1Planner::new())), + }; + let (release_tx, release_rx) = mpsc::channel(); + Ok(NativeVaapiDecoder { + display, + planner, + session: None, + health: DecodeHealth { + // VAAPI has no per-picture decode-status query — there is no + // counterpart to Vulkan's `RESULT_STATUS_ONLY`, exactly as on + // D3D11VA. Saying so is what keeps "clean" and "unmeasured" + // distinguishable on the stats line: `failed` is structurally 0 + // here, and `DecodeHealth::note` enforces that rather than trusting + // this rung to never pass a verdict it cannot have. + status_queries: false, + ..DecodeHealth::default() + }, + recovery_request: false, + generation: 0, + release_tx, + release_rx, + stale_releases: 0, + }) + } + + pub(crate) fn name(&self) -> &'static str { + self.planner.name() + } + + pub(crate) fn health(&self) -> DecodeHealth { + self.health + } + + /// Drain the re-anchor request a concealed AU raised. + pub(crate) fn take_recovery_request(&mut self) -> bool { + std::mem::take(&mut self.recovery_request) + } + + /// Return surfaces the consumer has finished with to the free list. + fn drain_releases(&mut self) { + drain_releases_into( + &self.release_rx, + self.session.as_mut(), + &mut self.stale_releases, + ); + } + + /// Decode one access unit. + /// + /// `Ok(None)` means "no picture from this AU", and covers three different + /// things, deliberately none of them errors: + /// + /// * the planner output nothing yet (reordering, or the very first AUs); + /// * the picture was CONCEALED — an integrity warning says a reference was + /// substituted, so the output is released unshown, [`DecodeHealth::damaged`] + /// records it and a re-anchor is requested through the pump's one throttle. + /// Not an error, because three errors in a second demote the rung on exactly + /// the lossy links it exists to diagnose — libavcodec concealed the same event + /// silently and kept its job; + /// * an HEVC RASL picture skipped after an open-GOP join. `PlanError::RaslSkipped` + /// is the spec's own answer (8.1.3 NOTE) and must NEVER reach the reanchor + /// path — mapping it to an error would make every open-GOP join beg the host + /// for a keyframe it has no reason to send. + pub(crate) fn decode(&mut self, au: &[u8]) -> Result> { + self.drain_releases(); + let result = match self.planner { + Planner::H264(_) => self.decode_h264(au), + Planner::H265(_) => self.decode_h265(au), + // ⚠ An AV1 "access unit" is a TEMPORAL UNIT and may carry several + // frames; this arm is the only one whose planner returns a `Vec`. + Planner::Av1(_) => self.decode_av1(au), + }; + // ONE verdict per access unit, folded here and nowhere else. Damage is + // reported by the codec arm rather than counted inside it, so a failure + // AFTER a clean plan (a submission, an export) is a refusal and only a + // refusal — not a clean AU that also refused, which would reset the run + // counter a support engineer reads first. + match &result { + Ok((_, damaged)) => self.health.note(*damaged, false, 0), + Err(_) => self.health.note(false, true, 0), + } + result.map(|(frame, _)| frame) + } + + fn decode_h264(&mut self, au: &[u8]) -> Result<(Option, bool)> { + let plan = match &mut self.planner { + Planner::H264(p) => p.plan_au(au).map_err(|e| anyhow!("{e:?}"))?, + _ => unreachable!("dispatched on the planner's own arm"), + }; + let shape = shape_of( + plan.picture.coded_width, + plan.picture.coded_height, + plan.picture.display_crop, + plan.picture.max_dpb_frames, + plan.picture.chroma_format_idc, + 8 + plan.picture.bit_depth_luma_minus8, + )?; + let damaged = plan.warnings.iter().any(pf_vaadec::is_integrity_warning); + if !plan.warnings.is_empty() { + tracing::debug!(warnings = ?plan.warnings, damaged, "native VAAPI plan warnings"); + } + + let Self { + display, session, .. + } = self; + let s = ensure_session( + display, + session, + pf_vaadec::Codec::H264, + shape, + &mut self.generation, + )?; + let free = s + .free_surface() + .ok_or_else(|| anyhow!("surface pool exhausted ({} surfaces)", s.surfaces.len()))?; + let target = s.surfaces[free]; + let table = s.surface_table(); + let converted = pf_vaadec::plan_to_va(&plan, au, &mut s.slots, &table, target) + .map_err(|e| anyhow!("{e}"))?; + + bind_setup(s, plan.dpb.stored, Some(free)); + + let iq = Some(as_ptr(&converted.iq_matrix)); + let slices = one_record_each(&converted.slices, &converted.slice_data)?; + submit( + display, + s, + target, + as_ptr(&converted.pic_params), + iq, + &slices, + au, + )?; + + let display_size = (s.shape.display_width, s.shape.display_height); + let frame = finish( + display, + s, + &plan.dpb.outputs, + &plan.dpb.removed, + damaged, + plan.picture.is_idr, + colour_of(&plan.picture.colour), + display_size, + &mut self.recovery_request, + &self.release_tx, + )?; + Ok((frame, damaged)) + } + + fn decode_h265(&mut self, au: &[u8]) -> Result<(Option, bool)> { + let plan = match &mut self.planner { + Planner::H265(p) => match p.plan_au(au) { + Ok(plan) => plan, + // The contract pf-bitstream's h265 module docs record for this + // wiring: a skipped RASL picture is an Ok-skip, never an error and + // never a re-anchor. See [`Self::decode`]. + Err(pf_vaadec::PlanErrorH265::RaslSkipped { .. }) => return Ok((None, false)), + Err(e) => return Err(anyhow!("{e:?}")), + }, + _ => unreachable!("dispatched on the planner's own arm"), + }; + let shape = shape_of( + plan.picture.coded_width, + plan.picture.coded_height, + plan.picture.display_crop, + plan.picture.max_dpb_frames, + plan.picture.chroma_format_idc, + 8 + plan.picture.bit_depth_luma_minus8, + )?; + let damaged = plan + .warnings + .iter() + .any(pf_vaadec::is_integrity_warning_h265); + if !plan.warnings.is_empty() { + tracing::debug!(warnings = ?plan.warnings, damaged, "native VAAPI plan warnings"); + } + + let Self { + display, session, .. + } = self; + let s = ensure_session( + display, + session, + pf_vaadec::Codec::H265, + shape, + &mut self.generation, + )?; + let free = s + .free_surface() + .ok_or_else(|| anyhow!("surface pool exhausted ({} surfaces)", s.surfaces.len()))?; + let target = s.surfaces[free]; + let table = s.surface_table(); + let converted = pf_vaadec::plan_to_va_h265(&plan, au, &mut s.slots, &table, target) + .map_err(|e| anyhow!("{e}"))?; + + bind_setup(s, plan.dpb.stored, Some(free)); + + // The IQ matrix is submitted ONLY where the sequence codes scaling lists. + // Handing the driver an all-zero matrix on a "use the defaults" stream is + // not a harmless extra buffer: the driver must apply what it is given, every + // residual dequantises to zero, and the picture drifts to flat prediction. + // (M5's review caught exactly this on the DXVA rung, where the buffer was + // unconditional. The conversion answers `None` here so the rung cannot.) + let iq = converted.iq_matrix.as_ref().map(as_ptr); + let slices = one_record_each(&converted.slices, &converted.slice_data)?; + submit( + display, + s, + target, + as_ptr(&converted.pic_params), + iq, + &slices, + au, + )?; + + let display_size = (s.shape.display_width, s.shape.display_height); + let frame = finish( + display, + s, + &plan.dpb.outputs, + &plan.dpb.removed, + damaged, + plan.picture.is_idr, + colour_of(&plan.picture.colour), + display_size, + &mut self.recovery_request, + &self.release_tx, + )?; + Ok((frame, damaged)) + } + + /// One AV1 **temporal unit**: decode every frame in it, present at most one. + /// + /// This is the whole of what AV1 adds to this rung's contract, and it is the + /// SPEC's shape rather than an assumption about punktfunk hosts. A temporal unit + /// may carry several frame headers; the vendored 250-packet conformance vector + /// decodes **274 frames** and shows 250, so 24 of its units carry a hidden + /// picture (an alt-ref later frames predict from) ahead of the one that + /// displays. Those hidden frames must be DECODED — they are references — and + /// must never reach the presenter, which would show each of them for a frame and + /// stutter every time. + /// + /// AV1 admits at most one shown frame per temporal unit, so "the last shown + /// frame wins" cannot silently drop a picture. + /// + /// # Concealment is per UNIT here, per picture on the other two codecs + /// + /// A damaged frame is still CONVERTED and still SUBMITTED, exactly as the H.264 + /// and H.265 arms above do it. Converting is what assigns its ledger slot, and + /// skipping that would desynchronise this rung's slot map from the planner's store + /// and turn every later reference to it into a hard `Err` — a demotion streak + /// earned by one lost packet. Submitting is what puts a decoded picture in the + /// surface, which matters because a hidden frame's surface is a REFERENCE for + /// later frames and can still be exported by a later `show_existing_frame`; a + /// surface the driver never wrote is uninitialised video memory, not a stale + /// picture. + /// + /// What concealment does instead is withhold the DISPLAY: nothing from the unit + /// is presented, because a shown frame that predicts from a concealed reference in + /// the same unit is not fit to display either, and the unit is the smallest thing + /// this rung can honestly drop. + /// + /// ⚠ Submitting a frame whose references were lost needs one thing from the + /// conversion, and `va_dec_av1.h:352` is where it comes from: *"Driver is not + /// responsible to validate reference frames' id … If missing frame is identified, + /// application may choose to perform error recovery by pointing problematic index + /// to an alternative frame buffer."* So `plan_to_va_av1` points every empty + /// `ref_frame_map` entry at a live surface and reports which + /// (`DecodePlanVaAv1::substituted_refs`); no `VA_INVALID_ID` reaches a driver that + /// says it will not check. + /// + /// The one frame that is NOT submitted is the one with nothing to submit: an + /// access unit whose tile groups were lost. That refusal is handled in + /// [`Self::frame_av1`] and binds no surface at all, so its picture can be neither + /// exported nor predicted from. + fn decode_av1(&mut self, au: &[u8]) -> Result<(Option, bool)> { + let plans = match &mut self.planner { + Planner::Av1(p) => p.plan_au(au).map_err(|e| anyhow!("{e}"))?, + _ => unreachable!("dispatched on the planner's own arm"), + }; + let mut shown = None; + let mut damaged_unit = false; + for plan in &plans { + let damaged = plan + .warnings + .iter() + .any(pf_vaadec::is_integrity_warning_av1); + damaged_unit |= damaged; + if !plan.warnings.is_empty() { + tracing::debug!(warnings = ?plan.warnings, damaged, "native VAAPI AV1 plan warnings"); + } + if let Some(frame) = self.frame_av1(au, plan, damaged)? { + shown = Some(frame); + } + } + if damaged_unit { + // A frame may already have been exported before a LATER frame of the + // same unit turned out to be damaged. Dropping it here is safe rather + // than merely tolerable: `DmabufFrame`'s guard closes its fds and returns + // the surface to the free list, which is exactly what an unshown picture + // should do. + drop(shown); + return Ok((None, true)); + } + Ok((shown, false)) + } + + /// One frame of a temporal unit: converted, submitted, and exported only if it is + /// the frame the unit displays and the unit is clean. + /// + /// `damaged` changes two things and neither of them is the submission. It decides + /// whether the picture may be SHOWN (through [`finish`]), and it decides how a + /// conversion refusal is answered: a lost tile group on an already-damaged plan is + /// concealed, the same refusal on a plan that arrived whole is a defect and stays + /// an error. + fn frame_av1( + &mut self, + au: &[u8], + plan: &pf_vaadec::AuPlanAv1, + damaged: bool, + ) -> Result> { + // `show_existing_frame` decodes nothing at all: it re-displays a picture some + // earlier hidden frame put in a reference slot. + if plan.dpb.stored.is_none() { + return self.show_existing_av1(plan, damaged); + } + let shape = shape_of_av1(plan); + let Self { + display, session, .. + } = self; + let s = ensure_session( + display, + session, + pf_vaadec::Codec::Av1, + shape, + &mut self.generation, + )?; + let free = s + .free_surface() + .ok_or_else(|| anyhow!("surface pool exhausted ({} surfaces)", s.surfaces.len()))?; + let target = s.surfaces[free]; + let table = s.surface_table(); + let converted = match pf_vaadec::plan_to_va_av1(plan, au, &mut s.slots, &table, target) { + Ok(converted) => converted, + Err(e) => { + // ⚠ The ledger has already been mutated — the conversion assigns the + // setup slot before its tile walk, so that a refusal here does not + // desynchronise it from the planner's store — and the caller's half of + // that contract is to bind NOTHING (see [`bind_setup`]). Unconditional, + // because it is also correct for the refusals that fire before any + // mutation: there is no slot to clear and no surface to bind either + // way. + bind_setup(s, plan.dpb.stored, None); + // A lost tile group on a plan the planner ALREADY called damaged is + // concealment, not a defect: the access unit simply did not carry the + // tiles its frame header announced, which is what one dropped packet + // looks like. Answering with an error instead would burn the demotion + // streak on exactly the lossy links this rung exists to diagnose. The + // frame is not submitted (there is nothing to submit), its surface is + // bound to nothing, and the unit is dropped by the caller. + if damaged && e.lost_tiles() { + tracing::debug!( + error = %e, + id = plan.dpb.stored, + "native VAAPI AV1: concealed a truncated access unit" + ); + finish( + display, + s, + &plan.dpb.outputs, + &plan.dpb.removed, + true, + plan.picture.is_key, + colour_of(&plan.picture.colour), + // Unread — `finish` returns before it looks at the display + // region when `damaged` — but written the same way as the + // submitting path below, so the two cannot drift apart. + ( + plan.picture.render_width.min(plan.picture.upscaled_width), + plan.picture.render_height.min(plan.picture.frame_height), + ), + &mut self.recovery_request, + &self.release_tx, + )?; + return Ok(None); + } + return Err(anyhow!("{e}")); + } + }; + + // `bind_setup` asks the LEDGER where the picture landed rather than being + // told, which is what makes it right for the AV1 frame that refreshes no + // slot: the conversion has already handed that slot back, so nothing binds + // the surface and only the pending-output claim keeps it out of the free + // list. (`DecodePlanVaAv1::setup_slot` is `None` there; it is not consulted + // here for exactly that reason.) + bind_setup(s, plan.dpb.stored, Some(free)); + + if converted.substituted_refs != 0 { + tracing::debug!( + slots = format_args!("{:#010b}", converted.substituted_refs), + "native VAAPI AV1: concealed reference slot(s) with a live surface" + ); + } + let mut slices: Vec = Vec::with_capacity(converted.tile_groups.len()); + for group in &converted.tile_groups { + slices.push(SlicePair { + params: group.tiles.as_ptr().cast::(), + record_size: size_of::(), + // ⚠ Several records in ONE buffer — the only place this rung does + // that, and what libavcodec's `vaapi_av1.c` does per tile group. + records: group.tiles.len(), + data: group.data.clone(), + }); + } + submit( + display, + s, + target, + as_ptr(&converted.pic_params), + // AV1 transmits no quantisation matrix: its matrices are SELECTED by + // index out of tables the decoder already holds. + None, + &slices, + au, + )?; + + // AV1's display region is the RENDER size, not the coded size — and it is a + // per-FRAME value, so it cannot live in the session shape the way a + // conformance window does. + // + // ⚠ CLAMPED to the decoded picture. AV1 5.9.6 puts no upper bound on the + // render size — a stream may legally ask to be shown at more than it coded — + // and an unclamped crop would hand the presenter a region larger than the + // surface. The same clamp is in the Vulkan and D3D11 rungs. + // + // ⚠ Treated as a CROP, which is what both other native rungs do. libavcodec + // instead keeps the frame at `upscaled_width` x `frame_height` and expresses + // the render size as a sample aspect RATIO, so on a stream where the two + // differ this rung shows less picture than libavcodec would. No + // punktfunk host emits such a stream; the choice is here so the three native + // rungs answer alike, not because it is settled. + let display_size = ( + plan.picture.render_width.min(plan.picture.upscaled_width), + plan.picture.render_height.min(plan.picture.frame_height), + ); + let frame = finish( + display, + s, + &plan.dpb.outputs, + &plan.dpb.removed, + damaged, + plan.picture.is_key, + colour_of(&plan.picture.colour), + display_size, + &mut self.recovery_request, + &self.release_tx, + )?; + + // A frame that enters no reference slot AND displays nothing is dead the + // moment it is decoded, and it is the one picture nothing else ever retires: + // the planner cannot report it removed (it was never stored) and cannot + // output it, so its pending entry — and therefore its surface — is held for + // the session's whole life. Seventeen of them exhaust the pool. + // + // Legal AV1 syntax that no encoder emits, which is exactly why it is worth a + // line: a damaged or truncated header can parse to it, and the symptom would + // be a session that dies of "pool exhausted" some minutes later with nothing + // pointing back here. + if converted.setup_slot.is_none() && !plan.dpb.outputs.contains(&converted.setup_id) { + s.pending.retain(|(id, _)| *id != converted.setup_id); + } + Ok(frame) + } + + /// A `show_existing_frame` access unit: export a surface the pool already holds. + /// + /// No conversion and no submission — the picture was decoded by an earlier frame + /// of an earlier temporal unit and is still in [`Session::pending`], because a + /// hidden frame is never output when it decodes. [`finish`] resolves the output + /// id to its surface exactly as it does for any other picture, so this path needs + /// no per-surface facts table: the plan's own `picture` carries the SHOWN frame's + /// geometry and type, which the vendored parser restores from the reference + /// (`load_reference_frame` copies `ref_upscaled_width` / `ref_frame_height` / + /// `ref_render_*` / `ref_frame_type` into the display-only header). + /// + /// ⚠ **Untested.** The vendored conformance vector uses `show_existing_frame` + /// zero times — pf-bitstream's planner test asserts that count stays 0 — so + /// nothing in any gate reaches this function. + fn show_existing_av1( + &mut self, + plan: &pf_vaadec::AuPlanAv1, + damaged: bool, + ) -> Result> { + let Self { + display, session, .. + } = self; + // Nothing has decoded yet: the unit is already concealed (the planner + // reported `MissingShowExisting`) and there is no session to look in. + let Some(s) = session.as_mut() else { + return Ok(None); + }; + // Showing a KEY frame this way resets the whole reference store (AV1 7.20), + // so the plan's removals are real and this rung's ledger has to follow them — + // or the map fills up and the next assignment fails. No conversion runs on + // this path, so this is the only place they can be applied. + for &id in &plan.dpb.removed { + s.slots.release(id); + } + s.sync_slot_bindings(); + let display_size = ( + plan.picture.render_width.min(plan.picture.upscaled_width), + plan.picture.render_height.min(plan.picture.frame_height), + ); + finish( + display, + s, + &plan.dpb.outputs, + &plan.dpb.removed, + damaged, + plan.picture.is_key, + colour_of(&plan.picture.colour), + display_size, + &mut self.recovery_request, + &self.release_tx, + ) + } +} + +impl Drop for NativeVaapiDecoder { + fn drop(&mut self) { + if self.stale_releases > 0 { + // Not an error — a renegotiated session's frames come home to a pool + // that no longer exists — but a count worth seeing, because the only + // other thing that produces it is release bookkeeping gone wrong. + tracing::debug!( + count = self.stale_releases, + "native VAAPI: releases for retired surface pools" + ); + } + if let Some(s) = self.session.take() { + s.destroy(&self.display); + } + } +} + +// --------------------------------------------------------------------------- +// Shared decode plumbing (free functions: the two codec arms differ only in the +// types their conversions produce, and splitting the borrow of `self` here is +// what lets one implementation serve both) +// --------------------------------------------------------------------------- + +/// Apply the consumer's finished-with tokens to the pool's holds. +/// +/// A free function rather than a method so the rule can be tested without a +/// `Display`: it is pure bookkeeping, and the one thing it must get right — refusing +/// a token from a pool that no longer exists — is precisely what a test can check and +/// hardware cannot. +fn drain_releases_into( + rx: &mpsc::Receiver, + mut session: Option<&mut Session>, + stale: &mut u64, +) { + while let Ok(token) = rx.try_recv() { + let Some(s) = session.as_deref_mut() else { + *stale += 1; + continue; + }; + if token.generation != s.generation { + // A retired pool's surface. Its whole session is gone, so there is + // nothing to free — and freeing that index in the CURRENT pool would hand + // a live surface to the decoder as a spare. + *stale += 1; + continue; + } + match s.held.get_mut(token.surface) { + Some(h) => *h = false, + None => *stale += 1, + } + } +} + +/// A live struct as a `(pointer, size)` for `vaCreateBuffer`, which COPIES. +fn as_ptr(value: &T) -> (*const c_void, usize) { + ((value as *const T).cast::(), size_of::()) +} + +/// H.273 code points straight off the picture's ACTIVE SPS/VUI — per frame, never +/// latched, because the Windows host switches an HDR desktop to PQ/BT.2020 IN-BAND +/// with a new SPS while the Welcome still says SDR. +fn colour_of(c: &pf_vaadec::ColourDescription) -> ColorDesc { + ColorDesc { + primaries: c.colour_primaries, + transfer: c.transfer_characteristics, + matrix: c.matrix_coefficients, + full_range: c.video_full_range, + } +} + +/// Derive the session shape, refusing what the hand-off cannot express. +fn shape_of( + coded_width: u32, + coded_height: u32, + crop: pf_vaadec::DisplayCrop, + max_dpb_frames: usize, + chroma_format_idc: u8, + bit_depth: u8, +) -> Result { + // A non-zero conformance-window ORIGIN would have to shift every plane's offset, + // and nothing downstream carries one: the dmabuf planes are handed over with the + // driver's own offsets and the consumer samples from (0,0). Refused rather than + // cropped from the wrong corner — the same latent gap M5 flagged on the D3D11 + // rung, closed here instead of inherited. Our hosts emit origin (0,0); a stream + // that does not simply falls through to the next rung. + if crop.x != 0 || crop.y != 0 { + bail!( + "conformance window at ({}, {}) — this rung hands the surface over \ + uncropped and cannot express a non-zero origin", + crop.x, + crop.y + ); + } + Ok(StreamShape { + coded_width, + coded_height, + display_width: crop.width, + display_height: crop.height, + max_dpb_frames, + chroma_format_idc, + bit_depth, + }) +} + +/// The session shape one AV1 plan implies. +/// +/// ⚠ The pool is sized from the SEQUENCE header's **maximum** frame size, not this +/// frame's. AV1 lets every frame pick its own size up to that maximum without a key +/// frame, and sizing the session from the frame would rebuild the config, the +/// surface pool and the ledger — dropping every reference — the first time a stream +/// resized downward. libavcodec does the same (`set_context_with_sequence` calls +/// `ff_set_dimensions(avctx, seq->max_frame_width_minus_1 + 1, …)`). +/// +/// The display fields carry the same maximum rather than the render region, for the +/// same reason: the render size is a per-FRAME value and putting it here would make +/// every render-size change a renegotiation. What actually reaches the presenter is +/// [`finish`]'s `display` parameter. +/// +/// The DPB depth is a constant of the codec — `NUM_REF_FRAMES` — never anything a +/// sequence header says. There is no conformance window to refuse, so unlike +/// [`shape_of`] this cannot fail. +fn shape_of_av1(plan: &pf_vaadec::AuPlanAv1) -> StreamShape { + let coded_width = u32::from(plan.sequence.max_frame_width_minus_1) + 1; + let coded_height = u32::from(plan.sequence.max_frame_height_minus_1) + 1; + StreamShape { + coded_width, + coded_height, + display_width: coded_width, + display_height: coded_height, + max_dpb_frames: pf_vaadec::AV1_MAX_DPB_FRAMES, + chroma_format_idc: plan.picture.chroma_format_idc, + // AV1 codes ONE bit depth for all three planes, so there is no luma/chroma + // pair to reconcile the way H.264 and H.265 need. + bit_depth: plan.picture.bit_depth, + } +} + +/// The session for this shape, rebuilt whole if the stream renegotiated. +fn ensure_session<'a>( + d: &Display, + slot: &'a mut Option, + codec: pf_vaadec::Codec, + shape: StreamShape, + generation: &mut u64, +) -> Result<&'a mut Session> { + if slot.as_ref().is_some_and(|s| s.shape == shape) { + return Ok(slot.as_mut().expect("just matched")); + } + if let Some(old) = slot.take() { + tracing::info!( + from = ?old.shape, + to = ?shape, + "native VAAPI stream renegotiated — rebuilding the session" + ); + // Dropped BEFORE the replacement is built so the old pool's video memory is + // released first; a 4K pool is on the order of a hundred megabytes. + // + // Surfaces the CONSUMER still holds are destroyed here too, and that is + // sound: an exported PRIME fd holds its own reference on the underlying + // buffer object, and the presenter dup'd every fd it imported. The pixels + // outlive the VASurface — which is the whole mechanism zero-copy rests on. + old.destroy(d); + } + // A NEW generation, always — this is what makes those outstanding frames safe to + // let go of. Their release tokens name surface indices in a pool that no longer + // exists, and applying one to the new pool would mark a live surface free and + // hand it to the decoder as a spare. The bump is here, at the one place a pool is + // ever replaced, rather than at the call sites. + *generation += 1; + let mut built = Session::build(d, codec, shape)?; + built.generation = *generation; + Ok(slot.insert(built)) +} + +/// Record which surface holds the picture just planned — or that NOTHING does. +/// +/// The slot bindings are re-derived from the ledger FIRST — the conversion has +/// already applied this AU's removals, so a slot the planner released binds nothing +/// — and only then is the setup picture bound, by asking the ledger where it landed. +/// Asking rather than assuming is what handles the one awkward case: a non-reference +/// picture with no free frame buffer is stored and evicted inside a single plan, so +/// it holds NO slot when the conversion returns. Its surface is kept out of the free +/// list by `pending` instead, until it has been output. +/// +/// ⚠ `surface` is `None` on the AV1 refusal path, and that call is not optional. The +/// AV1 conversion assigns the ledger slot BEFORE its tile walk — deliberately, so a +/// lost tile group does not desynchronise the ledger from the planner's store forever +/// (`pf_vaadec::plan_to_va_av1`'s docs) — which means a refusal can leave a slot +/// re-assigned to a picture that never decoded while `slot_surface` still holds the +/// surface of whatever occupied that slot BEFORE. That is not a missing reference, it +/// is a WRONG one, and nothing downstream could tell. Binding `None` makes the slot +/// read back as `VA_INVALID_ID`, which the conversion then substitutes with a live +/// surface. Nothing is pushed to `pending` either: an undecoded surface must never be +/// exportable. +fn bind_setup(s: &mut Session, stored: Option, surface: Option) { + s.sync_slot_bindings(); + let Some(id) = stored else { return }; + if let Some(slot) = s.slots.slot_of(id) { + s.slot_surface[usize::from(slot)] = surface; + } + if let Some(surface) = surface { + s.pending.push((id, surface)); + } +} + +/// One slice-parameter (AV1: tile-parameter) buffer and the bitstream region its +/// records address. +/// +/// The two travel together because `vaRenderPicture` is what establishes which data +/// buffer a record's `slice_data_offset` is relative to — it is handed the pair. +struct SlicePair { + /// The record array. Borrowed from the caller's converted plan, which outlives + /// the `submit` call that reads it. + params: *const c_void, + /// ONE record's size. `vaCreateBuffer` takes the element size and the element + /// count separately and they are not interchangeable. + record_size: usize, + /// How many records this buffer carries: **1** for H.264 and H.265 — one slice, + /// one buffer, exactly as libavcodec sends them — and a whole tile group's worth + /// for AV1, which is the one codec libavcodec packs several records into a single + /// buffer for. + records: usize, + /// The bitstream those records address, in ACCESS-UNIT coordinates. + data: std::ops::Range, +} + +/// The H.264/H.265 shape of the above: one record per buffer, parallel to its data +/// range. +/// +/// ⚠ The length check is not ceremony, even though both conversions build the two +/// vectors in one loop today and cannot produce a mismatch. `zip` would answer a +/// future divergence by SILENTLY TRUNCATING — a picture submitted with some of its +/// slices, which decodes to a partial frame rather than to an error, and which no +/// gate here has hardware to catch. A refusal is the honest answer and costs one +/// comparison per access unit. +fn one_record_each(records: &[T], data: &[std::ops::Range]) -> Result> { + if records.len() != data.len() { + bail!( + "{} slice record(s) for {} data range(s) — the conversion's two halves \ + disagree", + records.len(), + data.len() + ); + } + Ok(records + .iter() + .zip(data) + .map(|(record, range)| SlicePair { + params: (record as *const T).cast::(), + record_size: size_of::(), + records: 1, + data: range.clone(), + }) + .collect()) +} + +/// One picture's buffers, in the order libavcodec's VAAPI path submits them: the +/// parameter buffers in one `vaRenderPicture`, then the interleaved +/// slice-parameter/slice-data pairs in another. Matching the path drivers are +/// validated against is worth more than any tidier arrangement. +fn submit( + d: &Display, + s: &Session, + target: VaSurfaceId, + pic: (*const c_void, usize), + iq: Option<(*const c_void, usize)>, + slices: &[SlicePair], + au: &[u8], +) -> Result<()> { + let mut params: Vec = Vec::with_capacity(2); + let mut slice_buffers: Vec = Vec::with_capacity(slices.len() * 2); + // A picture that was BEGUN must be ended even if a step in between failed, or + // the context stays mid-picture and every later `vaBeginPicture` fails on a + // stream that was otherwise recoverable. libavcodec's VAAPI path has the same + // `fail_with_picture` label for the same reason. + let mut begun = false; + // Every buffer created below must be destroyed whatever happens next — libva + // does not reclaim them at `vaEndPicture` (see `Display::destroy_buffers`). + let result = (|| -> Result<()> { + params.push( + d.create_buffer( + s.context, + pf_vaadec::va::VA_PICTURE_PARAMETER_BUFFER_TYPE, + pic.1, + 1, + pic.0, + ) + .context("picture parameter buffer")?, + ); + if let Some((ptr, size)) = iq { + params.push( + d.create_buffer( + s.context, + pf_vaadec::va::VA_IQ_MATRIX_BUFFER_TYPE, + size, + 1, + ptr, + ) + .context("IQ matrix buffer")?, + ); + } + for (n, pair) in slices.iter().enumerate() { + let range = pair.data.clone(); + let data = au.get(range.clone()).ok_or_else(|| { + anyhow!( + "slice {n}: range {range:?} is outside a {}-byte access unit", + au.len() + ) + })?; + if pair.records == 0 { + bail!("slice {n}: a parameter buffer with no records"); + } + slice_buffers.push( + d.create_buffer( + s.context, + pf_vaadec::va::VA_SLICE_PARAMETER_BUFFER_TYPE, + pair.record_size, + pair.records, + pair.params, + ) + .with_context(|| format!("slice {n} parameter buffer"))?, + ); + slice_buffers.push( + d.create_buffer( + s.context, + pf_vaadec::va::VA_SLICE_DATA_BUFFER_TYPE, + data.len(), + 1, + data.as_ptr().cast::(), + ) + .with_context(|| format!("slice {n} data buffer"))?, + ); + } + + // SAFETY: a live display, context and target surface; both buffer arrays are + // locals that outlive their calls and their counts match their lengths. + unsafe { + d.va.check( + "vaBeginPicture", + (d.va.begin_picture)(d.display, s.context, target), + )?; + begun = true; + d.va.check( + "vaRenderPicture(parameters)", + (d.va.render_picture)( + d.display, + s.context, + params.as_mut_ptr(), + params.len() as c_int, + ), + )?; + d.va.check( + "vaRenderPicture(slices)", + (d.va.render_picture)( + d.display, + s.context, + slice_buffers.as_mut_ptr(), + slice_buffers.len() as c_int, + ), + )?; + begun = false; + d.va.check("vaEndPicture", (d.va.end_picture)(d.display, s.context))?; + } + Ok(()) + })(); + if begun { + // SAFETY: a live display and context with a picture open; the status is + // deliberately discarded — the real failure is `result`, and reporting this + // one would replace the cause with its consequence. + unsafe { (d.va.end_picture)(d.display, s.context) }; + } + d.destroy_buffers(¶ms); + d.destroy_buffers(&slice_buffers); + result +} + +/// Turn this AU's OUTPUT list into at most one shipped frame. +/// +/// Display order, not decode order. `plan.dpb.outputs` is what the planner says is +/// ready to be shown and in what order, and the surface for each is looked up by +/// `PicId` — so a reordering stream presents correctly rather than in the order the +/// pictures happened to decode. (The native D3D11VA rung does present in decode +/// order; that is a known finding on a rung that blits its output away, and there +/// was no reason to inherit it here where the display-order queue costs a lookup.) +/// +/// Newest wins, which is the same rule the FFmpeg VAAPI rung applies inside its +/// receive loop: on a live stream a picture already superseded is not worth a frame +/// interval. Superseded outputs are released rather than exported. +/// +/// The retirement rule is `pf_vkdecode`'s `settle_dpb`, reimplemented here over this +/// rung's flat pending list rather than reasoned out again, because both halves of it +/// are easy to get wrong: +/// +/// * **`removed` retires a pending picture too.** A picture can leave the DPB without +/// ever being output (`no_output_of_prior_pics` at an IDR is the everyday case), and +/// a pending list that only shrinks on OUTPUT keeps its surface off the free list +/// for the rest of the session — a slow, silent walk into pool exhaustion. +/// * **An output naming no pending picture is a TRACE, not an error.** Ids planned +/// before this decoder existed, or dropped across a session rebuild, are +/// display-order gaps. +#[allow(clippy::too_many_arguments)] +fn finish( + d: &Display, + s: &mut Session, + outputs: &[u64], + removed: &[u64], + damaged: bool, + keyframe: bool, + color: ColorDesc, + // The DISPLAY region for this picture. A parameter rather than a read of + // `s.shape` because AV1's is per-FRAME: its render size may change without a key + // frame, so it cannot live in the shape that rebuilds the session. + display: (u32, u32), + recovery_request: &mut bool, + tx: &mpsc::Sender, +) -> Result> { + // A concealed picture is not shown: it was decoded from a substitute reference, + // so shipping it paints the substitution on screen. Nothing this AU output is + // shown, the pump is asked to re-anchor, and the caller records the damage. + let shown = if damaged { + None + } else { + outputs.last().copied() + }; + // OUTPUTS FIRST, and the shown one is taken out before anything else runs. + // A picture is normally output and removed by the SAME access unit — that is + // what bumping is — so retiring `removed` before claiming the frame would + // discard the very picture about to be displayed, on essentially every AU. + let claimed = shown.and_then(|id| { + let found = s.pending.iter().position(|(pid, _)| *pid == id); + if found.is_none() { + tracing::trace!(id, "output id without a pending picture"); + } + found.map(|index| s.pending.remove(index).1) + }); + for id in outputs { + if Some(*id) != shown { + s.pending.retain(|(pid, _)| pid != id); + } + } + // Whatever left the DPB is retired from the pending list whether or not it was + // ever output. Its SURFACE only becomes free if nothing else holds it — a + // reference still bound to a slot, or a frame the consumer has, stays put. + for id in removed { + s.pending.retain(|(pid, _)| pid != id); + } + if damaged { + *recovery_request = true; + return Ok(None); + } + let Some(surface_index) = claimed else { + return Ok(None); + }; + let surface = s.surfaces[surface_index]; + + // OWNED from here. `export` wraps the descriptor's fds the moment the call + // succeeds, so every refusal below closes them by dropping rather than by + // remembering to — an earlier draft leaked one fd per refused frame. + let (exported, fds) = export(d, surface)?; + if exported.fourcc != s.fourcc { + // The pool was created with an explicit pixel format; a surface exporting a + // different one means the driver silently substituted, and the consumer + // would import the wrong layout. + bail!( + "surface exported fourcc {:#010x}, the pool was created as {:#010x}", + exported.fourcc, + s.fourcc + ); + } + if exported.planes.len() < 2 { + bail!( + "a two-plane surface exported {} plane(s) — the chroma is missing", + exported.planes.len() + ); + } + + s.held[surface_index] = true; + let planes = exported + .planes + .iter() + .map(|p| DmabufPlane { + fd: p.fd, + offset: p.offset, + stride: p.stride, + }) + .collect(); + Ok(Some(DmabufFrame { + // The DISPLAY region. The surface is allocated at the coded size and is + // taller/wider than the picture; handing over the coded size would show the + // codec's granule padding. + width: display.0, + height: display.1, + fourcc: exported.fourcc, + modifier: exported.modifier, + planes, + color, + keyframe, + guard: DrmFrameGuard(VaFrameGuard { + _fds: fds, + tx: tx.clone(), + release: VaRelease { + surface: surface_index, + generation: s.generation, + }, + }), + })) +} + +/// Wait for the decode and export the surface as DRM-PRIME dmabufs. +/// +/// The `vaSyncSurface` is what makes the hand-off safe: VAAPI exposes no fence to +/// the importer, so the accepted contract on this path — and what libavcodec's own +/// VAAPI→DRM_PRIME mapping does — is to sync before the fds leave. It is a blocking +/// wait on the pump thread, which is worth naming: at 60 fps against decodes of a +/// millisecond or two it is slack, and the alternative is handing the presenter a +/// surface the GPU has not finished writing. +/// +/// Returns the flattened surface AND the fds it owns, together — so that from the +/// instant the export succeeds those fds are RAII-owned and every later refusal +/// closes them by dropping. `ExportedSurface::planes` still carries the raw fds, +/// borrowed from these: several planes routinely name one object, and each object's +/// fd must be closed exactly once. +fn export(d: &Display, surface: VaSurfaceId) -> Result<(pf_vaadec::ExportedSurface, Vec)> { + // SAFETY: a live display and a surface from its own pool. + d.va.check("vaSyncSurface", unsafe { + (d.va.sync_surface)(d.display, surface) + })?; + + let mut desc = pf_vaadec::VaDrmPrimeSurfaceDescriptor::zeroed(); + // SAFETY: a live display and surface; `desc` is a local of exactly the layout + // `VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2` writes (measured by + // `pf-vaadec/layout-probe.c` and compile-asserted), and it outlives the call. + d.va.check("vaExportSurfaceHandle", unsafe { + (d.va.export_surface_handle)( + d.display, + surface, + VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, + pf_vaadec::VA_EXPORT_SURFACE_SEPARATE_LAYERS | pf_vaadec::VA_EXPORT_SURFACE_READ_ONLY, + (&mut desc as *mut pf_vaadec::VaDrmPrimeSurfaceDescriptor).cast::(), + ) + })?; + + match pf_vaadec::flatten(&desc) { + Ok(exported) => { + let fds = exported + .object_fds + .iter() + // SAFETY: each fd came out of a successful `vaExportSurfaceHandle` + // and is owned by this process exactly once. `flatten` lists one per + // OBJECT, so no fd is wrapped twice even where planes share it. + .map(|fd| unsafe { OwnedFd::from_raw_fd(*fd) }) + .collect(); + Ok((exported, fds)) + } + Err(e) => { + // The export SUCCEEDED, so its fds belong to this process even though the + // descriptor cannot be read as a surface. Every writable slot is swept + // rather than the first `num_objects` — a refusal for a bogus + // `num_objects` is exactly the case where that count cannot be trusted to + // bound anything. + for object in &desc.objects { + if object.fd >= 0 { + // SAFETY: an fd this process owns from the successful export; + // wrapping it in an `OwnedFd` that immediately drops closes it once. + drop(unsafe { OwnedFd::from_raw_fd(object.fd) }); + } + } + Err(anyhow!("{e}")) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A session whose libva handles are never used — every field exercised below is + /// plain bookkeeping, which is exactly why this rule can be tested at all without + /// a GPU. The pool is deliberately small so an exhausted free list is reachable. + fn session(surfaces: usize, slots: usize) -> Session { + Session { + shape: StreamShape { + coded_width: 64, + coded_height: 64, + display_width: 64, + display_height: 64, + max_dpb_frames: slots - 1, + chroma_format_idc: 1, + bit_depth: 8, + }, + config: VA_INVALID_ID, + context: VA_INVALID_ID, + surfaces: (0..surfaces as u32).map(|i| 0x100 + i).collect(), + held: vec![false; surfaces], + slot_surface: vec![None; slots], + pending: Vec::new(), + slots: pf_vaadec::SlotMap::new(slots - 1), + fourcc: pf_vaadec::VA_FOURCC_NV12, + generation: 1, + } + } + + /// The whole rule, in one test: a surface is free only when NOTHING claims it, + /// and the three claims end at different moments. + #[test] + fn a_surface_is_free_only_when_no_slot_no_output_and_no_consumer_claims_it() { + let mut s = session(4, 3); + assert_eq!( + s.free_surface(), + Some(0), + "a fresh pool starts at the front" + ); + + // 0: a live DPB reference. 1: decoded, still owing an output. 2: on screen. + s.slot_surface[0] = Some(0); + s.pending.push((7, 1)); + s.held[2] = true; + assert_eq!( + s.free_surface(), + Some(3), + "the first three are each claimed a different way" + ); + + // Losing ONE claim is not enough when another still stands. + s.held[3] = true; + s.slot_surface[1] = Some(1); + assert_eq!( + s.free_surface(), + None, + "an exhausted pool must say so rather than hand out a claimed surface" + ); + + // Surface 1 is both slot-bound and pending: releasing only the output keeps + // it out, and only when the slot goes too does it come back. + s.pending.clear(); + assert_eq!(s.free_surface(), None); + s.slot_surface[1] = None; + assert_eq!(s.free_surface(), Some(1)); + } + + /// The consumer's release is what ends the third claim — and it must be matched + /// to the generation that issued it, or a renegotiation hands a live surface out. + #[test] + fn a_release_from_a_retired_pool_never_frees_a_surface_in_the_new_one() { + let mut s = session(4, 3); + s.held[2] = true; + let (tx, rx) = mpsc::channel(); + let mut stale = 0u64; + + // A token from the pool that was retired before this one. Surface index 2 + // exists in BOTH pools, which is what makes this dangerous: the index is + // valid, and only the generation says it means a different surface. + tx.send(VaRelease { + surface: 2, + generation: 0, + }) + .expect("the receiver is alive"); + drain_releases_into(&rx, Some(&mut s), &mut stale); + assert!( + s.held[2], + "a stale generation must not clear a hold in the CURRENT pool" + ); + assert_eq!(stale, 1, "and it must be counted, not silent"); + + // The matching generation does free it. + tx.send(VaRelease { + surface: 2, + generation: 1, + }) + .expect("the receiver is alive"); + drain_releases_into(&rx, Some(&mut s), &mut stale); + assert!(!s.held[2]); + assert_eq!(stale, 1); + + // An index the pool does not have is counted, never a panic. + tx.send(VaRelease { + surface: 99, + generation: 1, + }) + .expect("the receiver is alive"); + drain_releases_into(&rx, Some(&mut s), &mut stale); + assert_eq!(stale, 2); + } + + /// The bindings follow the ledger: a slot the planner released binds nothing, + /// and the surface it held is only free if nothing else claims it. + #[test] + fn syncing_bindings_drops_the_slots_the_ledger_no_longer_holds() { + let mut s = session(4, 3); + s.slot_surface[0] = Some(0); + s.slot_surface[1] = Some(1); + s.slots.assign(11).expect("a free slot"); + s.sync_slot_bindings(); + assert_eq!( + s.slot_surface, + vec![Some(0), None, None], + "slot 0 is held by picture 11; slot 1's picture is gone" + ); + } + + /// A picture the conversion REFUSED binds no surface — so nothing can show it and + /// nothing can predict from it. + /// + /// Both halves matter and they fail differently. The AV1 conversion assigns its + /// ledger slot before the tile walk, so a refusal leaves a slot re-assigned to a + /// picture that never decoded; leaving the slot's PREVIOUS binding in place would + /// hand the next frame a real, decoded, WRONG picture, which no later check could + /// notice. And a `pending` entry for it would let a later `show_existing_frame` + /// claim the surface and ship it — a surface the driver never wrote, which is + /// uninitialised video memory rather than a stale frame. + #[test] + fn a_refused_picture_binds_nothing_and_can_never_be_exported() { + let mut s = session(4, 3); + + // Picture 11 decoded into surface 0 and took slot 0. + s.slots.assign(11).expect("a free slot"); + bind_setup(&mut s, Some(11), Some(0)); + assert_eq!(s.slot_surface[0], Some(0)); + assert_eq!(s.surface_table()[0], s.surfaces[0]); + assert_eq!(s.pending, vec![(11, 0)]); + + // Picture 12's access unit lost its tile groups. The conversion released 11, + // handed 12 the slot it just gave back — the routine case, not a contrived one + // — and then refused. + s.slots.release(11); + assert_eq!(s.slots.assign(12).expect("the slot 11 gave back"), 0); + bind_setup(&mut s, Some(12), None); + + assert_eq!( + s.slot_surface[0], None, + "the slot must not keep picture 11's surface: picture 12 never decoded, \ + and a reference to 12 that reads 11 is a wrong picture, not a missing one" + ); + assert_eq!( + s.surface_table()[0], + VA_INVALID_ID, + "and the table the conversion reads must say so, so it can substitute" + ); + assert!( + !s.pending.iter().any(|(id, _)| *id == 12), + "an undecoded picture owes no output — a pending entry is what would let \ + a later show_existing_frame export a surface the driver never wrote" + ); + + // The slot is still LIVE in the ledger, which is the whole point of the + // conversion mutating before it refuses: the next frame resolves picture 12 + // rather than hard-erroring on it. + assert_eq!(s.slots.slot_of(12), Some(0)); + } + + /// A conformance window with a non-zero ORIGIN is refused, not cropped from the + /// wrong corner: nothing downstream carries an origin. + #[test] + fn a_non_zero_crop_origin_is_refused() { + let ok = shape_of( + 1920, + 1088, + pf_vaadec::DisplayCrop { + x: 0, + y: 0, + width: 1920, + height: 1080, + }, + 4, + 1, + 8, + ) + .expect("the ordinary 1088-coded 1080 picture"); + assert_eq!((ok.display_width, ok.display_height), (1920, 1080)); + assert_eq!((ok.coded_width, ok.coded_height), (1920, 1088)); + + assert!(shape_of( + 1920, + 1088, + pf_vaadec::DisplayCrop { + x: 8, + y: 0, + width: 1912, + height: 1080, + }, + 4, + 1, + 8, + ) + .is_err()); + } + + /// One synthetic AV1 plan: a sequence that permits `max` and a frame that codes + /// `frame`, so the two can be told apart. + fn av1_plan(max: (u16, u16), frame: (u32, u32), render: (u32, u32)) -> pf_vaadec::AuPlanAv1 { + let sequence = pf_vaadec::ParsedSequenceHeaderAv1 { + max_frame_width_minus_1: max.0 - 1, + max_frame_height_minus_1: max.1 - 1, + ..Default::default() + }; + pf_vaadec::AuPlanAv1 { + picture: pf_vaadec::PicturePlanAv1 { + frame_type: pf_vaadec::FrameTypeAv1::KeyFrame, + is_key: true, + show_frame: true, + showable_frame: false, + order_hint: 0, + upscaled_width: frame.0, + frame_width: frame.0, + frame_height: frame.1, + render_width: render.0, + render_height: render.1, + bit_depth: 8, + chroma_format_idc: 1, + colour: pf_vaadec::ColourDescription { + colour_primaries: 1, + transfer_characteristics: 1, + matrix_coefficients: 1, + video_full_range: false, + }, + }, + tiles: Vec::new(), + refs: [None; 7], + dpb: pf_vaadec::DpbUpdateAv1::default(), + dpb_refs: Vec::new(), + warnings: Vec::new(), + sequence: std::rc::Rc::new(sequence), + header: std::rc::Rc::new(pf_vaadec::ParsedFrameHeaderAv1::default()), + } + } + + /// An AV1 session is sized from the SEQUENCE, never from the frame — and its DPB + /// depth is the codec's constant. + /// + /// Both halves are the difference between a stream that survives a mid-GOP resize + /// and one that rebuilds its pool, drops every reference and conceals its way back + /// to a keyframe. AV1 permits a frame to code any size up to the sequence maximum + /// with no key frame in sight, so a shape derived from the frame changes when + /// nothing renegotiated. + #[test] + fn an_av1_session_is_sized_from_the_sequence_maximum_not_the_frame() { + let big = shape_of_av1(&av1_plan((1920, 1080), (1920, 1080), (1920, 1080))); + assert_eq!((big.coded_width, big.coded_height), (1920, 1080)); + assert_eq!( + big.max_dpb_frames, 8, + "NUM_REF_FRAMES, not a stream property" + ); + + // The same sequence, a frame coded smaller and shown smaller still. Neither + // may move the shape, or this is a renegotiation. + let small = shape_of_av1(&av1_plan((1920, 1080), (1280, 720), (960, 540))); + assert_eq!( + small, big, + "a frame that resized itself must not rebuild the session" + ); + + // A genuinely different sequence does move it. + let other = shape_of_av1(&av1_plan((1280, 720), (1280, 720), (1280, 720))); + assert_ne!(other, big); + } + + /// The render size reaches the presenter CLAMPED to the decoded picture. + /// + /// AV1 5.9.6 puts no upper bound on the render size, so a stream may legally ask + /// to be shown at more than it coded; handing that to the presenter as a crop + /// would address rows the surface does not have. This restates the clamp in + /// `frame_av1`, which cannot itself be reached without a device. + #[test] + fn an_oversized_render_region_is_clamped_to_the_decoded_picture() { + let plan = av1_plan((1920, 1080), (1280, 720), (4096, 4096)); + let display = ( + plan.picture.render_width.min(plan.picture.upscaled_width), + plan.picture.render_height.min(plan.picture.frame_height), + ); + assert_eq!(display, (1280, 720)); + + let ordinary = av1_plan((1920, 1080), (1920, 1088), (1920, 1080)); + let display = ( + ordinary + .picture + .render_width + .min(ordinary.picture.upscaled_width), + ordinary + .picture + .render_height + .min(ordinary.picture.frame_height), + ); + assert_eq!(display, (1920, 1080), "the ordinary crop still crops"); + } + + /// H.264 and H.265 send ONE record per parameter buffer; AV1 is the exception. + /// + /// `vaCreateBuffer` takes an element size and an element count, and getting the + /// pair backwards is a driver reading `size` records of `count` bytes. This pins + /// the side every codec but AV1 is on. + #[test] + fn a_slice_pair_carries_one_record_unless_av1_says_otherwise() { + let records = [7u32, 8, 9]; + let ranges = vec![0..4, 4..8, 8..12]; + let pairs = one_record_each(&records, &ranges).expect("parallel lengths"); + assert_eq!(pairs.len(), 3); + for pair in &pairs { + assert_eq!(pair.records, 1); + assert_eq!(pair.record_size, size_of::()); + } + assert_eq!(pairs[1].data, 4..8); + + // A record without its data range REFUSES. `zip` would drop it silently and + // submit a picture missing a slice, which decodes rather than fails. + assert!(one_record_each(&records, &ranges[..2]).is_err()); + assert!(one_record_each(&records[..1], &ranges).is_err()); + } + + /// A shape this rung cannot decode is refused BEFORE libva is even loaded. + /// + /// The ordering is the point, not the refusal. M3 WP-2's review caught the + /// opposite arrangement on the Vulkan rung: a backend that accepts a session and + /// then refuses its first access unit has already cost the ladder its + /// fall-through — the refusal arrives as a decode error, burns the demotion + /// streak, and lands the session several rungs lower than it would have been. + /// Asserting on the MESSAGE is what pins the order: this test passes on a machine + /// with libva and on one without, and only stays passing while the profile probe + /// comes first. + #[test] + fn a_shape_with_no_profile_is_refused_before_libva_is_loaded() { + let e = NativeVaapiDecoder::new( + pf_vaadec::Codec::H264, + StreamFormat { + chroma_format_idc: 3, + bit_depth: 8, + }, + ) + .err() + .expect("4:4:4 H.264 has no VAAPI profile in this rung's envelope"); + let text = format!("{e:#}"); + assert!( + text.contains("profile"), + "the refusal must name the stream shape, not whatever libva said: {text}" + ); + } + + /// On-glass probe: resolve every entry point against the REAL libva on this + /// machine, then say what each render node does. + /// + /// This is the one thing no gate can check. `dlsym` takes a STRING: a mistyped + /// entry point compiles, clippies and unit-tests perfectly and fails only on a + /// machine with libva — so the 19 names are worth exercising once against a real + /// runtime, and this test is how. It is also the honest report of a box's VAAPI + /// situation: a node that will not initialise is a legitimate outcome (NVIDIA has + /// no usable VAAPI driver), printed rather than failed, because the rung's claim + /// is that such a box REFUSES CLEANLY and lets the ladder fall through. + /// + /// `cargo test -p pf-client-core --lib probe_this_machines_libva -- --ignored --nocapture` + #[test] + #[ignore = "needs a machine with a libva runtime"] + fn probe_this_machines_libva() { + let va = match Libva::load() { + Ok(va) => { + eprintln!("libva: every entry point resolved"); + va + } + Err(e) => { + eprintln!("libva: NOT LOADED — {e:#}"); + eprintln!("(this is the clean-refusal path; the ladder falls through here)"); + return; + } + }; + + let mut nodes: Vec = std::fs::read_dir("/dev/dri") + .expect("/dev/dri") + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .filter(|p| { + p.file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.starts_with("renderD")) + }) + .collect(); + nodes.sort(); + eprintln!("render nodes: {nodes:?}"); + + let mut opened = 0; + for node in &nodes { + let path = node.to_string_lossy().into_owned(); + match Display::probe(&va, &path) { + Ok((display, fd, version)) => { + opened += 1; + eprintln!(" {path}: VA-API {}.{}", version.0, version.1); + let d = Display { + va: Libva::load().expect("libva loaded once already"), + display, + node: Some(fd), + path: path.clone(), + version, + }; + for (name, profile) in [ + ("H.264 High", pf_vaadec::config::VA_PROFILE_H264_HIGH), + ("HEVC Main", pf_vaadec::config::VA_PROFILE_HEVC_MAIN), + ("HEVC Main 10", pf_vaadec::config::VA_PROFILE_HEVC_MAIN10), + ] { + match d.require_entrypoint(profile) { + Ok(()) => eprintln!(" {name}: VLD decode"), + Err(e) => eprintln!(" {name}: no ({e})"), + } + } + } + Err(e) => eprintln!(" {path}: {e:#}"), + } + } + eprintln!( + "{opened} of {} node(s) initialised a VAAPI display", + nodes.len() + ); + } +} diff --git a/crates/pf-client-core/src/video_vk_native.rs b/crates/pf-client-core/src/video_vk_native.rs new file mode 100644 index 00000000..2d1cd6bb --- /dev/null +++ b/crates/pf-client-core/src/video_vk_native.rs @@ -0,0 +1,2415 @@ +//! Native Vulkan Video decode backend (WP-C of the native-decode program, widened to +//! HEVC by M3 WP-2 and to AV1 by M7): pf-vkdecode's +//! [`VkH264Decoder`]/[`VkH265Decoder`]/[`VkAv1Decoder`] running on the PRESENTER's own +//! VkDevice — the same zero-copy shape the FFmpeg-Vulkan backend had, with no FFmpeg in +//! the path. Auto's TOP rung on both desktop OSes, for ALL THREE codecs — each +//! leg has bit-exact parity against libavcodec (H.264/H.265 on three drivers plus a +//! 92-minute soak, M2/M3; AV1 250/250 on an RTX 5070 Ti, M7 — `video`'s evidence table +//! holds the record) — also pinnable via `PUNKTFUNK_DECODER=native-vulkan`; +//! `video::native_vulkan_gate` is the admission either way, and a failure falls through +//! to the rung below (the platform's own native rung). +//! +//! **Codec dispatch:** the negotiated codec picks the decoder ONCE, at construction +//! ([`Codec`]) — H.264, H.265 or AV1, the three codecs pf-vkdecode speaks. The +//! negotiated picture SHAPE (chroma format + bit depth) is checked there too, against +//! the device: an H.265 or AV1 session this GPU has no decode format for is refused at +//! construction, where the ladder simply walks on to the next rung, rather than at the +//! first AU, where the only exit is an error streak PAST that rung +//! ([`NativeVulkanDecoder::new`]). Nothing below the codec enum is per-codec: the +//! shipped-frame ledger, the release tokens, the +//! decode-status reads, the timeline waits and the teardown drain are shared, because +//! all three decoders deliver the identical [`DecodedVkFrame`] contract (same pool/slot +//! lifecycle, same `value + 1` write-back, same query slots, same generations). +//! Forking that machinery per codec would fork the one part of this backend hardware +//! has already proven. +//! +//! **One AU, several FRAMES (AV1 only).** An AV1 access unit is a TEMPORAL UNIT and may +//! carry more than one frame — the vendored conformance vector puts 274 frames in 250 +//! units. [`VkAv1Decoder::decode`] walks them all internally and hands back the first +//! picture the planner declared DISPLAYABLE; the rest come out of `take_ready`, which +//! this backend already drains for H.265's burst output. Two AV1 facts make that walk +//! invisible from here, and both are the decoder's doing rather than this module's: +//! a HIDDEN frame (`show_frame = 0`) is decoded but never declared an output, so it +//! never enters `take_ready` and can never be shipped; and a `show_existing_frame` +//! (`dpb.stored == None`) decodes nothing at all and merely declares an +//! already-decoded picture displayable. So the contract this backend keeps is +//! unchanged — ONE access unit in, at most one displayable frame out. +//! +//! That contract is the SPEC's, not an assumption about punktfunk hosts: AV1 admits +//! exactly one shown frame per temporal unit, and the 24 two-frame units of the +//! vendored vector are a hidden ALTREF plus the frame that shows — one output +//! between them (`pf_bitstream::av1`'s conformance golden pins `shown = 250` across +//! 250 units, with `show_existing = 0`). [`NativeVulkanDecoder::decode`]'s +//! deliverable bound is therefore defence in depth against a stream that is NOT +//! that — a non-conformant encoder, or a scalable stream whose temporal unit carries +//! several operating points — and not the routine case it would be if a +//! `show_existing_frame` could ride alongside a shown frame. It cannot. +//! +//! **A skipped RASL picture is NOT a decode error.** An HEVC stream joined at a CRA +//! carries leading pictures whose references precede the join; the spec's own answer +//! (8.1.3 NOTE) is to decode and output nothing for them. [`VkH265Decoder::decode`] +//! implements exactly that: `h265::PlanError::RaslSkipped` never becomes a +//! `VkDecodeError`, so the AU comes back as `Ok` with whatever was ALREADY +//! display-ready (usually `None`) and with the warning ledger cleared. This backend +//! must therefore treat `Ok(None)` as "no picture this AU" and nothing more — no +//! release-unshown, no re-anchor request, no error. Mapping it to an error would make +//! every open-GOP join beg the host for a keyframe it has no reason to send. (Dead in +//! the field today — punktfunk hosts emit IDR-only re-entry points — but it is the +//! contract pf-bitstream's `h265` module docs record for this wiring.) +//! +//! AV1's post-failure wait is NOT that shape, and the difference is deliberate. +//! After a failed frame the decoder empties its own slot ledger and skips every +//! frame until the next key frame (`VkAv1Decoder::awaiting_key`), because the +//! planner's eight-slot store still believes the flushed pictures are resident. But +//! a temporal unit in which every frame was skipped comes back as an ERROR +//! (`VkDecodeError::AwaitingKeyAv1`), once per access unit — exactly what H.264 and +//! H.265 answer for the same wait through their planners' `PlanError::AwaitingIdr`, +//! and for a reason this module owns: an `Ok(None)` with an empty warning ledger is +//! read here as a CLEAN access unit, and a clean AU clears `video.rs`'s demotion +//! streak. A rung whose every key frame fails would then never demote — one error +//! per key frame, zeroed by the skipped frames between them — and the `!delivered` +//! fall-through to the rung below, the documented backstop for a level above +//! `maxLevelIdc`, a sequence header disagreeing with the Welcome and (AV1 only) +//! film grain, would be unreachable. All three codecs demote identically here. +//! +//! **Queue lock:** pf-vkdecode submits on queue 0 of the decode family +//! ([`DECODE_QUEUE_INDEX`] — the presenter creates exactly one queue per family). When +//! the decode family IS the presenter's graphics family, that is the very `VkQueue` the +//! presenter/Skia/overlay submit and present on, so every decode submit must hold the +//! device's shared [`video::QueueLock`] (`vkQueueSubmit` external sync — the 2026-07-09 +//! `VK_ERROR_DEVICE_LOST` class). When the families differ, the decode queue has exactly +//! one submitter (this backend, on the pump thread) and locking would serialize decode +//! against present for nothing — [`submit_queues_collide`] is the whole decision. (The +//! FFmpeg path locked on every family only because `lock_queue` was one callback pair for +//! the whole device; the collision the lock exists to prevent is the shared-queue one.) +//! +//! **Release lifecycle** (decode → present → retire → release): each delivered frame +//! ships as a [`NativeVkFrame`] whose [`NativeReleaseGuard`] sends a token (seq + +//! generation) into this backend's channel on drop. The presenter drops the frame only +//! after the sampling submission's fence has been waited (its retired-frame slot), so a +//! returned token proves the GPU is done with the image; a frame dropped UNPRESENTED +//! (newest-wins displacement, post-demotion drain) releases through the same drop. The +//! backend drains the channel at every `decode` entry and calls +//! [`Codec::release_frame`] — but only once the frame's decode-status query has +//! also been read (the slot stays pinned meanwhile, which is what makes re-polling the +//! query safe: an unreleased slot can never be recycled under the poll). +//! +//! **Status queries:** every decode op carries a `RESULT_STATUS_ONLY` query — +//! [`Codec::poll_status`], read non-blockingly here at each decode entry. A +//! `Failed` verdict is driver-reported decode corruption, the class libavcodec's +//! `vulkan_decode.c` (`nb_queries = 0`) architecturally cannot see — the Xbox Ally X +//! field case. It surfaces as an `Err` from the CURRENT `decode_frame` call so the +//! existing streak/reanchor machinery fires exactly as it did for libavcodec errors. +//! +//! **The recovery policy** (M4) — what a damaged stream ASKS for, and why it cannot +//! storm. There are two kinds of damage and they are answered differently: +//! +//! - **Concealment** (the plan needed a substitute for something lost: an integrity +//! warning). The AU's output is released UNSHOWN, [`DecodeHealth`] records it, and +//! `decode` answers `Ok(None)` with [`NativeVulkanDecoder::take_recovery_request`] +//! raised. `video::Decoder` turns that into its ordinary `want_keyframe`, which the +//! pump drains, arms the freeze on, and asks through the ONE ~100 ms recovery +//! throttle every other ask already shares (`session.rs`'s `last_kf_req`: frame-gap +//! RFI, dropped-climb, no-output streak, overdue backstop, decoder recovery). It is +//! deliberately NOT an `Err`: an error ticks the demotion streak, and three of them +//! in a second would demote the native rung on exactly the lossy links it exists to +//! diagnose — libavcodec concealed the same event silently and kept its job. +//! - **A driver `Failed` verdict** (and its query-less twin, a decode status that +//! could not be established at all — [`StatusVerdicts`]). That is a statement about +//! the DECODER, not the stream, so it stays an `Err`: the same volume libavcodec's +//! reference-miss errors had, streak-eligible, and a driver making it repeatedly is +//! precisely what demotion is for. +//! - **A REFUSED AU** — the decoder answering `Err` outright (a plan error, a +//! Vulkan/session failure). Also an error, also streak-eligible, and counted +//! separately from concealment in [`DecodeHealth::refused`]: "the stream is +//! damaged and I coped" and "I could not run" are opposite statements about the +//! rung, and only the second one means the session is looking at a frozen screen. +//! +//! **AV1 answers a LOST REFERENCE as a refusal, not as concealment**, and that is the +//! codec's doing rather than a policy difference here. AV1's reference array is indexed +//! by reference NAME, so a lost reference leaves a HOLE and there is no legal substitute +//! to write into it — `-1` for a name the frame really references is a spec violation +//! whose firmware behaviour is undefined, so [`VkAv1Decoder::decode`] refuses the AU +//! (`MissingReferenceAv1`) instead of concealing. The refusal counts in +//! [`DecodeHealth::refused`], the `Err` sets `want_keyframe` through `video::Decoder`'s +//! own error arm, and the decoder then skips to the next key frame — answering an +//! `Err` for every access unit of that wait, so the streak keeps ticking until the +//! re-anchor lands (the paragraph above). What must not be done is to launder either +//! answer into a concealment, or into a clean AU: the pictures really were not +//! decoded, and reporting "damaged, coped" — or "nothing to object to" — would put a +//! clean-looking bill of health on a rung that produced no picture. +//! +//! **The invariant all of the above serves: an answer may clear the demotion +//! streak only if it PROVES the rung works.** `video::Decoder::decode_frame` resets +//! the streak on a shipped frame or a CLEAN access unit, and on nothing else — so +//! every state in which this backend produces no picture has to reach it as either +//! a concealment (`Ok(None)` + a recovery request) or an `Err`, never as a clean +//! `Ok(None)`. Concealment is therefore left untouched by the reset (otherwise a +//! driver failing every other AU on a lossy link has its errors zeroed by the +//! concealment between them, and a rung that conceals FOREVER — a host framing +//! regression: every AU damaged, no frame ever shipped — has no escape hatch at +//! all), and the AV1 key-frame wait is an `Err` rather than the clean `Ok(None)` it +//! superficially resembles. The one genuinely clean `Ok(None)` is the decoder that +//! ran and had nothing to object to: it buffered, or it skipped an H.265 RASL +//! picture after an open-GOP join. +//! +//! Neither can storm, for two independent reasons. The ask is throttled to one per +//! 100 ms per session whatever the damage rate; and once the freeze is armed the gate +//! lifts only on a proven re-anchor, so a run of damaged AUs refreshes an existing +//! freeze rather than compounding into more requests. A stream that never recovers +//! therefore costs one keyframe ask per 100 ms, not one per AU. +//! +//! **Recovery-point SEI** (M4): pf-vkdecode's `RecoveryWatch` folds the parsed SEI +//! into a per-picture mark that rides the frame ([`NativeVkFrame::recovery`]) into +//! the shared gate's `on_local_recovery`. It is the only way a client can see an +//! intra-refresh session heal on the two backends that run a wave WITHOUT setting the +//! wire mark (Windows AMF and QSV — only Linux libav-NVENC sets it): the wave emits +//! no IDR and libavcodec flags none, so without this such a session freezes for the +//! full 500 ms backstop and then forces the very IDR the wave exists to avoid. +//! Additional, never a replacement: the wire path is untouched and the other rungs +//! keep exactly the behaviour they had. +//! +//! **Teardown:** dropping this backend (demotion, session end) waits — bounded — for +//! every shipped frame's token before dropping the decoder, because the decoder's Drop +//! destroys the pool images and its own drain only covers DECODE work, not the +//! presenter's in-flight sampling. Tokens arrive as the presenter's fence waits/drops +//! displace the frames; a presenter wedged past [`TEARDOWN_BUDGET`] forfeits (warned). + +use crate::video::{ + ColorDesc, DecodeHealth, NativeReleaseGuard, NativeReleaseToken, NativeVkFrame, NativeVkLayout, + VulkanDecodeDevice, +}; +use anyhow::{anyhow, bail, Result}; +use pf_vkdecode::ash::vk; +use pf_vkdecode::ash::vk::Handle as _; +use pf_vkdecode::{ + DecodeStatus, DecodedVkFrame, DeviceHandles, VkAv1Decoder, VkDecodeError, VkH264Decoder, + VkH265Decoder, +}; +use std::sync::mpsc; +use std::time::{Duration, Instant}; + +/// The queue index this backend submits on within the decode family: the presenter +/// creates exactly ONE queue (index 0) per family it enables (`vk/setup.rs` — one +/// `VkDeviceQueueCreateInfo` per family, `queue_count = 1`), so 0 is the only queue +/// that exists. +const DECODE_QUEUE_INDEX: u32 = 0; + +/// Teardown budget for the presenter to hand back every outstanding frame token (its +/// next present's fence wait, typically one frame). Generous against a paused stream, +/// finite against a wedged presenter — after this the pools are destroyed anyway +/// (warned; the realistic residue is a logically-held frame, not in-flight GPU work). +const TEARDOWN_BUDGET: Duration = Duration::from_millis(500); + +/// Query-poll belt: a frame whose token has returned had its decode op complete on the +/// GPU (the presenter's submit waited the decode timeline), so its status query MUST be +/// readable — if it still reads Pending after this many polls, give the slot back +/// anyway rather than strand it (debug-logged; the status is then simply unknown). +const MAX_POLLS_AFTER_RELEASE: u32 = 3; + +/// Do the presenter's and the decoder's submit queues collide? Both sides use queue +/// index 0 of their family by construction (the presenter's graphics queue is +/// `get_device_queue(qfi, 0)`, the decoder's is [`DECODE_QUEUE_INDEX`] of `decode_qf`), +/// so the collision test is family equality. Pure — the queue-lock decision is +/// CPU-testable. +fn submit_queues_collide(graphics_qf: u32, decode_qf: u32) -> bool { + graphics_qf == decode_qf +} + +/// [`pf_vkdecode::QueueLock`] over the device's shared [`crate::video::QueueLock`] — +/// or over nothing, when the decode queue provably has no other submitter (see the +/// module doc's queue-lock section). +enum NativeQueueLock { + /// Decode shares the presenter's graphics queue: serialize with everyone. + Shared(std::sync::Arc), + /// A separate decode family/queue: this backend is its only submitter. + Uncontended, +} + +impl pf_vkdecode::QueueLock for NativeQueueLock { + fn lock(&self) { + if let NativeQueueLock::Shared(l) = self { + l.lock(); + } + } + fn unlock(&self) { + if let NativeQueueLock::Shared(l) = self { + l.unlock(); + } + } +} + +/// The codecs pf-vkdecode has a decoder for — the native rung's whole vocabulary, +/// named ash-free so `video.rs` can pick one from the negotiated wire codec without +/// this module knowing about the wire's codec bits (and `video::native_vulkan_gate` +/// stays the single admission decision). +/// +/// Being IN this enum is not the same as being in `auto`: this list says pf-vkdecode has +/// a decoder, `video::native_vulkan_gate` says whether the automatic ladder may pick it +/// (and the device's own codec-operation caps bit is half of that answer). All three legs +/// are in `auto`, and that is the gate's decision to change, not this list's. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeCodec { + H264, + H265, + Av1, +} + +/// The decoder this backend drives, chosen ONCE from the negotiated codec. +/// +/// Dispatch stops here. Everything the backend does around a decoder — the +/// shipped-frame ledger, release tokens, status-query settling, timeline waits, +/// teardown — is codec-agnostic, because [`VkH264Decoder`], [`VkH265Decoder`] and +/// [`VkAv1Decoder`] expose the same surface over the same [`DecodedVkFrame`] contract +/// (same pool/slot lifecycle, same `value + 1` write-back, same query slots, same +/// generations). The forwarders below are therefore mechanically identical per arm on +/// purpose: the H.264 path is hardware-verified bit-exact, and dispatch must not be +/// able to change its behaviour. +// Unboxed on purpose, against `large_enum_variant`: the arms differ by ~1.7 KB (every +// decoder carries a planner, a slot ledger and pinned Std parameter sets), and exactly +// ONE of these exists per session — inside the `Box` the backend +// already lives in. So the "waste" is 1.7 KB of slack in a single session-lifetime +// allocation, while boxing would put a second indirection between the pump and the +// decoder on the per-AU path and change how the hardware-verified H.264 decoder is +// reached. Neither trade is worth 1.7 KB. +#[allow(clippy::large_enum_variant)] +enum Codec { + H264(VkH264Decoder), + H265(VkH265Decoder), + Av1(VkAv1Decoder), +} + +impl Codec { + /// Feed one access unit — see [`VkH264Decoder::decode`] / + /// [`VkH265Decoder::decode`] / [`VkAv1Decoder::decode`]. `Ok(None)` means "no + /// display-ready picture from this AU", which for H.265 also covers a RASL + /// picture skipped after an open-GOP join (the module doc's contract: never an + /// error). + /// + /// What `Ok(None)` deliberately does NOT cover on any arm is a decoder waiting + /// to re-anchor after a failure: H.264/H.265 answer that with their planners' + /// `PlanError::AwaitingIdr` and AV1 with `VkDecodeError::AwaitingKeyAv1`, one + /// `Err` per access unit for as long as the wait lasts. A clean `Ok(None)` + /// would clear the demotion streak once per frame and strand the session on a + /// rung that produces nothing (module doc). + /// + /// AV1 is the one arm where "an access unit" is not "a frame": its AU is a + /// TEMPORAL UNIT, the decoder walks every frame in it, and what comes back is + /// the FIRST displayable picture of the walk — the rest, if any, through + /// [`Self::take_ready`], exactly like H.265's burst output. + fn decode(&mut self, au: &[u8]) -> Result, VkDecodeError> { + match self { + Codec::H264(d) => d.decode(au), + Codec::H265(d) => d.decode(au), + Codec::Av1(d) => d.decode(au), + } + } + + /// Drain the plan warnings of the AU just decoded, TYPED — the three planners + /// have genuinely different enums ([`pf_vkdecode::PlanWarning`] has + /// `FrameNumGap`/`Mmco5Rebase`, [`pf_vkdecode::H265PlanWarning`] has + /// `NonZeroReorder`, [`pf_vkdecode::Av1PlanWarning`] has `MissingShowExisting`, + /// none a subset of another), so the set is carried as a three-armed value + /// rather than flattened. + /// + /// Typed and not rendered because the backend must BRANCH on them: only some + /// warnings mean the picture is damaged ([`PlanWarnings::integrity`]), and + /// dropping a frame for the others costs a visible hitch on a stream the + /// planner says it planned correctly. Strings would make that a substring + /// match on `Debug` output. + fn take_warnings(&mut self) -> PlanWarnings { + match self { + Codec::H264(d) => PlanWarnings::H264(d.take_warnings()), + Codec::H265(d) => PlanWarnings::H265(d.take_warnings()), + // The AV1 decoder concatenates the WHOLE temporal unit's warnings, in + // decode order — one unit, one concealment verdict, which is what this + // backend already assumes for an AU. + Codec::Av1(d) => PlanWarnings::Av1(d.take_warnings()), + } + } + + /// Pull the next already display-ready frame the last AU did not return + /// directly (H.265 burst output; on AV1 only a non-conformant or + /// multi-operating-point unit, since the spec admits one shown frame per + /// temporal unit — [`MAX_DELIVERABLE`]). Drained after EVERY decode, so a + /// burst can never be stranded inside the decoder. + fn take_ready(&mut self) -> Option { + match self { + Codec::H264(d) => d.take_ready(), + Codec::H265(d) => d.take_ready(), + Codec::Av1(d) => d.take_ready(), + } + } + + /// Hand a delivered frame back to its pool; `presented` reports whether the + /// consumer enqueued the frame's `value + 1` timeline signal. + fn release_frame( + &mut self, + frame: &DecodedVkFrame, + presented: bool, + ) -> Result<(), VkDecodeError> { + match self { + Codec::H264(d) => d.release_frame(frame, presented), + Codec::H265(d) => d.release_frame(frame, presented), + Codec::Av1(d) => d.release_frame(frame, presented), + } + } + + /// The decoder's current session generation (a frame from an older one has an + /// unknowable status verdict — see [`NativeVulkanDecoder::settle_statuses`]). + fn generation(&self) -> u64 { + match self { + Codec::H264(d) => d.generation(), + Codec::H265(d) => d.generation(), + Codec::Av1(d) => d.generation(), + } + } + + /// Non-blocking read of a frame's `RESULT_STATUS_ONLY` query. + fn poll_status(&mut self, frame: &DecodedVkFrame) -> DecodeStatus { + match self { + Codec::H264(d) => d.poll_status(frame), + Codec::H265(d) => d.poll_status(frame), + Codec::Av1(d) => d.poll_status(frame), + } + } + + /// Bounded host wait for a frame's decode-complete timeline signal (the pump's + /// sampled decode-latency stat). + fn wait_decoded(&self, frame: &DecodedVkFrame, timeout_ns: u64) -> bool { + match self { + Codec::H264(d) => d.wait_decoded(frame, timeout_ns), + Codec::H265(d) => d.wait_decoded(frame, timeout_ns), + Codec::Av1(d) => d.wait_decoded(frame, timeout_ns), + } + } + + /// Does this device answer per-op decode-status queries at all? A device + /// fact, not a codec one — forwarded per arm only because the decoders own + /// the `DecodeDevice`. + fn status_queries(&self) -> bool { + match self { + Codec::H264(d) => d.status_queries(), + Codec::H265(d) => d.status_queries(), + Codec::Av1(d) => d.status_queries(), + } + } + + /// The newest planned picture's DECODE-order ordinal — the watermark the + /// pump stamps when it arms a freeze (see [`NativeVkFrame::decode_order`]). + /// On AV1 a `show_existing_frame` does not advance it, because it decodes + /// nothing — which is what the watermark is comparing against. + fn decode_order(&self) -> u64 { + match self { + Codec::H264(d) => d.decode_order(), + Codec::H265(d) => d.decode_order(), + Codec::Av1(d) => d.decode_order(), + } + } +} + +/// What one pass of [`NativeVulkanDecoder::settle_statuses`] learned about the +/// decode status of previously shipped frames. +/// +/// Two numbers, not one, because the SAME `DecodeStatus::Failed` means two +/// different things depending on the device. Where the decode family answers +/// `RESULT_STATUS` queries it is the driver's own verdict on its own decode — the +/// Xbox Ally X signal, and the count `DecodeHealth::failed` reports. Where it does +/// NOT (RADV, whose VCN ring hangs if a query is recorded anyway), `poll_status` +/// degrades to reading the decode timeline, and a `Failed` there means the session +/// generation is gone, the device was lost, or the semaphore could not be read — +/// none of which the driver ever said anything about. Reporting those as driver +/// verdicts renders `integrity: driver-failed 1 · no driver status`, which is +/// self-contradictory and points a support engineer at hardware that never spoke. +/// +/// Both cost the picture, so both release their frame unshown, both surface as an +/// error, and both extend the concealed run. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +struct StatusVerdicts { + /// Frames the DRIVER reported corrupt. Only ever non-zero on a device that + /// answers status queries. + driver_failed: u32, + /// Frames whose status could not be established on a device with no query + /// support — the degraded timeline path. + unreadable: u32, +} + +impl StatusVerdicts { + fn total(&self) -> u32 { + self.driver_failed + self.unreadable + } +} + +/// The plan warnings one AU produced, still in their codec's own enum. +/// +/// The split that matters is INTEGRITY vs. spec-legal, not which codec. The +/// planners emit two kinds of warning through one channel: +/// +/// - **Integrity** — a reference the DPB does not hold, a `frame_num` gap, an AU +/// whose NALU walk stopped early. The plan was completed with a SUBSTITUTE in +/// place of something lost: the picture is damaged, so its output is released +/// unshown and a re-anchor is requested. +/// - **Spec-legal envelope signals** — h265's `NonZeroReorder` (the activated SPS +/// sets `sps_max_num_reorder_pics > 0`) and h264's `Mmco5Rebase`. pf-bitstream +/// documents both as "spec-legal and fully planned"; they exist as the field +/// signal that a punktfunk-host assumption broke, not as damage. `NonZeroReorder` +/// in particular fires on the AU that ACTIVATES an SPS — the opening IDR, and the +/// fresh IDR at every ABR resolution change — so treating it as concealment costs +/// a released-unshown frame plus a keyframe round trip at every renegotiation, on +/// a stream the planner planned correctly. pf-bitstream's own conformance harness +/// excludes `NonZeroReorder` from its integrity set for exactly this reason. +/// +/// Everything is logged either way; only integrity warnings drop the frame. +/// +/// AV1 (M7) has an EMPTY right-hand column: its planner reports nothing that is +/// spec-legal-but-notable, because AV1 has no reorder envelope to announce (no +/// bumping process, no `max_num_reorder_pics`) and no MMCO to rebase. Every AV1 +/// warning is damage, and `pf_vkdecode::is_integrity_warning_av1` says so +/// exhaustively so a warning added later cannot default to "clean". The branch +/// below is therefore not dead code on that arm — it is the place a future +/// spec-legal AV1 warning would land without costing a frame. +enum PlanWarnings { + H264(Vec), + H265(Vec), + Av1(Vec), +} + +impl PlanWarnings { + fn is_empty(&self) -> bool { + match self { + PlanWarnings::H264(w) => w.is_empty(), + PlanWarnings::H265(w) => w.is_empty(), + PlanWarnings::Av1(w) => w.is_empty(), + } + } + + /// Just the warnings that mean the picture is damaged — the concealment set. + /// Allocates, but only off the clean path: [`Self::is_empty`] is true for every + /// AU of a healthy stream. + /// + /// The predicate itself lives in pf-vkdecode + /// ([`pf_vkdecode::is_integrity_warning`]) rather than here, so the + /// fault-injection harness asserts detection against the SAME list this + /// conceals on. Two copies would let a test prove a detection production does + /// not actually perform. + fn integrity(&self) -> PlanWarnings { + match self { + PlanWarnings::H264(w) => PlanWarnings::H264( + w.iter() + .filter(|x| pf_vkdecode::is_integrity_warning(x)) + .cloned() + .collect(), + ), + PlanWarnings::H265(w) => PlanWarnings::H265( + w.iter() + .filter(|x| pf_vkdecode::is_integrity_warning_h265(x)) + .cloned() + .collect(), + ), + PlanWarnings::Av1(w) => PlanWarnings::Av1( + w.iter() + .filter(|x| pf_vkdecode::is_integrity_warning_av1(x)) + .cloned() + .collect(), + ), + } + } + + fn len(&self) -> usize { + match self { + PlanWarnings::H264(w) => w.len(), + PlanWarnings::H265(w) => w.len(), + PlanWarnings::Av1(w) => w.len(), + } + } + + /// The concealment log. Per arm so the rendering is the codec's OWN enum — + /// `warnings=[FrameNumGap { .. }]`, exactly what the hardware-verified H.264 + /// path emitted before dispatch existed (a `Vec` renders + /// `["FrameNumGap { .. }"]`, and a wrapper enum would prefix the arm). + /// + /// `concealed` is how many of the rendered warnings are INTEGRITY warnings — + /// the count the frame was actually dropped for. Both numbers are carried + /// because they can differ: the list is every warning of the AU (a spec-legal + /// companion is context worth having), while the count is the damage. On H.264 + /// the two coincide for every warning a punktfunk host can produce. + fn warn_concealment(&self, concealed: usize) { + // Spelled out per arm rather than shared through a `const`: this is the + // H.264 path's PRODUCTION log line, and a literal is what keeps it a static + // tracing message rather than a formatted one. + match self { + PlanWarnings::H264(w) => tracing::warn!( + concealed, + warnings = ?w, + "native decode planned with concealment — dropping the frame, \ + requesting re-anchor" + ), + PlanWarnings::H265(w) => tracing::warn!( + concealed, + warnings = ?w, + "native decode planned with concealment — dropping the frame, \ + requesting re-anchor" + ), + PlanWarnings::Av1(w) => tracing::warn!( + concealed, + warnings = ?w, + "native decode planned with concealment — dropping the frame, \ + requesting re-anchor" + ), + } + } + + /// The spec-legal log: the planner flagged an envelope fact and planned the AU + /// in full, so the frame is SHOWN. Rare by construction (SPS activation, MMCO + /// 5), which is why it is a `warn` and not a per-frame `debug`. Unreachable on + /// the AV1 arm today — every AV1 warning is damage — and spelled out anyway so + /// a future spec-legal AV1 warning gets the same treatment rather than the + /// concealment branch's. + fn warn_planned_in_full(&self) { + match self { + PlanWarnings::H264(w) => tracing::warn!( + warnings = ?w, + "native decode: spec-legal envelope signal — the AU was planned in \ + full and the frame is kept" + ), + PlanWarnings::H265(w) => tracing::warn!( + warnings = ?w, + "native decode: spec-legal envelope signal — the AU was planned in \ + full and the frame is kept" + ), + PlanWarnings::Av1(w) => tracing::warn!( + warnings = ?w, + "native decode: spec-legal envelope signal — the AU was planned in \ + full and the frame is kept" + ), + } + } +} + +/// One frame shipped to the presenter and not yet fully settled: settled = its release +/// token came back (GPU reads proven done) AND its status query was read. +struct Shipped { + seq: u64, + frame: DecodedVkFrame, + /// The presenter (or a drop on the way there) returned the token. + released: bool, + /// The token said the sampling submission (with its `value + 1` timeline + /// signal) was enqueued — forwarded to `release_frame` so the decoder waits + /// the write-back before reusing the image. + presented: bool, + /// The status query read a conclusive verdict (or the poll belt expired). + resolved: bool, + /// Polls attempted after the token returned — see [`MAX_POLLS_AFTER_RELEASE`]. + polls_after_release: u32, +} + +/// Mark the shipped entry a token names as released. Returns false when nothing +/// matches (a late token from before a demotion drain — benign). Pure bookkeeping, +/// split out so the channel-drain behavior is CPU-testable. +fn note_token(outstanding: &mut [Shipped], token: NativeReleaseToken) -> bool { + match outstanding.iter_mut().find(|s| s.seq == token.seq) { + Some(s) => { + debug_assert_eq!( + s.frame.generation, token.generation, + "a token's generation always matches the frame it rode on" + ); + s.released = true; + s.presented = token.presented; + true + } + None => false, + } +} + +/// Flatten a delivered [`DecodedVkFrame`] into the ash-free [`NativeVkFrame`] the +/// presenter consumes. Pure over the frame (the guard is the caller's), so the +/// projection — every fact the presenter can no longer look up for itself — is +/// CPU-testable. +/// +/// The one that is easy to get wrong is [`NativeVkFrame::vk_format`]: the picture +/// format is the STREAM's, not the codec's. H.264 in this program is always the 8-bit +/// 4:2:0 envelope (NV12), but an H.265 session decodes Main to NV12, Main 10 to P010 +/// and RExt 4:4:4 to the two-plane 4:4:4 formats — and can change format mid-stream +/// when the host renegotiates. A consumer that assumes 8-bit 4:2:0 renders a Main 10 +/// picture with 8-bit transfer/range math: plausible-looking and wrong. So the format +/// is carried, never inferred, all the way to the presenter's CSC pass. +fn project_frame(frame: &DecodedVkFrame, guard: NativeReleaseGuard) -> NativeVkFrame { + NativeVkFrame { + image: frame.image.as_raw(), + vk_format: crate::video::RawVkFormat(frame.format.as_raw()), + plane_views: [frame.plane_views[0].as_raw(), frame.plane_views[1].as_raw()], + layer: frame.layer, + layout: if frame.layout == vk::ImageLayout::VIDEO_DECODE_DPB_KHR { + NativeVkLayout::DecodeDpb + } else { + NativeVkLayout::DecodeDst + }, + semaphore: frame.semaphore.as_raw(), + semaphore_value: frame.value, + generation: frame.generation, + width: frame.crop.width, + height: frame.crop.height, + coded_width: frame.coded_width, + coded_height: frame.coded_height, + crop_x: frame.crop.x, + crop_y: frame.crop.y, + // H.273 code points straight off the picture's ACTIVE SPS/VUI — per frame, + // never latched, because the Windows host switches an HDR desktop to + // PQ/BT.2020 IN-BAND (the Welcome still says SDR). pf-bitstream applies + // E.2.1's "unspecified" inference (2/2/2, limited) where the VUI is + // silent, and `csc_rows` resolves "unspecified" to its BT.709-limited + // SDR default — same verdicts libavcodec's CICP passthrough produced. + color: ColorDesc { + primaries: frame.colour.colour_primaries, + transfer: frame.colour.transfer_characteristics, + matrix: frame.colour.matrix_coefficients, + full_range: frame.colour.video_full_range, + }, + keyframe: frame.is_idr, + poc: frame.poc, + // The recovery point SEI's verdict for THIS picture, folded by + // pf-vkdecode's `RecoveryWatch` at plan time and translated here into the + // shared gate's vocabulary. The two structs are deliberately separate + // types with the same shape: pf-vkdecode must not depend on punktfunk-core + // to describe a bitstream fact, and punktfunk-core must not depend on + // pf-vkdecode to accept one. + recovery: punktfunk_core::reanchor::LocalRecovery { + sei_here: frame.recovery.sei_here, + is_recovery_point: frame.recovery.is_recovery_point, + }, + // Which side of a loss this picture was DECODED on. Carried beside the + // recovery mark because the mark is worthless without it: a post-failure + // DPB flush delivers pre-loss pictures after the loss, and their marks + // describe a wave that completed before it. + decode_order: frame.decode_order, + guard, + } +} + +/// The picture format a session of the negotiated shape decodes to, or a named +/// refusal for a shape pf-vkdecode has no output format for at all. `codec` names the +/// codec in the refusal text and nothing else — the map is the CRATE's one +/// (sampling, depth) → format table, shared by every codec it decodes. +/// +/// The DEVICE-INDEPENDENT half of [`NativeVulkanDecoder::new`]'s shape check: 4:2:2, +/// monochrome and 12-bit are legal H.265/AV1 that no punktfunk host emits and this +/// client has no plumbing for, so no driver has to be asked about them. Pure, so the +/// refusal is CPU-testable — the device-dependent half (a shape with a format that +/// THIS driver does not advertise) is `probe_stream_support`, covered by +/// pf-vkdecode's `derive_caps_h265`/`derive_caps_av1` refusal tests. +/// +/// One function for both codecs because the ENVELOPE is identical: pf-vkdecode's +/// AV1 profile builder admits exactly the four (sampling, depth) pairs +/// [`pf_vkdecode::output_format_for`] maps, and refusing here in different terms +/// than the probe refuses one line later would be two gates to keep in agreement. +fn picture_format(codec: &str, stream: crate::video::StreamFormat) -> Result { + let depth = stream.bit_depth_minus8().ok_or_else(|| { + anyhow!( + "negotiated {codec} bit depth {} is outside the 8/10-bit decode envelope", + stream.bit_depth + ) + })?; + pf_vkdecode::output_format_for(stream.chroma_format_idc, depth).ok_or_else(|| { + anyhow!( + "no native picture format for the negotiated {codec} stream shape \ + (chroma_format_idc={}, {}-bit)", + stream.chroma_format_idc, + stream.bit_depth + ) + }) +} + +/// The film-grain flag the AV1 construction-time probe asks the device about. +/// +/// Grain synthesis is part of the AV1 decode PROFILE — a device that decodes AV1 +/// need not offer the grain-enabled one — and the negotiation carries no grain bit, +/// so this is the one probe input that is an ASSUMPTION rather than a negotiated +/// fact. `false` is the right assumption and the safe one: +/// +/// * a punktfunk host encodes desktop capture, where film-grain synthesis is off +/// (it exists to re-add grain a denoiser removed from camera footage); +/// * and the failure directions are not symmetric. Probing `false` on a device that +/// only offers the grain profile would REFUSE a session it could have run — but +/// there is no such device (grain support is an added capability, never a +/// replacement). Probing `true` on the far more common device that offers only +/// the grain-LESS profile would refuse every session this rung can actually +/// decode. +/// +/// If a grain stream ever does arrive, `ensure_state` re-keys from the SEQUENCE +/// header (never softened to make a query pass) and refuses at the first AU — which +/// lands on the "never delivered a frame" arm in [`crate::video::Decoder`], the same +/// backstop that already covers a level above `maxLevelIdc` and a sequence header +/// disagreeing with the Welcome. +const AV1_PROBE_FILM_GRAIN: bool = false; + +/// What the CLIENT PIPELINE itself holds, at its worst moment: how many delivered +/// frames are unreleased between this backend and the screen at once. +/// +/// pf-vkdecode's [`pf_vkdecode::HOLD_HEADROOM`] docs enumerate them — two bounded(2) +/// channels, the FrameStore's 1..=3 preroll, the in-flight present, the retired-frame +/// slot — as 4-7 at steady state. Taken at the MAXIMUM, because a bound derived from +/// the average is a bound that fails exactly when it is needed. +const PIPELINE_HOLD: usize = 7; + +/// How many display-ready frames the backend will hold back for LATER access units +/// before it starts dropping the oldest (see [`trim_deliverable`]). +/// +/// **Derived, not chosen.** [`pf_vkdecode::HOLD_HEADROOM`] is the TOTAL number of +/// delivered-but-unreleased frames the pool is sized for (`picture_count = +/// required_slots + HOLD_HEADROOM`), and a queued frame counts against it exactly +/// like a shipped one: `build_frame` increments the picture's `held` the moment the +/// decoder declares it ready, and it stays held until [`Codec::release_frame`]. So +/// the queue's share of the headroom is whatever the pipeline does not already +/// occupy, and a queue bounded any deeper does not prevent the failure it names — +/// it merely caps the memory while the pool runs out anyway (8 queued + 7 in flight +/// against a headroom of 8 is `NoFreeSlot` on the next AU). +/// +/// The other bound it has to stay inside is the STATUS-QUERY ring, which is +/// `picture_count` deep (17 on AV1: nine DPB slots plus the headroom) and is +/// re-armed once per SUBMISSION — up to two per temporal unit. A frame's query is +/// first read the AU after it ships, so a frame that waits `MAX_DELIVERABLE` access +/// units in this queue burns roughly `2 * (MAX_DELIVERABLE + 1)` of those 17 slots +/// before anyone looks at it. Overrun the ring and `read_status` reports the +/// re-armed slot as `Failed`, which [`NativeVulkanDecoder::settle_statuses`] +/// attributes to `driver_failed` — a FABRICATED driver-corruption verdict polluting +/// the one signal [`DecodeHealth::failed`] exists to carry (the Xbox Ally X class). +/// At the derived depth the wait is ~4 of 17 and the question does not arise. +/// +/// The queue exists because a decoder can make several pictures display-ready from +/// one AU while the caller takes exactly one per AU: H.265 bumping outputs a burst +/// after a reordering stretch. AV1 cannot — the spec admits exactly one shown frame +/// per temporal unit, and pf-bitstream's conformance golden pins it (250 shown +/// frames across 250 units, no `show_existing_frame` at all) — so on that codec this +/// is defence in depth against a non-conformant or multi-operating-point stream, not +/// a routine case. Either way punktfunk hosts reorder nothing, so on the wire the +/// queue is empty every single AU and the bound never engages. +/// +/// It is a bound and not a plain queue because "transient" is an assumption about the +/// HOST, and the failure it fails into is silent: a stream that reliably made two +/// frames displayable per AU would grow this by one per AU until the pool ran out — +/// after which every AU refuses with `NoFreeSlot`, three in a second demote the rung, +/// and nothing in the log would say the cause was a queue that could never drain. +/// +/// What the derived depth costs, stated plainly: a stream that really does bump a +/// burst of more than two pictures at once loses the middle of the burst rather than +/// queueing it. That is the right way round. The frames are already several AUs late +/// by the time a burst exists, the stage after this one is newest-wins anyway, and +/// the alternative — a queue deep enough to hold the burst — spends the pool's whole +/// headroom on it and answers `NoFreeSlot` on the next access unit, which is a frozen +/// screen and a demotion rather than a hitch. Reachable only on a reordering stream, +/// which punktfunk hosts do not emit and which the planner already flags +/// (`H265PlanWarning::NonZeroReorder`). +const MAX_DELIVERABLE: usize = pf_vkdecode::HOLD_HEADROOM as usize - PIPELINE_HOLD; + +// The derivation must leave the queue able to do its job: carry the one frame a +// two-output access unit strands. A `PIPELINE_HOLD` raised to the headroom (or past +// it) would silently turn every burst into a dropped frame — or underflow the const. +const _: () = assert!( + MAX_DELIVERABLE >= 1, + "the deliverable queue must be able to carry at least one frame between AUs" +); + +/// One `warn` per this many dropped deliverable frames, after the first. The shape +/// that drops at all drops on EVERY access unit, and a warn per frame at frame rate +/// buries the log it exists to explain — while a single line at the start of a +/// session that then goes quiet reads as a one-off. So: the first drop in full, then +/// a heartbeat with the running total (~every 5 s at 60 fps). +const DROP_WARN_EVERY: u64 = 300; + +/// Trim the deliverable queue to `cap` by dropping from the FRONT, returning the +/// dropped frames so the caller can release them unshown. +/// +/// Oldest-first, because by the time a queue this deep exists the front frame is +/// several AUs stale and the consumer one stage on is itself newest-wins (the pump's +/// `force_send` overwrites an unconsumed frame). Dropping the NEWEST would keep the +/// stalest picture and present the stream in ever-lagging order; dropping the oldest +/// keeps display order for everything that survives and costs the frames that were +/// already too late to matter. +/// +/// ⚠ Called AFTER this AU's own frame has been taken off the front, so `cap` bounds +/// the CARRY-OVER — what is held back for later access units — exactly as +/// [`MAX_DELIVERABLE`] says. Trimming before the take would make an AU that produced +/// two outputs drop the FIRST of them and ship the second, which is display order +/// inverted inside a single access unit. +/// +/// Pure over the queue, so the bound is CPU-testable without a GPU. +fn trim_deliverable( + queue: &mut std::collections::VecDeque, + cap: usize, +) -> Vec { + let mut dropped = Vec::new(); + while queue.len() > cap { + match queue.pop_front() { + Some(frame) => dropped.push(frame), + // Unreachable: `len() > cap >= 0` means the queue is non-empty. Written + // as a break rather than an `expect` so a bound of 0 on an empty queue + // could never be a panic in the decode path. + None => break, + } + } + dropped +} + +/// The native backend: the decoder plus the shipped-frame ledger and release channel. +pub(crate) struct NativeVulkanDecoder { + dec: Codec, + /// Cloned into every shipped frame's guard. `Option` so teardown can DROP the + /// backend's own sender: only then does `release_rx` report Disconnected once + /// the last guard is gone — the teardown short-circuit signal. + release_tx: Option>, + release_rx: mpsc::Receiver, + /// Display-ready frames not yet handed to the pump (an H.265 burst output, or a + /// temporal unit that declared more pictures displayable than AV1 permits — + /// decode delivers one per call; the rest wait here, oldest first, bounded by + /// [`MAX_DELIVERABLE`]). Every frame in here holds a picture-pool image. + deliverable: std::collections::VecDeque, + outstanding: Vec, + next_seq: u64, + /// The session's integrity counters (M4). Plain adds on the decode path, read + /// once per stats window — no allocation, no per-frame work. + health: DecodeHealth, + /// Stream damage happened and the host should be asked for a re-anchor. + /// Drained by `video::Decoder::decode_frame`, which routes it into the same + /// `want_keyframe` every other recovery ask uses (module doc's policy). + want_recovery: bool, + /// Corrupt the AU on its way into the decoder — `PUNKTFUNK_AU_FAULT`, + /// `None` unless armed. Lives at THIS boundary rather than in + /// `video::Decoder::decode_frame` on purpose: this is the lane whose detectors + /// the injector exists to fire, and putting the knob here means a faulted AU + /// is byte-identical to what the decoder would have been handed by a lossy + /// network — no other backend's behaviour can be perturbed by a typo'd + /// variable. + fault: Option, +} + +// SAFETY: the decoder is used strictly serially through `&mut self` from whichever +// single thread owns the enclosing `Decoder` (the session pump) — `Send` only moves +// that ownership. The `Rc`s inside pf-vkdecode's planners (H.264 and H.265 alike) +// never escape them, so they all move together; every queue submission runs under the +// collision-aware queue lock; the mpsc endpoints are `Send`. Same contract, same shape +// as the `VulkanDecoder` and `PyroWaveDecoder` impls above/beside it. Deliberately NOT +// `Sync`. +unsafe impl Send for NativeVulkanDecoder {} + +impl NativeVulkanDecoder { + /// Build the backend over the presenter's device for `codec` — the codec the + /// session negotiated, already admitted by `video::native_vulkan_gate` (which + /// checked that the decode family advertises this codec's decode op; the + /// decoders re-check it themselves rather than trust the caller, because + /// creating a video session for a codec operation the family cannot run is + /// undefined behaviour rather than an error). + /// + /// Sessions and pools are built lazily from the first AU's parameter sets, so + /// nothing BELOW this constructor depends on the stream's shape — which is why + /// the shape is checked HERE, against `stream` (the host's resolved Welcome + /// facts), rather than being discovered at the first decode. + /// + /// The difference is which rung a refusal lands on. pf-vkdecode's picture format + /// is the STREAM's (Main → NV12, Main 10 → P010, RExt 4:4:4 → the two-plane 4:4:4 + /// formats) and a device that advertises H.265 or AV1 decode need not advertise a + /// format for every shape of it: 4:4:4 is absent everywhere but NVIDIA. Discovered + /// lazily, that is a mid-stream ERROR STREAK, and the streak machinery demotes a + /// Vulkan rung to VAAPI/D3D11VA — which on NVIDIA/Linux (no usable VAAPI) means a + /// 4K HEVC session lands on SOFTWARE. Refused here it is an ordinary construction + /// failure, and `video::Decoder::new` simply walks on to the next rung. + /// + /// Three legs the probe cannot see, because they are stream facts no negotiation + /// carries: a level above the device's `maxLevelIdc`, an SPS (or AV1 sequence + /// header) that disagrees with the Welcome, and — AV1 only — a sequence that + /// enables FILM GRAIN, which is part of the decode profile and which the probe + /// therefore has to assume ([`AV1_PROBE_FILM_GRAIN`]). All three still surface at + /// the first decode, where the demotion walk's next candidate is the rung DIRECTLY + /// below this one — the property the pre-M10 "never delivered a frame" arm existed + /// to guarantee, now structural (see [`crate::video::Decoder::decode_frame`]). + /// + /// H.264 is deliberately NOT probed: its envelope is fixed at 8-bit 4:2:0, so the + /// only fact a probe could add is a profile idc guess — on the one path in this + /// program that is hardware-verified bit-exact against libavcodec. It keeps the + /// never-delivered arm as its backstop. + pub(crate) fn new( + vk: &VulkanDecodeDevice, + codec: NativeCodec, + stream: crate::video::StreamFormat, + ) -> Result { + if !vk.video_decode { + bail!("presenter device lacks Vulkan Video decode"); + } + let lock: Box = + if submit_queues_collide(vk.graphics_qf, vk.decode_qf) { + Box::new(NativeQueueLock::Shared(vk.queue_lock.clone())) + } else { + Box::new(NativeQueueLock::Uncontended) + }; + let handles = DeviceHandles { + get_instance_proc_addr: vk.get_instance_proc_addr, + instance: vk.instance, + physical_device: vk.physical_device, + device: vk.device, + decode_qf: vk.decode_qf, + decode_queue_index: DECODE_QUEUE_INDEX, + graphics_qf: vk.graphics_qf, + }; + // The `DeviceHandles` caller contract, held for the decoder's whole lifetime + // and identical for both arms (it is the HANDLES' contract, not the codec's): + // the handles are the presenter's live instance/device, which outlives every + // session pump (the run loop tears the pump — and with it this decoder — down + // first: the exact liveness contract the PyroWave backend also relies on over + // the same bundle). `video_decode` (checked above) is set only + // when the presenter enabled the Vulkan Video decode extension stack + + // synchronization2/timelineSemaphore at device creation — including the + // per-codec `VK_KHR_video_decode_h264`/`_h265`/`_av1` extensions, one for + // every codec operation the decode family advertises (`vk/setup.rs` enables + // exactly those it finds). What the decoders then re-check for themselves is + // the QUEUE FAMILY's advertised `videoCodecOperations` — the device's own + // claim about the family, which is what `native_vulkan_gate` reads too. That + // is not a proof the extension was enabled at `vkCreateDevice`; it is the + // same fact `vk/setup.rs` derived its enable list FROM, so the two agree by + // construction here and the check catches a caller that got the family wrong. + // `decode_qf`/`graphics_qf` mirror the families the presenter created queues + // for (one queue, index 0, each). + let dec = match codec { + NativeCodec::H264 => { + // SAFETY: the handle contract stated directly above. + let d = unsafe { VkH264Decoder::new(&handles, lock) } + .map_err(|e| anyhow!("VkH264Decoder init: {e}"))?; + Codec::H264(d) + } + NativeCodec::H265 => { + // The device-independent half of the shape check, first: a stream + // shape pf-vkdecode has NO picture format for (4:2:2, 12-bit) needs + // no driver to refuse it. + let wanted = picture_format("HEVC", stream)?; + // SAFETY: the handle contract stated directly above. + let d = unsafe { VkH265Decoder::new(&handles, lock) } + .map_err(|e| anyhow!("VkH265Decoder init: {e}"))?; + // …and the device-dependent half: does THIS driver advertise that + // format for a decode session of this profile? Same query and same + // derivation `ensure_state` would run at the first AU — only the + // timing differs, and the timing is the whole point. + let depth = stream + .bit_depth_minus8() + .expect("picture_format accepted the depth"); + d.probe_stream_support(stream.chroma_format_idc, depth) + .map_err(|e| { + anyhow!( + "device cannot decode the negotiated HEVC stream shape \ + (chroma_format_idc={}, {}-bit, needs {wanted:?}): {e}", + stream.chroma_format_idc, + stream.bit_depth + ) + })?; + Codec::H265(d) + } + NativeCodec::Av1 => { + // Exactly the H.265 shape check, one codec over — AV1's decode + // profile is a (seq_profile, sampling, depth, film grain) tuple and + // a device that advertises the AV1 decode OPERATION need not offer + // every profile of it. Refused here, the ladder walks to the next + // rung; discovered at the first AU, the only exit is an error streak + // PAST that rung. + let wanted = picture_format("AV1", stream)?; + // SAFETY: the handle contract stated directly above. + let d = unsafe { VkAv1Decoder::new(&handles, lock) } + .map_err(|e| anyhow!("VkAv1Decoder init: {e}"))?; + d.probe_stream_support( + stream.chroma_format_idc, + // AV1's profile key takes the ABSOLUTE bit depth (8/10), not + // H.265's `bit_depth_luma_minus8` — the two probes really do + // want different numbers, and `picture_format` above is the + // one that proved this depth is in the envelope at all. + stream.bit_depth, + AV1_PROBE_FILM_GRAIN, + ) + .map_err(|e| { + anyhow!( + "device cannot decode the negotiated AV1 stream shape \ + (chroma_format_idc={}, {}-bit, needs {wanted:?}): {e}", + stream.chroma_format_idc, + stream.bit_depth + ) + })?; + Codec::Av1(d) + } + }; + let (release_tx, release_rx) = mpsc::channel(); + let status_queries = dec.status_queries(); + if !status_queries { + // Said once, loudly, at construction rather than only in the stats + // line: on this device a clean integrity report means "nothing was + // detectable", not "nothing was wrong" — and a support engineer + // reading a log after the fact has no stats window to consult. + tracing::warn!( + "native decode: this device's decode queue family does not support \ + RESULT_STATUS queries — driver-reported corruption is not \ + observable on this session (decode status degrades to timeline \ + completion — the only signal libavcodec's rungs ever had)" + ); + } + // `PUNKTFUNK_AU_FAULT=[:]` — the deliberate-corruption knob + // (pf_vkdecode::fault). Unset is the only normal state; a spec that does + // not parse leaves the injector disarmed and says so rather than half + // arming. + let fault = std::env::var("PUNKTFUNK_AU_FAULT").ok().and_then(|spec| { + match pf_vkdecode::AuFault::from_spec(&spec) { + Some(f) => { + tracing::warn!( + mode = ?f.mode(), + period = f.period(), + "PUNKTFUNK_AU_FAULT: deliberately corrupting decoder input" + ); + Some(f) + } + None => { + tracing::warn!( + value = %spec, + "PUNKTFUNK_AU_FAULT not understood (want drop|truncate|flip[:period]) \ + — ignored" + ); + None + } + } + }); + Ok(NativeVulkanDecoder { + dec, + release_tx: Some(release_tx), + release_rx, + deliverable: std::collections::VecDeque::new(), + outstanding: Vec::new(), + next_seq: 0, + health: DecodeHealth { + status_queries, + ..DecodeHealth::default() + }, + want_recovery: false, + fault, + }) + } + + /// This session's integrity counters — see [`DecodeHealth`]. + pub(crate) fn health(&self) -> DecodeHealth { + self.health + } + + /// The newest planned picture's DECODE-order ordinal — see + /// [`NativeVkFrame::decode_order`]. + pub(crate) fn decode_order(&self) -> u64 { + self.dec.decode_order() + } + + /// Drain the "the stream was damaged; please ask the host to re-anchor" flag. + /// Deliberately separate from an `Err` return — see the module doc's recovery + /// policy: concealment is a fact about the STREAM and must not tick the + /// decoder-demotion streak. + pub(crate) fn take_recovery_request(&mut self) -> bool { + std::mem::take(&mut self.want_recovery) + } + + /// Feed one complete access unit. + /// + /// One access unit, at most one DISPLAYABLE frame out. On AV1 the access unit is + /// a temporal unit and the decoder may decode several frames from it — hidden + /// frames included, which are never declared displayable and therefore never + /// reach this ledger at all. Anything a single AU makes displayable beyond the + /// first waits in [`Self::deliverable`] for the next call, bounded by + /// [`MAX_DELIVERABLE`]. + /// + /// `Ok(Some)` = a display-ready picture. `Ok(None)` = no picture this AU, which + /// covers three unrelated things and the caller treats all three the same + /// (its no-output/re-anchor machinery): the decoder + /// buffered without output, an H.265 RASL picture was skipped after an open-GOP + /// join, or the AU's plan needed CONCEALMENT and its output was released + /// unshown. `Err` = the DECODER is in trouble — a Vulkan/session error, an AV1 + /// reference the plan could not resolve (which AV1 refuses rather than + /// conceals — module doc), an AV1 temporal unit skipped in full while the + /// decoder waits for the next key frame (the H.26x planners' `AwaitingIdr` + /// under another name), or a driver `RESULT_STATUS` verdict of Failed on a + /// prior frame — which the caller's streak/demotion machinery is entitled to + /// act on. + /// + /// That split is the M4 recovery policy and it is deliberate (module doc): + /// concealment says the STREAM lost data, not that this decoder is failing, so + /// it raises [`Self::take_recovery_request`] instead of an error. The ask + /// reaches the host at the same moment and through the same 100 ms throttle it + /// always did; what it no longer does is spend a life on the demotion streak + /// and cost the session its hardware rung on a lossy link. + /// + /// A skipped RASL picture is not trouble at all: the decoder never turns + /// `h265::PlanError::RaslSkipped` into a `VkDecodeError` and clears the warning + /// ledger on its way out, so the concealment branch cannot fire on it either. + /// Nothing is released unshown and no re-anchor is asked for (module doc; + /// pf-bitstream `h265`). + /// + /// Ordering: the CURRENT AU decodes FIRST — the planner's reference state must + /// advance even when a PRIOR frame's status turns out Failed, or the recovery + /// IDR would land on a decoder that skipped an AU and reports a phantom + /// reference gap. The prior-frame verdicts are checked after; a corrupt verdict + /// costs exactly this one AU's output (released unshown), never parser state. + pub(crate) fn decode(&mut self, au: &[u8]) -> Result> { + self.drain_releases(); + + // Fault injection, at the last possible moment before the decoder: a + // faulted AU is byte-for-byte what a lossy network would have delivered, + // so every detector below sees the real thing rather than a special case. + // Inert (and free — no branch cost worth naming, no copy) unless armed. + let faulted; + let au = match self.fault.as_mut().map(|f| f.apply(au)) { + None | Some(pf_vkdecode::FaultAction::Pass) => au, + Some(pf_vkdecode::FaultAction::Drop) => { + tracing::warn!(len = au.len(), "PUNKTFUNK_AU_FAULT: dropping this AU"); + // Never fed, so nothing decodes and nothing is display-ready — + // the same observable state a lost AU produces. The NEXT AU is + // where detection happens. + // + // Prior frames' verdicts still have to SETTLE here, though, or a + // fault run defers every one of them by an AU and the query slots + // sit unread meanwhile. What is deliberately NOT folded is a + // clean verdict: the health ledger holds one entry per AU the + // decoder was FED, and an AU that never reached it is no evidence + // that anything is healthy — folding `note(false, false, 0)` here + // would reset the concealed run on the very AU that was lost. + let verdicts = self.settle_statuses(); + if verdicts.total() > 0 { + self.health.note(false, false, verdicts.total()); + return Err(self.status_error(verdicts)); + } + return Ok(None); + } + Some(pf_vkdecode::FaultAction::Corrupt(bytes)) => { + tracing::warn!( + len = au.len(), + corrupted_len = bytes.len(), + "PUNKTFUNK_AU_FAULT: corrupting this AU" + ); + faulted = bytes; + &faulted[..] + } + }; + + // A REFUSAL is the loudest thing this lane can say, and it has to reach + // the health ledger before it reaches the caller. Folded here rather than + // after the `?` because there is no after: a `PlanError` + // (`Parse`/`OutsideEnvelope`/`AwaitingIdr`/`NoActiveParamSet`) or a + // Vulkan/session failure returns straight out, and until M4's review this + // path incremented nothing at all — so a rung refusing EVERY AU (a host + // renegotiating outside the envelope: a frozen screen) reported + // `damaged 0 · failed 0 · run 0` and printed no integrity line whatsoever. + // A clean bill of health on a decoder that decoded nothing is the exact + // failure this program exists to end. + // + // Prior frames' status verdicts settle first, for the same reason the + // clean path settles before it folds: the refusal costs this AU, and the + // frames already shipped still owe their verdicts. + let delivered = match self.dec.decode(au) { + Ok(delivered) => delivered, + Err(e) => { + // NOTHING from a refused AU reaches the screen — the same rule the + // concealment branch below keeps, and it has to be enforced here + // too because a refusal can STRAND a frame inside the decoder. + // AV1's `decode_inner` settles each plan of a multi-frame temporal + // unit in turn, so frame 1 can already sit in `ready` when frame 2 + // fails; left there, the NEXT access unit pops it out of + // `take_ready` and ships it with an empty warning ledger. That + // would put a picture from a REFUSED unit on screen, clear the + // demotion streak with it, and set `video.rs`'s `delivered` — + // reporting a rung as working on exactly the stream shapes that + // refuse every AU. + // + // On H.264/H.265 this also catches the pictures `recover_dpb` + // FLUSHES out of the DPB on the AU after a failure (that AU then + // answers `AwaitingIdr`, so it lands here). Releasing them costs + // nothing observable: they were decoded BEFORE the loss, they + // arrive while the pump's freeze is armed, and the freeze gate + // withholds a non-keyframe there anyway — `session.rs` goes further + // and discards their recovery marks by `decode_order` for exactly + // this reason. The pool image comes back an access unit sooner. On + // a punktfunk stream the set is empty regardless: zero reorder means + // the DPB buffers no output to flush. + while let Some(frame) = self.dec.take_ready() { + if let Err(e) = self.dec.release_frame(&frame, false) { + tracing::debug!(error = %e, "releasing a stranded frame failed"); + } + } + // …and the unit's warnings go with it. They describe a plan whose + // frames were all released unshown; carried over, the NEXT AU would + // read them as its own fresh damage. + let _ = self.dec.take_warnings(); + let verdicts = self.settle_statuses(); + self.health.note(false, true, verdicts.total()); + tracing::warn!( + error = %e, + driver_failed = verdicts.driver_failed, + "native decode refused the access unit" + ); + return Err(anyhow!("decode: {e}")); + } + }; + let warnings = self.dec.take_warnings(); + // Everything this AU made display-ready, oldest first (`take_ready` drained + // so burst outputs are never stranded inside the decoder). + let mut fresh: Vec = Vec::new(); + if let Some(frame) = delivered { + fresh.push(frame); + } + while let Some(frame) = self.dec.take_ready() { + fresh.push(frame); + } + + let verdicts = self.settle_statuses(); + // ONLY integrity warnings are concealment (see [`PlanWarnings`]): a + // spec-legal envelope signal — h265's `NonZeroReorder` on every SPS + // activation, h264's `Mmco5Rebase` — is an AU the planner planned in FULL, + // and dropping its frame would hitch the picture at every renegotiation. + let integrity = warnings.integrity(); + let concealed = !integrity.is_empty(); + // One fold per AU, whatever the verdict: a clean AU is what ENDS a run, + // and a counter that only ever counts damage cannot tell a lossy link + // apart from a stream that never came back. + self.health.note(concealed, false, verdicts.total()); + if concealed || verdicts.total() > 0 { + // Concealment planned into THIS AU, or a bad status verdict on a + // PRIOR frame (driver-reported corruption — the Ally X class, + // invisible to libavcodec's query-less decoder — or a status that could + // not be established at all): this call's output is released unshown + // either way, because the picture is not fit to present. + for frame in fresh { + if let Err(e) = self.dec.release_frame(&frame, false) { + tracing::debug!(error = %e, "releasing an unshown frame failed"); + } + } + if verdicts.total() > 0 { + // A verdict about the DECODER rather than the stream: an error, + // streak-eligible, at the volume libavcodec's reference-miss errors + // had (never quieter). + return Err(self.status_error(verdicts)); + } + warnings.warn_concealment(integrity.len()); + self.want_recovery = true; + return Ok(None); + } + if !warnings.is_empty() { + warnings.warn_planned_in_full(); + } + + self.deliverable.extend(fresh); + // This AU's frame comes off the FRONT first: the bound is on the CARRY-OVER + // (see [`trim_deliverable`]), so a unit that produced two outputs ships the + // first and holds the second rather than dropping the first to ship the + // second. + let shipped = self.deliverable.pop_front().map(|frame| self.ship(frame)); + // The queue can only ever hand ONE frame per AU to the caller, so anything + // it cannot drain is a frame holding a pool image forever — see + // [`MAX_DELIVERABLE`]. Inert on every stream a punktfunk host emits. + let queued = self.deliverable.len(); + for frame in trim_deliverable(&mut self.deliverable, MAX_DELIVERABLE) { + self.health.note_dropped(); + // Rate-limited, because the shape this fires on is a stream producing a + // surplus frame on EVERY access unit: unthrottled that is a warn per + // frame at frame rate, which buries the log it is supposed to explain. + // The first one carries the diagnosis; the rest are a running count. + // `queued` is the PRE-trim depth — the number that says how far past the + // bound the queue actually got. Read after the trim it would be the + // constant `MAX_DELIVERABLE` every single time. + if self.health.dropped == 1 || self.health.dropped % DROP_WARN_EVERY == 0 { + tracing::warn!( + queued, + dropped_total = self.health.dropped, + poc = frame.poc, + "native decode: more display-ready frames than the pump can take — \ + dropping the oldest so its pool image is not held forever" + ); + } + if let Err(e) = self.dec.release_frame(&frame, false) { + tracing::debug!(error = %e, "releasing an over-queued frame failed"); + } + } + Ok(shipped) + } + + /// Wrap a delivered [`DecodedVkFrame`] for the presenter and enter it into the + /// shipped ledger (the original stays here — release/poll need it). + fn ship(&mut self, frame: DecodedVkFrame) -> NativeVkFrame { + let seq = self.next_seq; + self.next_seq += 1; + let token = NativeReleaseToken { + seq, + generation: frame.generation, + presented: false, + }; + let native = project_frame( + &frame, + NativeReleaseGuard::new( + self.release_tx + .as_ref() + .expect("release_tx lives until Drop") + .clone(), + token, + ), + ); + self.outstanding.push(Shipped { + seq, + frame, + released: false, + presented: false, + resolved: false, + polls_after_release: 0, + }); + native + } + + /// Bounded wait for a shipped frame's decode-complete signal — the pump's + /// sampled decode-latency stat (`Decoder::wait_hw_decoded`), one frame per + /// stats window. The raw pair names a frame still in the shipped ledger (the + /// pump waits on the same thread that just shipped it, before any settle + /// could retire it); the ledger lookup is the liveness proof — an unreleased + /// frame pins its pool, so a pair matching nothing (already settled, or a + /// stray) just declines the sample instead of touching unknown handles. + pub(crate) fn wait_timeline(&self, sem: u64, value: u64, timeout_ns: u64) -> bool { + self.outstanding + .iter() + .find(|s| s.frame.semaphore.as_raw() == sem && s.frame.value == value) + .is_some_and(|s| self.dec.wait_decoded(&s.frame, timeout_ns)) + } + + /// Drain the release channel, marking returned frames (release itself waits for + /// the status read — see [`Self::settle_statuses`]). + fn drain_releases(&mut self) { + while let Ok(token) = self.release_rx.try_recv() { + if !note_token(&mut self.outstanding, token) { + tracing::debug!( + seq = token.seq, + generation = token.generation, + "release token without an outstanding frame" + ); + } + } + } + + /// The error a bad status verdict surfaces as, worded for the device it came + /// from: a driver that reported corruption is named as such, a device that + /// cannot report one is not blamed for a verdict it never gave. + fn status_error(&self, verdicts: StatusVerdicts) -> anyhow::Error { + if verdicts.driver_failed > 0 { + anyhow!( + "driver reported decode corruption on {} prior frame(s) \ + (RESULT_STATUS_ONLY query) — re-anchor needed", + verdicts.driver_failed + ) + } else { + anyhow!( + "decode status unreadable on {} prior frame(s) (this device answers \ + no RESULT_STATUS queries — the verdict degraded to the decode \ + timeline) — re-anchor needed", + verdicts.unreadable + ) + } + } + + /// Poll the status query of every unresolved shipped frame (non-blocking) and + /// release the ones that are both status-settled and token-returned. Returns the + /// frames that NEWLY read `Failed`, split by whether this device can produce a + /// driver verdict at all — see [`StatusVerdicts`]. + /// + /// Polling an unreleased frame is always sound: its slot is pinned until + /// `release_frame`, so the query slot it names cannot have been recycled under it + /// (the false-`Failed` a recycled slot would read). + fn settle_statuses(&mut self) -> StatusVerdicts { + let mut verdicts = StatusVerdicts::default(); + // A device fact, read once: it decides which KIND of verdict a `Failed` + // read below is (`StatusVerdicts`), never whether the frame is dropped. + let status_queries = self.dec.status_queries(); + let Self { + dec, outstanding, .. + } = self; + for s in outstanding.iter_mut() { + if s.resolved { + continue; + } + // A session rebuild (stream renegotiation) already made this frame stale: + // its SESSION objects (query pool included) are gone — the picture pool + // lives on in the decoder's graveyard while we hold the image, but the + // query verdict is unknowable and poll_status would report the + // conservative Failed — which is NOT driver corruption. Resolve it + // quietly; the rebuild rode an IDR, so the stream has its re-anchor + // already. + if s.frame.generation != dec.generation() { + tracing::debug!( + poc = s.frame.poc, + frame_generation = s.frame.generation, + "outstanding frame outlived its session generation — status unknowable" + ); + s.resolved = true; + continue; + } + match dec.poll_status(&s.frame) { + DecodeStatus::Ok => s.resolved = true, + DecodeStatus::Failed => { + s.resolved = true; + if status_queries { + verdicts.driver_failed += 1; + tracing::warn!( + poc = s.frame.poc, + slot = s.frame.query_slot, + "decode status query: Failed (driver-reported corruption)" + ); + } else { + // No query pool on this device, so nothing here is the + // driver's opinion of the decode: `poll_status` degraded + // to reading the decode timeline and could not establish + // completion (a lost device, an unreadable semaphore). + // The picture is dropped exactly the same way — it is the + // ATTRIBUTION that must not be invented (`StatusVerdicts`). + verdicts.unreadable += 1; + tracing::warn!( + poc = s.frame.poc, + "decode status unreadable — this device answers no \ + RESULT_STATUS queries, so this is a timeline failure, \ + not a driver verdict" + ); + } + } + DecodeStatus::Pending => { + if s.released { + // Token back ⇒ the decode op completed before the presenter's + // sampling ⇒ the query should be readable. Belt, not a path. + s.polls_after_release += 1; + if s.polls_after_release >= MAX_POLLS_AFTER_RELEASE { + tracing::debug!( + poc = s.frame.poc, + "status query still pending after release — giving \ + the slot back with an unknown verdict" + ); + s.resolved = true; + } + } + } + } + } + outstanding.retain(|s| { + if !(s.released && s.resolved) { + return true; + } + match dec.release_frame(&s.frame, s.presented) { + Ok(()) => {} + // Not a best-effort no-op: stale-generation frames release into the + // decoder's graveyard (a rebuild retires a still-held pool INTACT, + // and this very call is what lets it die on its last token). An Err + // is therefore a bookkeeping ghost — a double release — never a + // held image left dangling. + Err(e) => tracing::debug!(error = %e, "release_frame: {e}"), + } + false + }); + verdicts + } +} + +impl Drop for NativeVulkanDecoder { + fn drop(&mut self) { + // Ordering contract: the run loop drops the PRESENTER's frame (its retired + // slot, fence-waited) before joining the pump that owns this backend — so + // by the time this Drop runs, outstanding tokens are either already in the + // channel or arrive imminently; the bounded wait below is for that hand-off, + // not for future GPU work. + // + // Frames never handed to the pump release directly (unsampled). + for frame in std::mem::take(&mut self.deliverable) { + if let Err(e) = self.dec.release_frame(&frame, false) { + tracing::debug!(error = %e, "releasing an undelivered frame failed"); + } + } + // Drop our own sender FIRST: once every shipped guard is gone too, the + // channel reports Disconnected — the "presenter can no longer produce + // tokens" signal that short-circuits the wait instead of burning the full + // budget against a presenter that is already gone. + drop(self.release_tx.take()); + // Wait (bounded) for the presenter to hand back every shipped frame before + // the decoder's Drop destroys the pool images: a returned token proves the + // sampling submission's fence was waited, i.e. no GPU work of the + // presenter's still reads the pools (the decoder's own drain covers only + // decode work). Graveyarded pools ride the same token contract — a + // mid-stream renegotiation retires a still-held pool INTACT, and the + // release calls below route stale-generation frames into the graveyard, + // so those pools too die only once their last presenter fence was waited. + let deadline = Instant::now() + TEARDOWN_BUDGET; + loop { + self.drain_releases(); + let Self { + dec, outstanding, .. + } = self; + outstanding.retain(|s| { + if !s.released { + return true; + } + if let Err(e) = dec.release_frame(&s.frame, s.presented) { + tracing::debug!(error = %e, "teardown release_frame: {e}"); + } + false + }); + if self.outstanding.is_empty() { + break; + } + let now = Instant::now(); + if now >= deadline { + tracing::warn!( + outstanding = self.outstanding.len(), + "native decode teardown: presenter still holds frames past the \ + budget — destroying the pools anyway" + ); + break; + } + match self + .release_rx + .recv_timeout((deadline - now).min(Duration::from_millis(50))) + { + Ok(token) => { + note_token(&mut self.outstanding, token); + } + // Every sender is gone (ours dropped above, every guard dropped): + // no more tokens can EVER arrive — anything still outstanding is a + // bookkeeping ghost, not a held frame. Stop waiting. + Err(mpsc::RecvTimeoutError::Disconnected) => { + if !self.outstanding.is_empty() { + tracing::debug!( + outstanding = self.outstanding.len(), + "release channel disconnected with entries outstanding — \ + no tokens can arrive; proceeding with teardown" + ); + } + break; + } + Err(mpsc::RecvTimeoutError::Timeout) => continue, + } + } + // `self.dec` drops after this body: it drains its own decode-side GPU work + // and destroys any remaining graveyard pools (warned — a forfeit here means + // the presenter kept frames past the budget). + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A delivered frame whose every field carries a DISTINCT non-zero value. + /// + /// Deliberately not "inert handles, zeros elsewhere": [`project_frame`] is a + /// 20-field struct literal lifted out of `ship`, and the bugs it can hide are + /// field SWAPS and DROPS — `crop_x: frame.crop.y`, `semaphore_value: frame.poc + /// as u64`, a `keyframe` that stopped being carried. Against zeros every one of + /// those passes. So: no two numbers here are equal, no boolean is false, and + /// each CICP code point differs from the others. + fn decoded(format: vk::Format, layout: vk::ImageLayout, generation: u64) -> DecodedVkFrame { + DecodedVkFrame { + image: vk::Image::from_raw(0x1001), + format, + view: vk::ImageView::from_raw(0x2001), + plane_views: [ + vk::ImageView::from_raw(0x2002), + vk::ImageView::from_raw(0x2003), + ], + layer: 3, + layout, + coded_width: 1920, + coded_height: 1088, + // A non-origin crop: punktfunk hosts emit origin crops only, but x != y + // here is what makes an x/y swap in the projection visible. + crop: pf_vkdecode::DisplayCrop { + x: 8, + y: 4, + width: 1904, + height: 1072, + }, + // BT.2020 primaries / PQ transfer / a third code point for the matrix, + // so no two CICP fields share a value. + colour: pf_vkdecode::ColourDescription { + colour_primaries: 9, + transfer_characteristics: 16, + matrix_coefficients: 10, + video_full_range: true, + }, + semaphore: vk::Semaphore::from_raw(0x3001), + value: 7, + poc: 5, + is_idr: true, + // Both facts SET and distinct from the defaults, for the same reason + // every other field here is: a projection that dropped the recovery + // mark would silently reinstate the 500 ms freeze on every + // intra-refresh session, and against `false` that passes. + recovery: pf_vkdecode::RecoveryMark { + sei_here: true, + is_recovery_point: true, + }, + // Distinct from every other number here for the same reason: a + // projection that dropped the decode ordinal would make every frame + // look pre-loss (0) and silently disable the local-recovery path. + decode_order: 17, + query_slot: 2, + submission: 11, + picture: 6, + generation, + } + } + + /// A shipped-ledger entry with inert handles — the bookkeeping under test is pure. + fn shipped(seq: u64, generation: u64) -> Shipped { + Shipped { + seq, + // The ledger under test never reads the picture format; NV12 is what an + // H.264 session always delivers. + frame: decoded( + pf_vkdecode::NV12, + vk::ImageLayout::VIDEO_DECODE_DST_KHR, + generation, + ), + released: false, + presented: false, + resolved: false, + polls_after_release: 0, + } + } + + /// Project one frame with a throwaway guard (the channel is the caller's). + fn project(frame: &DecodedVkFrame) -> NativeVkFrame { + let (tx, _rx) = mpsc::channel(); + project_frame( + frame, + NativeReleaseGuard::new( + tx, + NativeReleaseToken { + seq: 0, + generation: frame.generation, + presented: false, + }, + ), + ) + } + + /// The picture format is the STREAM's, and it must reach the presenter intact: + /// H.264 and H.265 Main deliver NV12, Main 10 delivers P010, RExt 4:4:4 delivers + /// the two-plane 4:4:4 formats. The presenter picks bit depth, MSB packing and + /// chroma siting from exactly this number, so a projection that dropped or + /// defaulted it would render a Main 10 picture with 8-bit math — decoded + /// correctly, displayed wrong, and nothing would flag it. + #[test] + fn the_projection_carries_the_pictures_own_format_whatever_the_codec() { + for format in [ + pf_vkdecode::NV12, + pf_vkdecode::P010, + pf_vkdecode::YUV444_8, + pf_vkdecode::YUV444_10, + ] { + let frame = decoded(format, vk::ImageLayout::VIDEO_DECODE_DST_KHR, 1); + assert_eq!( + project(&frame).vk_format, + crate::video::RawVkFormat(format.as_raw()), + "the presenter reads the format off the frame, never off the codec" + ); + } + } + + /// EVERY field of the projection, against a frame whose values are all distinct + /// (see [`decoded`]): the display crop is what the presenter shows, the coded + /// extent is what it must divide by (the 1088-row lesson), the crop ORIGIN is + /// what its UV-scale path assumes is (0,0), the timeline pair is what it waits, + /// the CICP quadruple is what it does colour maths with, and the decode layout is + /// what it has to restore after sampling. A swap or a drop among any of them is a + /// silently wrong picture, so the list here is deliberately exhaustive — if + /// `NativeVkFrame` grows a field, this test should stop compiling before it can + /// go unchecked. + #[test] + fn the_projection_carries_every_field_the_presenter_can_no_longer_look_up() { + let frame = decoded(pf_vkdecode::P010, vk::ImageLayout::VIDEO_DECODE_DST_KHR, 4); + let p = project(&frame); + // Destructured, not field-accessed: a NEW field on NativeVkFrame breaks this + // pattern and lands the author right here. + let NativeVkFrame { + image, + vk_format, + plane_views, + layer, + layout, + semaphore, + semaphore_value, + generation, + width, + height, + coded_width, + coded_height, + crop_x, + crop_y, + color, + keyframe, + poc, + recovery, + decode_order, + guard: _, + } = p; + assert_eq!(image, 0x1001); + assert_eq!( + vk_format, + crate::video::RawVkFormat(pf_vkdecode::P010.as_raw()) + ); + assert_eq!( + plane_views, + [0x2002, 0x2003], + "the plane views, in order — NOT the whole-image view (0x2001)" + ); + assert_eq!(layer, 3, "the picture's array layer, not slot 0"); + assert_eq!(layout, NativeVkLayout::DecodeDst); + assert_eq!(semaphore, 0x3001); + assert_eq!( + semaphore_value, 7, + "the frame's timeline value — not its POC (5)" + ); + assert_eq!(generation, 4); + assert_eq!((width, height), (1904, 1072), "the display crop's SIZE"); + assert_eq!( + (coded_width, coded_height), + (1920, 1088), + "the allocated surface — the UV-scale denominator" + ); + assert_eq!((crop_x, crop_y), (8, 4), "the crop ORIGIN, x then y"); + assert_eq!(color.primaries, 9); + assert_eq!(color.transfer, 16); + assert_eq!(color.matrix, 10); + assert!(color.full_range); + assert!( + keyframe, + "is_idr rides through as the pump's re-anchor signal" + ); + assert_eq!(poc, 5); + assert_eq!( + recovery, + punktfunk_core::reanchor::LocalRecovery { + sei_here: true, + is_recovery_point: true, + }, + "the recovery point SEI's verdict reaches the gate — it is the ONLY \ + clean point an intra-refresh session has" + ); + assert_eq!( + decode_order, 17, + "the decode ordinal rides along — without it the pump cannot tell a \ + frame decoded before a loss from one decoded after it" + ); + + // Coincide mode: the picture IS a DPB slot, so the presenter must put the + // layer back in DPB layout after sampling. + let dpb = project(&decoded( + pf_vkdecode::NV12, + vk::ImageLayout::VIDEO_DECODE_DPB_KHR, + 4, + )); + assert_eq!(dpb.layout, NativeVkLayout::DecodeDpb); + } + + /// The construction-time shape refusal, device-independent half. A negotiated + /// shape pf-vkdecode has no picture format for must be refused where + /// `Decoder::new` can still walk to the next rung — NOT discovered at the first AU, + /// where the only exit is an error streak that demotes PAST that rung (and on + /// NVIDIA/Linux, where VAAPI is unusable, straight to software). + #[test] + fn a_stream_shape_with_no_native_picture_format_is_refused_at_construction() { + use crate::video::StreamFormat; + let f = |chroma, bit_depth| { + picture_format( + "HEVC", + StreamFormat { + chroma_format_idc: chroma, + bit_depth, + }, + ) + }; + // What the envelope DOES admit resolves, and to the right format — Main, + // Main 10 and both RExt 4:4:4 depths. + assert_eq!(f(1, 8).unwrap(), pf_vkdecode::NV12); + assert_eq!(f(1, 10).unwrap(), pf_vkdecode::P010); + assert_eq!(f(3, 8).unwrap(), pf_vkdecode::YUV444_8); + assert_eq!(f(3, 10).unwrap(), pf_vkdecode::YUV444_10); + assert_eq!( + picture_format("HEVC", StreamFormat::SDR_420_8).unwrap(), + pf_vkdecode::NV12, + "the default/older-host shape is the ordinary one" + ); + // 4:2:2 and monochrome are legal H.265 with no output plumbing here. + assert!(f(2, 8).is_err(), "4:2:2"); + assert!(f(0, 8).is_err(), "monochrome"); + // 12-bit has no output format either, and a depth BELOW 8 must not wrap + // around into a plausible `bit_depth_luma_minus8`. + assert!(f(1, 12).is_err(), "12-bit"); + assert!( + f(1, 0).is_err(), + "an absurd depth refuses, never underflows" + ); + assert!(f(3, 6).is_err()); + } + + /// AV1's construction-time shape gate is the SAME envelope, and it has to stay + /// that way: [`picture_format`] is what refuses first, and one line later + /// `VkAv1Decoder::probe_stream_support` builds an `Av1ProfileKey`, which refuses + /// exactly the same set (monochrome, 4:2:2, the planner's 4:4:0 sentinel, any + /// depth but 8/10). Two gates that disagreed would mean either a shape refused + /// here that the device could have decoded, or — worse — a shape admitted here + /// and then refused mid-stream, where the exit is an error streak past this rung. + /// + /// The label is checked too, because it is the only thing a support engineer + /// reading the refusal has to tell an AV1 session's refusal from an HEVC one. + #[test] + fn the_av1_shape_gate_admits_exactly_what_pf_vkdecodes_av1_profile_key_admits() { + use crate::video::StreamFormat; + let f = |chroma, bit_depth| { + picture_format( + "AV1", + StreamFormat { + chroma_format_idc: chroma, + bit_depth, + }, + ) + }; + // AV1 Main 8-bit / Main 10 / High 4:4:4 at both depths — every combination + // `Av1ProfileKey::from_negotiated` maps to a profile. + assert_eq!(f(1, 8).unwrap(), pf_vkdecode::NV12); + assert_eq!(f(1, 10).unwrap(), pf_vkdecode::P010); + assert_eq!(f(3, 8).unwrap(), pf_vkdecode::YUV444_8); + assert_eq!(f(3, 10).unwrap(), pf_vkdecode::YUV444_10); + // …and the ones it refuses. + assert!(f(0, 8).is_err(), "monochrome"); + assert!(f(2, 8).is_err(), "4:2:2"); + assert!( + f(4, 8).is_err(), + "the planner's 4:4:0 sentinel is not 4:4:4" + ); + assert!(f(1, 12).is_err(), "12-bit"); + assert!( + f(1, 0).is_err(), + "an absurd depth refuses, never underflows" + ); + + // The refusal names the codec — the label is the whole reason this function + // takes one. + let err = format!("{:#}", f(2, 8).unwrap_err()); + assert!(err.contains("AV1"), "{err}"); + let hevc = format!( + "{:#}", + picture_format( + "HEVC", + StreamFormat { + chroma_format_idc: 2, + bit_depth: 8 + } + ) + .unwrap_err() + ); + assert!(hevc.contains("HEVC"), "{hevc}"); + + // The probe's OWN gate, asked the same questions through pf-vkdecode's + // profile key: this is the agreement the comment above claims, asserted + // rather than assumed. + for (chroma, depth) in [(1u8, 8u8), (1, 10), (3, 8), (3, 10)] { + assert!( + pf_vkdecode::Av1ProfileKey::from_negotiated(chroma, depth, AV1_PROBE_FILM_GRAIN) + .is_ok(), + "{chroma}/{depth} passes here, so it must pass the probe's key too" + ); + } + for (chroma, depth) in [(0u8, 8u8), (2, 8), (4, 8), (1, 12), (1, 0)] { + assert!( + pf_vkdecode::Av1ProfileKey::from_negotiated(chroma, depth, AV1_PROBE_FILM_GRAIN) + .is_err(), + "{chroma}/{depth} refuses here, so the probe's key must refuse it too" + ); + } + } + + /// The deliverable queue can only hand ONE frame per AU to the pump, and every + /// frame waiting in it pins a picture-pool image. A stream that made two + /// pictures displayable per access unit would grow it by one per AU until the + /// pool ran out, after which every AU refuses with `NoFreeSlot`, three in a + /// second demote the rung, and nothing in the log would name a queue that could + /// never drain as the cause. + /// + /// ⚠ Which stream that is, precisely — the earlier claim here was wrong and the + /// crate's own golden disproves it. AV1 permits exactly ONE shown frame per + /// temporal unit, so a `show_existing_frame` can never ride alongside a shown + /// frame; pf-bitstream's conformance test pins `shown = 250` across 250 units + /// with `show_existing = 0`, and its 24 two-frame units are a hidden ALTREF plus + /// the frame that shows it. The real producers are H.265 bumping after a + /// reordering stretch, and — as defence in depth — a non-conformant or + /// multi-operating-point AV1 stream. The bound is worth having for those; it is + /// not the routine case. + /// + /// So the bound drops from the FRONT: by the time the queue is this deep the + /// oldest frame is several AUs stale, and the stage after this one (the pump's + /// `force_send`) is itself newest-wins. Dropping the newest instead would keep + /// the stalest picture and present the stream in ever-lagging order. + /// + /// ⚠ What this exercises is [`trim_deliverable`] ALONE — the pure half. The + /// wiring it cannot see is the caller's: that the trim runs AFTER this AU's + /// frame is taken off the front, that every dropped frame is handed to + /// `release_frame(.., false)`, and that [`DecodeHealth::dropped`] counts it. + /// Replace those with `mem::forget` and this test stays green; only a device + /// (or the `NoFreeSlot` a leaked pool image eventually produces) would notice. + #[test] + fn the_deliverable_queue_drops_its_oldest_rather_than_pinning_pool_images_forever() { + let mut q: std::collections::VecDeque = (0..5) + .map(|i| { + let mut f = decoded(pf_vkdecode::NV12, vk::ImageLayout::VIDEO_DECODE_DST_KHR, 1); + // The only field this test reads — distinct per frame so "which + // ones were dropped" is decidable rather than merely counted. + f.poc = i; + f + }) + .collect(); + + let dropped = trim_deliverable(&mut q, 3); + assert_eq!( + dropped.iter().map(|f| f.poc).collect::>(), + vec![0, 1], + "the OLDEST two come back for release — not the newest" + ); + assert_eq!( + q.iter().map(|f| f.poc).collect::>(), + vec![2, 3, 4], + "…and what survives stays in display order" + ); + + // At or below the bound nothing moves: on every stream a punktfunk host + // emits this queue is empty, and the bound must be invisible there. + assert!(trim_deliverable(&mut q, 3).is_empty()); + assert_eq!(q.len(), 3); + + // The bound is DERIVED, and this is the arithmetic it is derived from: a + // queued frame holds a picture-pool image exactly like a shipped one, and + // pf-vkdecode sizes that pool at `required_slots + HOLD_HEADROOM`. So the + // queue at its bound PLUS what the pipeline itself holds must fit inside + // the headroom — otherwise the pool runs out, every AU refuses with + // `NoFreeSlot`, and the bound caps memory without preventing the failure it + // names. Pinned against pf-vkdecode's own constant so a hardcoded depth here + // (this shipped at 8, against a headroom of 8) fails the build rather than a + // field session. + assert!( + MAX_DELIVERABLE + PIPELINE_HOLD <= pf_vkdecode::HOLD_HEADROOM as usize, + "a queue of {MAX_DELIVERABLE} on top of the pipeline's {PIPELINE_HOLD} \ + exceeds the {} frames the picture pool is sized for", + pf_vkdecode::HOLD_HEADROOM + ); + + // The PRODUCTION bound, at the carry-over depth it is derived to: one AU's + // surplus frame is held (the burst this queue exists for), a second AU's is + // not. Asserted against `MAX_DELIVERABLE` itself so a change to + // `PIPELINE_HOLD` lands here rather than in a field log. + let mut q: std::collections::VecDeque = (0..MAX_DELIVERABLE) + .map(|i| { + let mut f = decoded(pf_vkdecode::NV12, vk::ImageLayout::VIDEO_DECODE_DST_KHR, 1); + f.poc = i as i32; + f + }) + .collect(); + assert!( + trim_deliverable(&mut q, MAX_DELIVERABLE).is_empty(), + "a queue AT the bound is exactly what a two-output AU leaves behind" + ); + assert_eq!(q.len(), MAX_DELIVERABLE); + + // A zero bound drains rather than looping or panicking. Reachable only + // through a caller that asks for it — teardown does NOT come through here + // (`Drop` empties the queue with `mem::take` and releases each frame), so + // this pins termination and the empty-queue edge, not a production path. + let drained = q.len(); + assert_eq!(trim_deliverable(&mut q, 0).len(), drained); + assert!(q.is_empty()); + assert!(trim_deliverable(&mut q, 0).is_empty(), "and it terminates"); + } + + #[test] + fn release_tokens_mark_their_frame_and_tolerate_strays() { + let mut outstanding = vec![shipped(0, 1), shipped(1, 1)]; + assert!(note_token( + &mut outstanding, + NativeReleaseToken { + seq: 1, + generation: 1, + presented: true, + } + )); + assert!(!outstanding[0].released); + assert!(outstanding[1].released); + assert!( + outstanding[1].presented, + "the token's presented flag rides into the ledger (the decoder waits \ + the presenter's value+1 write-back only when it was really enqueued)" + ); + // A stray token (frame already settled away — e.g. a post-demotion drain) + // matches nothing and must not panic or mis-mark. + assert!(!note_token( + &mut outstanding, + NativeReleaseToken { + seq: 7, + generation: 1, + presented: false, + } + )); + assert!(!outstanding[0].released); + } + + #[test] + fn the_guard_sends_its_token_exactly_once_on_drop() { + let (tx, rx) = mpsc::channel(); + let token = NativeReleaseToken { + seq: 42, + generation: 3, + presented: false, + }; + let guard = NativeReleaseGuard::new(tx, token); + assert!( + rx.try_recv().is_err(), + "nothing is sent while the frame lives" + ); + drop(guard); + assert_eq!(rx.try_recv().ok(), Some(token), "drop sends the token"); + assert!(rx.try_recv().is_err(), "exactly once"); + } + + #[test] + fn a_dropped_unpresented_frame_still_releases_through_the_same_guard() { + // The newest-wins channel/store displacement path: the frame never reaches a + // present, but dropping it must still return its slot. + let (tx, rx) = mpsc::channel(); + let frame = NativeVkFrame { + image: 0, + vk_format: crate::video::RawVkFormat(pf_vkdecode::NV12.as_raw()), + plane_views: [0; 2], + layer: 0, + layout: NativeVkLayout::DecodeDst, + semaphore: 0, + semaphore_value: 0, + generation: 5, + width: 1920, + height: 1080, + coded_width: 1920, + coded_height: 1088, + crop_x: 0, + crop_y: 0, + color: ColorDesc { + primaries: 2, + transfer: 2, + matrix: 2, + full_range: false, + }, + keyframe: true, + poc: 0, + recovery: punktfunk_core::reanchor::LocalRecovery::NONE, + decode_order: 1, + guard: NativeReleaseGuard::new( + tx, + NativeReleaseToken { + seq: 9, + generation: 5, + presented: false, + }, + ), + }; + drop(frame); + assert_eq!( + rx.try_recv().ok(), + Some(NativeReleaseToken { + seq: 9, + generation: 5, + presented: false, + }), + "an unpresented drop reports presented=false — the decoder must not \ + wait a value+1 write-back that was never enqueued" + ); + } + + #[test] + fn a_dead_channel_is_ignored_not_fatal() { + // Demotion mid-stream: the backend (and its Receiver) are gone while the + // presenter still holds a frame — its drop must be a no-op, not a panic. + let (tx, rx) = mpsc::channel(); + drop(rx); + let guard = NativeReleaseGuard::new( + tx, + NativeReleaseToken { + seq: 1, + generation: 1, + presented: false, + }, + ); + drop(guard); // must not panic + } + + /// Concealment is the INTEGRITY warnings, not "any warning at all". + /// + /// h265's `NonZeroReorder` is emitted on the AU that ACTIVATES an SPS with + /// `sps_max_num_reorder_pics > 0` — the opening IDR, and the fresh IDR at every + /// ABR resolution change. pf-bitstream documents it as spec-legal and fully + /// planned (C.5.2 bumping honours the reordering) and excludes it from its own + /// integrity set. Treating it as concealment releases that IDR UNSHOWN, errors, + /// and begs the host for a keyframe: a visible hitch at every renegotiation, on + /// a stream the planner says it planned correctly. + #[test] + fn a_spec_legal_envelope_warning_is_not_concealment() { + use pf_vkdecode::H265PlanWarning as H265; + use pf_vkdecode::PlanWarning as H264; + + // The case from the field: an SPS activation, nothing else. + let reorder = PlanWarnings::H265(vec![H265::NonZeroReorder { + max_num_reorder_pics: 1, + }]); + assert!(!reorder.is_empty(), "it IS a warning and IS logged"); + assert!( + reorder.integrity().is_empty(), + "…but it is not concealment: the frame must be shown, not dropped" + ); + + // h264's twin: an MMCO 5 was planned in full too (the plan carries the + // pre-rebase 8.2.1 values). + let mmco5 = PlanWarnings::H264(vec![H264::Mmco5Rebase]); + assert!(!mmco5.is_empty()); + assert!(mmco5.integrity().is_empty()); + + // Everything that means a reference or a slice was LOST still is — this is + // the H.264 behaviour the hardware-verified path shipped with. + for w in [ + H264::FrameNumGap { + expected: 4, + got: 7, + }, + H264::MissingReference { + context: "list0", + detail: "poc 12".into(), + }, + H264::TruncatedAu { offset: 900 }, + ] { + let warnings = PlanWarnings::H264(vec![w]); + assert_eq!(warnings.integrity().len(), 1, "damage is concealment"); + } + for w in [ + H265::MissingReference { + context: "StCurrBefore", + detail: "poc 12".into(), + }, + H265::TruncatedAu { offset: 900 }, + ] { + let warnings = PlanWarnings::H265(vec![w]); + assert_eq!(warnings.integrity().len(), 1); + } + + // Mixed AU: the damage decides, and the count the error reports is the + // damage count — the spec-legal companion rides along in the log only. + let mixed = PlanWarnings::H265(vec![ + H265::NonZeroReorder { + max_num_reorder_pics: 2, + }, + H265::TruncatedAu { offset: 12 }, + ]); + assert_eq!(mixed.len(), 2); + assert_eq!(mixed.integrity().len(), 1); + } + + /// The AV1 arm of the same split (M7). AV1's planner has no spec-legal + /// companion to `NonZeroReorder`/`Mmco5Rebase` — it announces no reorder + /// envelope and has no MMCO to rebase — so every warning it emits is damage and + /// every one of them must conceal. + /// + /// Worth asserting despite being "all true", because the failure it catches is + /// silent: an arm wired to the wrong predicate (or to an empty vector) would + /// SHOW a picture the stream lost data for and ask for no re-anchor, which is + /// exactly the invisible-damage shape this program exists to end. The one that + /// carries most of the weight is `MissingShowExisting` — a frame that decoded + /// nothing and displayed nothing — because it is the one an author is most + /// likely to read as harmless. + #[test] + fn every_av1_warning_conceals_because_av1_has_no_spec_legal_signal() { + use pf_vkdecode::Av1PlanWarning as Av1; + + for w in [ + Av1::MissingReference { + slot: 3, + ref_index: 1, + }, + Av1::MissingShowExisting { slot: 5 }, + Av1::TruncatedAu { offset: 900 }, + ] { + let warnings = PlanWarnings::Av1(vec![w.clone()]); + assert!(!warnings.is_empty()); + assert_eq!( + warnings.integrity().len(), + 1, + "{w:?} means the picture is not fit to present" + ); + } + + // The whole vocabulary at once — the count the concealment log reports is + // the damage count, and here it is the full list. + let all = PlanWarnings::Av1(vec![ + Av1::MissingReference { + slot: 0, + ref_index: 0, + }, + Av1::MissingShowExisting { slot: 1 }, + Av1::TruncatedAu { offset: 4 }, + ]); + assert_eq!((all.len(), all.integrity().len()), (3, 3)); + + // A clean AU is clean: the AV1 arm must not manufacture concealment out of + // an empty ledger, which is what a stream with no damage produces on every + // single access unit. + assert!(PlanWarnings::Av1(Vec::new()).is_empty()); + assert!(PlanWarnings::Av1(Vec::new()).integrity().is_empty()); + } + + /// The counter a support engineer reads first. A total alone cannot tell a + /// lossy link that keeps recovering apart from a stream that went down and + /// stayed down — `damaged 40 · run 0` and `damaged 40 · run 40` are the same + /// number and completely different problems. So the run must climb only while + /// damage is CONSECUTIVE, and the worst run must survive the recovery that + /// clears it (a once-per-second sample of `run` misses the bad moment almost + /// every time). + #[test] + fn the_concealed_run_separates_a_lossy_link_from_a_stream_that_never_came_back() { + let mut h = DecodeHealth::default(); + // A lossy link: single damaged AUs with clean stretches between. + for _ in 0..3 { + h.note(true, false, 0); + h.note(false, false, 0); + h.note(false, false, 0); + } + assert_eq!(h.damaged, 3); + assert_eq!(h.run, 0, "the last AU was clean"); + assert_eq!(h.worst_run, 1, "…and no two damaged AUs were adjacent"); + + // A stream that stopped recovering. + let mut h = DecodeHealth::default(); + for _ in 0..7 { + h.note(true, false, 0); + } + assert_eq!((h.damaged, h.run, h.worst_run), (7, 7, 7)); + // One clean AU ends the run but never the record. + h.note(false, false, 0); + assert_eq!((h.damaged, h.run, h.worst_run), (7, 0, 7)); + } + + /// A REFUSED AU — the decoder answering `Err` rather than concealing — has to + /// reach the ledger, and has to be told apart from concealment. + /// + /// This is the shape the M4 review found reporting a clean bill of health: a + /// host renegotiating outside the decode envelope makes every `plan_au` fail, + /// the picture freezes, and before this counter existed the stats surface read + /// `damaged 0 · failed 0 · run 0` and printed no integrity line at all. The + /// two counts must stay separate because they say opposite things about the + /// RUNG: concealment means the decoder coped with a damaged stream, refusal + /// means it could not run. + #[test] + fn a_rung_refusing_every_au_cannot_report_a_clean_bill_of_health() { + let mut h = DecodeHealth { + status_queries: true, + ..DecodeHealth::default() + }; + for _ in 0..5 { + h.note(false, true, 0); + } + assert_eq!(h.refused, 5, "every refusal is counted"); + assert_eq!(h.damaged, 0, "and none of them is concealment"); + assert_eq!(h.failed, 0, "nor a driver verdict — the driver never ran"); + assert_eq!( + (h.run, h.worst_run), + (5, 5), + "a refused AU is as absent from the screen as a concealed one" + ); + // A single good AU ends the run; the totals stand. + h.note(false, false, 0); + assert_eq!((h.refused, h.run, h.worst_run), (5, 0, 5)); + } + + /// The three verdicts count apart and share one run — because "the bitstream + /// arrived incomplete", "the decoder refused it" and "the hardware failed the + /// decode" have three different causes and three different fixes, while "did + /// the picture ever come back" has one answer. + #[test] + fn concealment_refusal_and_driver_failure_are_three_separate_counts() { + let mut h = DecodeHealth { + status_queries: true, + ..DecodeHealth::default() + }; + h.note(true, false, 0); + h.note(false, true, 0); + h.note(false, false, 2); + assert_eq!((h.damaged, h.refused, h.failed), (1, 1, 2)); + assert_eq!((h.run, h.worst_run), (3, 3), "one unbroken run of three"); + } + + /// A driver `Failed` verdict counts apart from concealment and extends the same + /// run. Apart, because "the bitstream arrived incomplete" and "the hardware + /// could not decode what arrived" have different causes and different fixes, + /// and collapsing them is how "the stream is fine, it's your GPU" arguments + /// start. Same run, because a frame the driver failed is as absent from the + /// screen as a concealed one — and "did the picture ever come back" is what the + /// run answers. + #[test] + fn driver_failures_count_separately_but_share_the_run() { + let mut h = DecodeHealth { + status_queries: true, + ..DecodeHealth::default() + }; + h.note(false, false, 2); // two prior frames reported corrupt at once + assert_eq!((h.damaged, h.failed, h.run), (0, 2, 1)); + h.note(true, false, 1); // and an AU that ALSO needed concealment + assert_eq!((h.damaged, h.failed, h.run), (1, 3, 2)); + h.note(false, false, 0); + assert_eq!((h.run, h.worst_run), (0, 2)); + } + + /// `status_queries` is set once from the device and never touched by the + /// per-AU fold — a counter update must not be able to turn "this driver cannot + /// report corruption" into "it reported none". + /// + /// And, the invariant the doc contracts on both sides of this boundary state: + /// where the device answers no status queries, `failed` can only ever read 0. + /// It is not a hypothetical. `read_status` returns `Failed` on such a device + /// for a lost device, a retired session generation or an unreadable semaphore, + /// and counting those would render `integrity: driver-failed 1 · no driver + /// status` — one line contradicting itself, pointing a support engineer at a + /// verdict the hardware cannot give. So this feeds `note` a real failure and + /// pins the zero; a test that only ever passed `0` would assert nothing. + #[test] + fn the_status_query_capability_survives_every_fold() { + let mut h = DecodeHealth { + status_queries: false, + ..DecodeHealth::default() + }; + h.note(true, false, 0); + h.note(false, false, 0); + assert!(!h.status_queries); + h.note(false, false, 1); + assert_eq!( + h.failed, 0, + "a device that answers no status queries can produce no driver \ + verdict — `failed` must stay 0 whatever `read_status` returned" + ); + assert_eq!( + h.run, 1, + "…but the frame was still dropped, so the run still counts it: the \ + ATTRIBUTION is what must not be invented, not the damage" + ); + assert_eq!(h.worst_run, 1); + + // The same fold on a device that CAN answer does count it. + let mut h = DecodeHealth { + status_queries: true, + ..DecodeHealth::default() + }; + h.note(false, false, 1); + assert_eq!((h.failed, h.run), (1, 1)); + } + + #[test] + fn the_queue_lock_is_shared_only_when_the_families_collide() { + // Same family ⇒ same VkQueue (both sides use index 0) ⇒ shared lock. + assert!(submit_queues_collide(0, 0)); + assert!(submit_queues_collide(2, 2)); + // A separate decode family has exactly one submitter — no lock. + assert!(!submit_queues_collide(0, 3)); + } +} diff --git a/crates/pf-client-core/src/video_vulkan.rs b/crates/pf-client-core/src/video_vulkan.rs deleted file mode 100644 index 3aa886e4..00000000 --- a/crates/pf-client-core/src/video_vulkan.rs +++ /dev/null @@ -1,535 +0,0 @@ -//! FFmpeg Vulkan Video decode over the presenter's own VkDevice (zero-copy VkImage). -#![allow(clippy::unnecessary_cast)] - -use crate::video::{ - averr, frame_is_keyframe, DrmFrameGuard, QueueLock, VkVideoFrame, VulkanDecodeDevice, - AVERROR_EAGAIN, -}; -use crate::video_color::ColorDesc; -use crate::video_libav::AvBuffer; -use anyhow::{bail, Context, Result}; -use ffmpeg_next as ffmpeg; -use std::ptr; - -// --- Vulkan Video backend ------------------------------------------------------------- - -/// FFmpeg's Vulkan Video decoder over the PRESENTER's device: the hwdevice context is -/// built from [`VulkanDecodeDevice`]'s handles (not `av_hwdevice_ctx_create`, which -/// would make FFmpeg create its own device the presenter can't sample from). Output -/// frames are `AVVkFrame`s whose VkImage the presenter feeds straight to its CSC pass. -pub(crate) struct VulkanDecoder { - ctx: *mut ffmpeg::ffi::AVCodecContext, - /// The Vulkan hwdevice, owned. Nothing reads this field after construction — the codec context - /// took its own ref via `av_buffer_ref` — it exists so the device outlives the decoder and is - /// unref'd exactly once when it drops. Declared after `ctx` so it still releases AFTER the - /// `Drop` below frees packet/frame/context, which is the order the hand-written unref had. - /// `dead_code` is answered here rather than by removing the field (that would free the device - /// early) or by an underscore name (that would hide what it is). - #[allow(dead_code)] - hw_device: AvBuffer, - packet: *mut ffmpeg::ffi::AVPacket, - frame: *mut ffmpeg::ffi::AVFrame, - /// `vkWaitSemaphores` on the shared device — the decode-complete measurement - /// (resolved through the same get_proc_addr chain FFmpeg uses). - wait_semaphores: pf_ffvk::PFN_vkWaitSemaphores, - vk_device: pf_ffvk::VkDevice, - /// The selected decoder's registry name (`(*codec).name`) — `"av1"` vs `"libdav1d"` - /// is the difference between hardware decode and a silent CPU fallback, so every - /// log a field report leans on carries it. - name: String, - /// Storage `AVVulkanDeviceContext` points into (extension string arrays + the - /// feature chain) — FFmpeg reads the extension lists past init (frames-context - /// setup keys code paths off them), so this lives exactly as long as `hw_device`. - _ctx_storage: Box, -} - -// SAFETY: `ctx`/`packet`/`frame` are allocations this decoder owns from its constructor to `Drop`, -// `hw_device` is an owning `AvBuffer` (atomic refcount), and `_ctx_storage` is a `Box` that merely -// has to outlive them. `Send` only moves that ownership between threads, which libav permits for a -// codec context used serially — `&mut self` on every method provides that. The presenter reaches -// decoded images through the `AVFrame` guard's own references and the shared `QueueLock`, not -// through this struct. Deliberately NOT `Sync`. -unsafe impl Send for VulkanDecoder {} - -struct VkCtxStorage { - _inst: Vec, - inst_ptrs: Vec<*const std::os::raw::c_char>, - _dev: Vec, - dev_ptrs: Vec<*const std::os::raw::c_char>, - f11: pf_ffvk::VkPhysicalDeviceVulkan11Features, - f12: pf_ffvk::VkPhysicalDeviceVulkan12Features, - f13: pf_ffvk::VkPhysicalDeviceVulkan13Features, - /// Keeps the shared queue lock alive for `AVHWDeviceContext.user_opaque` — the - /// `lock_queue`/`unlock_queue` trampolines below dereference it for as long as the - /// hw device context can fire them. - _queue_lock: std::sync::Arc, -} - -/// FFmpeg `AVVulkanDeviceContext.lock_queue` trampoline: take the device's shared -/// [`QueueLock`] (stashed in `AVHWDeviceContext.user_opaque`; owned by -/// [`VkCtxStorage`], which outlives the context). Replaces FFmpeg's internal default, -/// which only serializes FFmpeg against itself — the presenter submits to the same -/// graphics queue from another thread and holds this same lock around its calls. -/// -/// # Safety -/// FFmpeg calls this with the `AVHWDeviceContext` it owns, whose `user_opaque` we set to a -/// `*const QueueLock` before handing the context over. -unsafe extern "C" fn ffvk_lock_queue( - ctx: *mut pf_ffvk::AVHWDeviceContext, - _queue_family: u32, - _index: u32, -) { - // SAFETY: `ctx` is the live context FFmpeg passes to its own callback, and the two - // `AVHWDeviceContext` declarations (pf_ffvk's and ffmpeg-sys's) describe the same C struct, so - // the cast reads the same `user_opaque` field. That field holds the pointer we stored, which - // borrows `VkCtxStorage::_queue_lock` — an `Arc` the storage keeps alive for as long - // as the hw device context can fire this trampoline (see its field doc), so the lock outlives - // every call FFmpeg can make. - unsafe { - let dev = ctx as *mut ffmpeg::ffi::AVHWDeviceContext; - let lock = (*dev).user_opaque as *const QueueLock; - (*lock).lock(); - } -} - -/// The matching `unlock_queue` trampoline — see [`ffvk_lock_queue`]. -/// -/// # Safety -/// As [`ffvk_lock_queue`]; additionally, FFmpeg only calls this after a matching `lock_queue`, so -/// the lock it releases is one this pair took. -unsafe extern "C" fn ffvk_unlock_queue( - ctx: *mut pf_ffvk::AVHWDeviceContext, - _queue_family: u32, - _index: u32, -) { - // SAFETY: as `ffvk_lock_queue` — same live context from FFmpeg, same `user_opaque` pointer into - // the `Arc` that `VkCtxStorage` keeps alive for the context's whole lifetime. - unsafe { - let dev = ctx as *mut ffmpeg::ffi::AVHWDeviceContext; - let lock = (*dev).user_opaque as *const QueueLock; - (*lock).unlock(); - } -} - -impl VulkanDecoder { - pub(crate) fn new( - codec_id: ffmpeg::codec::Id, - vk: &VulkanDecodeDevice, - ) -> Result { - use ffmpeg::ffi; - // SAFETY: a self-contained builder — every allocation is made here and null-checked before - // use, the `AVVulkanDeviceContext` fields are filled from `vk`'s live handles and from - // `_ctx_storage`, which the decoder keeps alive alongside the context, and what survives is - // moved into the returned `VulkanDecoder`, which frees each exactly once in `Drop`. - unsafe { - let mut hw_device = - ffi::av_hwdevice_ctx_alloc(ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VULKAN); - if hw_device.is_null() { - bail!("av_hwdevice_ctx_alloc(VULKAN) failed (FFmpeg built without Vulkan?)"); - } - let devctx = (*hw_device).data as *mut ffi::AVHWDeviceContext; - let hwctx = (*devctx).hwctx as *mut pf_ffvk::AVVulkanDeviceContext; - - // Pinned storage for everything the context points into. - let mut store = Box::new(VkCtxStorage { - _inst: vk.instance_extensions.clone(), - inst_ptrs: Vec::new(), - _dev: vk.device_extensions.clone(), - dev_ptrs: Vec::new(), - f11: std::mem::zeroed(), - f12: std::mem::zeroed(), - f13: std::mem::zeroed(), - _queue_lock: vk.queue_lock.clone(), - }); - store.inst_ptrs = store._inst.iter().map(|c| c.as_ptr()).collect(); - store.dev_ptrs = store._dev.iter().map(|c| c.as_ptr()).collect(); - // The features enabled at device creation, as the 1.1/1.2/1.3 chain FFmpeg - // walks to learn what it may use (sType values are vulkan.h constants). - store.f11.sType = - pf_ffvk::VkStructureType_VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES; - store.f11.samplerYcbcrConversion = vk.f_sampler_ycbcr as u32; - store.f12.sType = - pf_ffvk::VkStructureType_VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; - store.f12.timelineSemaphore = vk.f_timeline_semaphore as u32; - store.f13.sType = - pf_ffvk::VkStructureType_VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; - store.f13.synchronization2 = vk.f_synchronization2 as u32; - store.f11.pNext = &mut store.f12 as *mut _ as *mut std::ffi::c_void; - store.f12.pNext = &mut store.f13 as *mut _ as *mut std::ffi::c_void; - - (*hwctx).get_proc_addr = std::mem::transmute::( - vk.get_instance_proc_addr, - ); - (*hwctx).inst = vk.instance as pf_ffvk::VkInstance; - (*hwctx).phys_dev = vk.physical_device as pf_ffvk::VkPhysicalDevice; - (*hwctx).act_dev = vk.device as pf_ffvk::VkDevice; - (*hwctx).device_features.sType = - pf_ffvk::VkStructureType_VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; - (*hwctx).device_features.pNext = &mut store.f11 as *mut _ as *mut std::ffi::c_void; - (*hwctx).enabled_inst_extensions = store.inst_ptrs.as_ptr(); - (*hwctx).nb_enabled_inst_extensions = store.inst_ptrs.len() as i32; - (*hwctx).enabled_dev_extensions = store.dev_ptrs.as_ptr(); - (*hwctx).nb_enabled_dev_extensions = store.dev_ptrs.len() as i32; - - // Queue map: the deprecated per-role indices (tx/comp are "Required") plus - // the qf[] list, which per the header must also carry every family named - // above. One merged entry when decode shares the graphics family. - let g = vk.graphics_qf as i32; - let d = vk.decode_qf as i32; - (*hwctx).queue_family_index = g; - (*hwctx).nb_graphics_queues = 1; - (*hwctx).queue_family_tx_index = g; - (*hwctx).nb_tx_queues = 1; - (*hwctx).queue_family_comp_index = g; - (*hwctx).nb_comp_queues = 1; - (*hwctx).queue_family_encode_index = -1; - (*hwctx).nb_encode_queues = 0; - (*hwctx).queue_family_decode_index = d; - (*hwctx).nb_decode_queues = 1; - const VIDEO_DECODE_BIT: u32 = 0x20; // VK_QUEUE_VIDEO_DECODE_BIT_KHR - // `flags`/`video_caps` are bindgen enum types: i32 under MSVC, u32 under - // Linux clang — the `as _` casts absorb the difference. - if g == d { - (*hwctx).qf[0] = pf_ffvk::AVVulkanDeviceQueueFamily { - idx: g, - num: 1, - flags: (vk.graphics_queue_flags | VIDEO_DECODE_BIT) as _, - video_caps: vk.decode_video_caps as _, - }; - (*hwctx).nb_qf = 1; - } else { - (*hwctx).qf[0] = pf_ffvk::AVVulkanDeviceQueueFamily { - idx: g, - num: 1, - flags: vk.graphics_queue_flags as _, - video_caps: 0, - }; - (*hwctx).qf[1] = pf_ffvk::AVVulkanDeviceQueueFamily { - idx: d, - num: 1, - flags: VIDEO_DECODE_BIT as _, - video_caps: vk.decode_video_caps as _, - }; - (*hwctx).nb_qf = 2; - } - - // Shared-queue external sync (see [`QueueLock`]): FFmpeg must take the - // same lock the presenter holds around its own submits/presents — set - // BEFORE init so FFmpeg never installs its internal defaults (which only - // serialize FFmpeg against itself; the cross-thread race with the - // presenter's queue was an intermittent VK_ERROR_DEVICE_LOST). - (*devctx).user_opaque = - std::sync::Arc::as_ptr(&store._queue_lock) as *mut std::ffi::c_void; - (*hwctx).lock_queue = Some(ffvk_lock_queue); - (*hwctx).unlock_queue = Some(ffvk_unlock_queue); - - let r = ffi::av_hwdevice_ctx_init(hw_device); - if r < 0 { - ffi::av_buffer_unref(&mut hw_device); - return Err(averr("av_hwdevice_ctx_init(VULKAN)", r)); - } - // Owned from here: every failure path below drops it instead of unref'ing by hand. - let hw_device = AvBuffer::from_raw(hw_device) - .context("av_hwdevice_ctx_alloc(VULKAN) gave no device")?; - - // vkWaitSemaphores for the pump's decode-complete stat: loader → - // vkGetDeviceProcAddr → device fn (core 1.2, guaranteed by our gate). - let gipa = (*hwctx) - .get_proc_addr - .expect("get_proc_addr was just set above"); - let gdpa: pf_ffvk::PFN_vkGetDeviceProcAddr = - std::mem::transmute(gipa((*hwctx).inst, c"vkGetDeviceProcAddr".as_ptr())); - let wait_semaphores: pf_ffvk::PFN_vkWaitSemaphores = std::mem::transmute(gdpa - .expect("vkGetDeviceProcAddr resolvable")( - (*hwctx).act_dev, - c"vkWaitSemaphores".as_ptr(), - )); - if wait_semaphores.is_none() { - bail!("vkWaitSemaphores unresolvable on this device"); - } - let vk_device = (*hwctx).act_dev; - - // NOT `avcodec_find_decoder`: the ID lookup returns the registry's FIRST - // decoder, and for AV1 that is libdav1d (upstream orders the hwaccel-only - // native decoder last) — a software decoder that silently ignores - // `hw_device_ctx` and fails every frame's Vulkan-format guard mid-stream. - // Select by capability instead: the first decoder that can drive - // AV_PIX_FMT_VULKAN via hw_device_ctx, or fail here at open. - let codec = - crate::video::find_hw_decoder(codec_id, ffi::AVPixelFormat::AV_PIX_FMT_VULKAN)?; - let name = crate::video::codec_name(codec); - let ctx = ffi::avcodec_alloc_context3(codec); - (*ctx).hw_device_ctx = ffi::av_buffer_ref(hw_device.as_ptr()); - (*ctx).get_format = Some(pick_vulkan); - (*ctx).flags |= ffi::AV_CODEC_FLAG_LOW_DELAY as i32; - (*ctx).thread_count = 1; // hwaccel: threads only add latency - // Same pool headroom rationale as VAAPI: the presenter pins the on-screen - // frame + the newest in flight past receive_frame. - (*ctx).extra_hw_frames = 4; - let r = ffi::avcodec_open2(ctx, codec, ptr::null_mut()); - if r < 0 { - let mut ctx = ctx; - ffi::avcodec_free_context(&mut ctx); - return Err(averr("avcodec_open2 (vulkan)", r)); - } - Ok(VulkanDecoder { - ctx, - hw_device, - packet: ffi::av_packet_alloc(), - frame: ffi::av_frame_alloc(), - wait_semaphores, - vk_device, - name, - _ctx_storage: store, - }) - } - } - - /// The selected decoder's registry name (e.g. `"av1"`) — see the field doc. - pub(crate) fn name(&self) -> &str { - &self.name - } - - pub(crate) fn decode(&mut self, au: &[u8]) -> Result> { - use ffmpeg::ffi; - // SAFETY: `packet`/`frame`/`ctx` are this decoder's own allocations, live for its whole - // lifetime; `au` outlives the synchronous `send_packet` that copies out of it, and every - // libav return is checked before the result is used. - unsafe { - let r = ffi::av_new_packet(self.packet, au.len() as i32); - if r < 0 { - return Err(averr("av_new_packet", r)); - } - ptr::copy_nonoverlapping(au.as_ptr(), (*self.packet).data, au.len()); - let r = ffi::avcodec_send_packet(self.ctx, self.packet); - ffi::av_packet_unref(self.packet); - if r < 0 { - return Err(averr("send_packet", r)); - } - let mut out = None; - loop { - let r = ffi::avcodec_receive_frame(self.ctx, self.frame); - if r == AVERROR_EAGAIN { - break; - } - if r < 0 { - return Err(averr("receive_frame", r)); - } - out = Some(self.extract()?); // newest wins; older guards drop here - ffi::av_frame_unref(self.frame); - } - Ok(out) - } - } - - /// Block until the timeline semaphore reaches `value` (GPU decode complete) or the - /// timeout passes. Pure measurement — the presenter's own GPU wait is what gates - /// sampling, so a timeout here only degrades the stat, never the picture. - pub(crate) fn wait_timeline(&self, sem: u64, value: u64, timeout_ns: u64) -> bool { - let sems = [sem as pf_ffvk::VkSemaphore]; - let values = [value]; - let info = pf_ffvk::VkSemaphoreWaitInfo { - sType: pf_ffvk::VkStructureType_VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO, - pNext: std::ptr::null(), - flags: 0, - semaphoreCount: 1, - pSemaphores: sems.as_ptr(), - pValues: values.as_ptr(), - }; - // SAFETY: resolved from this device at init; handles outlive the decoder. - let r = unsafe { - self.wait_semaphores.expect("checked at init")(self.vk_device, &info, timeout_ns) - }; - r == 0 // VK_SUCCESS (VK_TIMEOUT = 2) - } - - /// Lift the decoded `AVVkFrame` into a [`VkVideoFrame`]: clone the AVFrame (the - /// guard — keeps the image + frames context alive through present) and ship the - /// POINTERS; the presenter reads the live sync state under the frames-context lock - /// at its own submit time. - fn extract(&mut self) -> Result { - use ffmpeg::ffi; - // SAFETY: `self.frame` is this decoder's own `AVFrame`; the format check below is what - // proves it carries an `AVVkFrame` before anything reads the Vulkan image out of it, and - // the clone handed onward keeps the image + frames context alive through present. - unsafe { - if (*self.frame).format != ffi::AVPixelFormat::AV_PIX_FMT_VULKAN as i32 { - bail!("decoder returned a non-Vulkan frame"); - } - let hwfc_ref = (*self.frame).hw_frames_ctx; - if hwfc_ref.is_null() { - bail!("Vulkan frame without a hardware frames context"); - } - let fc = (*hwfc_ref).data as *mut ffi::AVHWFramesContext; - let sw = (*fc).sw_format; - // The 2-plane layouts the presenter's CSC can sample: 4:2:0 (NV12/P010) and - // full-chroma 4:4:4 (NV24/P410 — HEVC RExt decode, semi-planar like all - // NVDEC output). The presenter's `vkframe_plane_formats` table is the final - // authority; anything else bails here so the session demotes cleanly. - if sw != ffi::AVPixelFormat::AV_PIX_FMT_NV12 - && sw != ffi::AVPixelFormat::AV_PIX_FMT_P010LE - && sw != ffi::AVPixelFormat::AV_PIX_FMT_NV24 - && sw != ffi::AVPixelFormat::AV_PIX_FMT_P410LE - { - bail!("Vulkan decode output {sw:?} unsupported (NV12/P010/NV24/P410 only)"); - } - let vkfc = (*fc).hwctx as *const pf_ffvk::AVVulkanFramesContext; - let vk_format = (*vkfc).format[0] as i32; - let lock_frame = (*vkfc).lock_frame.map_or(0, |f| f as usize); - let unlock_frame = (*vkfc).unlock_frame.map_or(0, |f| f as usize); - if lock_frame == 0 || unlock_frame == 0 { - bail!("Vulkan frames context without lock functions"); - } - - let clone = ffi::av_frame_clone(self.frame); - if clone.is_null() { - bail!("av_frame_clone failed"); - } - let vkf = (*clone).data[0] as *mut pf_ffvk::AVVkFrame; - // v1 handles the (default) single multiplanar image; a disjoint/multi-image - // pool would need per-plane images — bail so the session demotes cleanly. - if !(*vkf).img[1].is_null() { - let mut clone = clone; - ffi::av_frame_free(&mut clone); - bail!("multi-image Vulkan frames unsupported (disjoint pool)"); - } - // Safe without the frames lock: the handle is creation-constant and - // sem_value was last written by the decode submission on THIS thread. - let timeline_sem = (*vkf).sem[0] as u64; - let decode_done_value = (*vkf).sem_value[0]; - log_layout_once( - (*self.frame).width, - (*self.frame).height, - (*fc).width, - (*fc).height, - sw, - &self.name, - ); - Ok(VkVideoFrame { - vkframe: vkf as usize, - frames_ctx: fc as usize, - lock_frame, - unlock_frame, - vk_format, - timeline_sem, - decode_done_value, - width: (*self.frame).width as u32, - height: (*self.frame).height as u32, - // The pool extent, not the frame's: `avcodec_get_hw_frames_parameters` - // sizes it from `coded_width`/`coded_height` and FFmpeg's Vulkan layer - // rounds that up again to the driver's picture-access granularity. The - // `max` is defensive — a pool SMALLER than the frame would mean sampling - // past the surface, so degrade to "no crop" rather than trust it. - coded_width: ((*fc).width.max((*self.frame).width)) as u32, - coded_height: ((*fc).height.max((*self.frame).height)) as u32, - color: ColorDesc::from_raw(self.frame), - keyframe: frame_is_keyframe(self.frame), - guard: DrmFrameGuard(clone), - }) - } - } -} - -/// One-time dump of the first decoded frame's layout — the forensics for a new GPU/driver. -/// `pool_*` is the allocated decode surface (`>=` the frame); the gap is the alignment -/// padding the presenter's UV scale excludes. The D3D11VA path logs the same pair. -fn log_layout_once( - width: i32, - height: i32, - pool_w: i32, - pool_h: i32, - sw: ffmpeg::ffi::AVPixelFormat, - decoder: &str, -) { - use std::sync::atomic::{AtomicBool, Ordering}; - static ONCE: AtomicBool = AtomicBool::new(true); - if ONCE.swap(false, Ordering::Relaxed) { - tracing::info!( - width, - height, - pool_w, - pool_h, - ?sw, - decoder, - "Vulkan Video first frame" - ); - } -} - -impl Drop for VulkanDecoder { - fn drop(&mut self) { - use ffmpeg::ffi; - // SAFETY: each pointer is this decoder's own allocation and nothing else holds it; `Drop` - // runs exactly once, and each free nulls the pointer through its `&mut`, so none can be - // released twice. Freed packet-then-frame-then-context, the order libav documents. - unsafe { - ffi::av_packet_free(&mut self.packet); - ffi::av_frame_free(&mut self.frame); - ffi::avcodec_free_context(&mut self.ctx); - // `hw_device` is an `AvBuffer` and unrefs itself when the field drops, right after this. - } - } -} - -/// libavcodec offers the formats it can decode into; pick the Vulkan hw surface and -/// hand the decoder OUR frames context — the default one lacks -/// `VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT`, without which the presenter can't create the -/// per-plane views its CSC pass samples. Returning NONE (over the software entry) keeps -/// failures loud: the session demotes explicitly instead of silently CPU-decoding. -unsafe extern "C" fn pick_vulkan( - ctx: *mut ffmpeg::ffi::AVCodecContext, - mut list: *const ffmpeg::ffi::AVPixelFormat, -) -> ffmpeg::ffi::AVPixelFormat { - use ffmpeg::ffi; - // SAFETY: libav calls this `get_format` callback with a list it owns, terminated by - // `AV_PIX_FMT_NONE` — the walk stops at that terminator, so it stays inside the array, and it - // only reads. - unsafe { - let mut offered = false; - while *list != ffi::AVPixelFormat::AV_PIX_FMT_NONE { - if *list == ffi::AVPixelFormat::AV_PIX_FMT_VULKAN { - offered = true; - break; - } - list = list.add(1); - } - if !offered { - return ffi::AVPixelFormat::AV_PIX_FMT_NONE; - } - let mut fr: *mut ffi::AVBufferRef = ptr::null_mut(); - let r = ffi::avcodec_get_hw_frames_parameters( - ctx, - (*ctx).hw_device_ctx, - ffi::AVPixelFormat::AV_PIX_FMT_VULKAN, - &mut fr, - ); - if r < 0 || fr.is_null() { - tracing::warn!(code = r, "avcodec_get_hw_frames_parameters(VULKAN) failed"); - return ffi::AVPixelFormat::AV_PIX_FMT_NONE; - } - // Owned until the codec takes it at the bottom: the init-failure path below just returns - // and the drop releases it. - let Some(fr) = AvBuffer::from_raw(fr) else { - return ffi::AVPixelFormat::AV_PIX_FMT_NONE; - }; - let fc = (*fr.as_ptr()).data as *mut ffi::AVHWFramesContext; - let vkfc = (*fc).hwctx as *mut pf_ffvk::AVVulkanFramesContext; - // MUTABLE_FORMAT: per-plane views (spec requirement); ALIAS is FFmpeg's default. - // (`as _`: the FlagBits constants are i32 under MSVC, the img_flags field u32.) - (*vkfc).img_flags = (pf_ffvk::VkImageCreateFlagBits_VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT - | pf_ffvk::VkImageCreateFlagBits_VK_IMAGE_CREATE_ALIAS_BIT) - as _; - let r = ffi::av_hwframe_ctx_init(fr.as_ptr()); - if r < 0 { - tracing::warn!(code = r, "av_hwframe_ctx_init(VULKAN) failed"); - return ffi::AVPixelFormat::AV_PIX_FMT_NONE; - } - if !(*ctx).hw_frames_ctx.is_null() { - ffi::av_buffer_unref(&mut (*ctx).hw_frames_ctx); - } - // Ownership TRANSFERS to the codec here, so hand over the raw pointer and forget the - // wrapper — dropping it as well would be the double-unref `AvBuffer` exists to prevent. - (*ctx).hw_frames_ctx = fr.into_raw(); - ffi::AVPixelFormat::AV_PIX_FMT_VULKAN - } -} diff --git a/crates/pf-client-core/tests/bars-601-limited.h264 b/crates/pf-client-core/tests/bars-601-limited.h264 new file mode 100644 index 00000000..c1bcf750 Binary files /dev/null and b/crates/pf-client-core/tests/bars-601-limited.h264 differ diff --git a/crates/pf-client-core/tests/bars-601-limited.h265 b/crates/pf-client-core/tests/bars-601-limited.h265 deleted file mode 100644 index 48654683..00000000 Binary files a/crates/pf-client-core/tests/bars-601-limited.h265 and /dev/null differ diff --git a/crates/pf-client-core/tests/bars-709-full.h264 b/crates/pf-client-core/tests/bars-709-full.h264 new file mode 100644 index 00000000..a2c78842 Binary files /dev/null and b/crates/pf-client-core/tests/bars-709-full.h264 differ diff --git a/crates/pf-client-core/tests/bars-709-full.h265 b/crates/pf-client-core/tests/bars-709-full.h265 deleted file mode 100644 index cc0da18d..00000000 Binary files a/crates/pf-client-core/tests/bars-709-full.h265 and /dev/null differ diff --git a/crates/pf-client-core/tests/bars-709-limited.h264 b/crates/pf-client-core/tests/bars-709-limited.h264 new file mode 100644 index 00000000..f99f2d3a Binary files /dev/null and b/crates/pf-client-core/tests/bars-709-limited.h264 differ diff --git a/crates/pf-client-core/tests/bars-709-limited.h265 b/crates/pf-client-core/tests/bars-709-limited.h265 deleted file mode 100644 index 9a1ad414..00000000 Binary files a/crates/pf-client-core/tests/bars-709-limited.h265 and /dev/null differ diff --git a/crates/pf-client-core/tests/gen-bars.sh b/crates/pf-client-core/tests/gen-bars.sh new file mode 100755 index 00000000..204cb842 --- /dev/null +++ b/crates/pf-client-core/tests/gen-bars.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# Regenerate the software rung's colour fixtures (`video_software.rs`'s M8 exit test). +# +# Three single-IDR H.264 streams of the same NINE colour bars, whose VUIs differ ONLY in +# matrix coefficients and range. That is the point: the pictures are DIFFERENT code points +# that must converge on the SAME RGB once the signalled matrix and range are honoured — +# which is what makes the test able to fail against a hardcoded matrix (the swscale BT.601 +# default the old libav rung needed correction code for). +# +# ⚠ The NINTH bar (192,128,64) is load-bearing and must not be dropped for tidiness. The +# eight before it are fully saturated primaries/secondaries plus black and white, and on +# THOSE a limited↔full range mistake only pushes values outside [0,1] — where the shader +# clamps — so the whole fixture set decodes with max error 0 under the WRONG range and the +# range axis could not fail. Measured on this fixture: (192,128,64) gives max error 13 +# under the wrong range, while a 50% grey gives only 3, which is inside the test's ±4 +# tolerance. So it has to be a non-neutral mid-tone, not just a mid-tone. +# +# Not lossless: x264 refuses qp 0 outside High 4:4:4 Predictive, which openh264 cannot +# decode. qp 1 over flat bars is exact to within a code point or two at the bar centres +# the test samples, and the test's tolerance is ±4. +# +# Needs: ffmpeg with libx264. Run from this directory; overwrites the three fixtures. +set -e + +python3 - <<'PY' +BARS = [(255,255,255),(255,255,0),(0,255,255),(0,255,0),(255,0,255),(255,0,0),(0,0,255),(0,0,0), + (192,128,64)] +W, H = 32 * len(BARS), 64 +row = bytearray() +for x in range(W): + row += bytes(BARS[x // 32]) +open('bars.rgb', 'wb').write(bytes(row) * H) +print(f'{W}x{H}') +PY + +# 288x64: nine 32-px bars. Both dimensions stay macroblock-aligned (18x4), so there is no +# encoder padding for the crop to have to undo. +for spec in "601-limited bt470bg tv" "709-limited bt709 tv" "709-full bt709 pc"; do + set -- $spec + name=$1; mtx=$2; rng=$3 + ffmpeg -y -hide_banner -loglevel error -f rawvideo -pix_fmt rgb24 -s 288x64 -i bars.rgb \ + -vf "scale=in_range=full:out_color_matrix=$mtx:out_range=$rng,format=yuv420p" \ + -frames:v 1 -c:v libx264 -qp 1 -profile:v high \ + -x264-params "keyint=1:no-scenecut=1:colorprim=bt709:transfer=bt709:colormatrix=$mtx" \ + -color_primaries bt709 -color_trc bt709 -colorspace "$mtx" -color_range "$rng" \ + -f h264 "bars-$name.h264" +done +rm -f bars.rgb +ls -l bars-*.h264 diff --git a/crates/pf-client-core/tests/pq-frame.h265 b/crates/pf-client-core/tests/pq-frame.h265 deleted file mode 100644 index a9f2c1ea..00000000 Binary files a/crates/pf-client-core/tests/pq-frame.h265 and /dev/null differ diff --git a/crates/pf-console-ui/src/glyphs.rs b/crates/pf-console-ui/src/glyphs.rs index fa224355..89db0a83 100644 --- a/crates/pf-console-ui/src/glyphs.rs +++ b/crates/pf-console-ui/src/glyphs.rs @@ -42,6 +42,8 @@ pub(crate) enum HintKey { Shoulders, /// ◀ ▶ — left/right adjusts the focused value. Adjust, + /// ▲ — up opens the focused item's own menu. + Up, Key(&'static str), } @@ -62,7 +64,17 @@ impl Hint { const LABEL_SIZE: f64 = 14.0; const BADGE_D: f64 = 22.0; // face-button badge diameter -/// The hint bar pill, anchored at its BOTTOM-LEFT corner. Returns the pill's size. +/// What a drawn hint bar left behind. +pub(crate) struct HintBar { + /// The pill's `(width, height)`. + pub size: (f64, f64), + /// One hit box per hint, in the order they were given. The legend is also the console's + /// only on-screen list of what the face buttons do, so for a pointer — which has no + /// face buttons — it doubles as the button bar itself. + pub rects: Vec<(HintKey, Rect)>, +} + +/// The hint bar pill, anchored at its BOTTOM-LEFT corner. pub(crate) fn hint_bar( canvas: &Canvas, fonts: &Fonts, @@ -71,9 +83,12 @@ pub(crate) fn hint_bar( x: f64, bottom: f64, k: f64, -) -> (f64, f64) { +) -> HintBar { if hints.is_empty() { - return (0.0, 0.0); + return HintBar { + size: (0.0, 0.0), + rects: Vec::new(), + }; } let pad = 13.0 * k; let gap_hint = 18.0 * k; @@ -111,7 +126,19 @@ pub(crate) fn hint_bar( let cy = bottom - h / 2.0; let mut pen = x + pad; + let mut rects = Vec::with_capacity(hints.len()); for (hint, (gw, lw)) in hints.iter().zip(&widths) { + // Glyph + label + half the gap to the next hint, full pill height: a comfortable + // target without stealing the neighbour's. + rects.push(( + hint.key, + Rect::from_xywh( + (pen - gap_glyph / 2.0) as f32, + (bottom - h) as f32, + (gw + gap_glyph + lw + gap_hint / 2.0) as f32, + h as f32, + ), + )); draw_glyph(canvas, fonts, hint.key, style, pen, cy, k); pen += gw + gap_glyph; // Baseline centered on the badge (cap height ≈ 0.72 em for Geist). @@ -126,13 +153,17 @@ pub(crate) fn hint_bar( ); pen += lw + gap_hint; } - (w, h) + HintBar { + size: (w, h), + rects, + } } fn glyph_width(fonts: &Fonts, key: HintKey, style: GlyphStyle, k: f64) -> f64 { match resolved(key, style) { Resolved::Badge(_) | Resolved::Adjust => BADGE_D * k, Resolved::Shoulders => 2.0 * shoulder_w(fonts, k) + 3.0 * k, + Resolved::Up => BADGE_D * k, Resolved::Key(text) => keycap_w(fonts, text, k), } } @@ -151,6 +182,9 @@ enum Resolved { Badge(Face), Shoulders, Adjust, + /// The d-pad's up — drawn the same in every style, because it is a direction rather + /// than a button whose label changes with the pad. + Up, Key(&'static str), } @@ -169,8 +203,11 @@ fn resolved(key: HintKey, style: GlyphStyle) -> Resolved { HintKey::Back => Resolved::Key("Esc"), HintKey::Secondary => Resolved::Key("Y"), HintKey::Tertiary => Resolved::Key("X"), - HintKey::Shoulders => Resolved::Key("PgUp/PgDn"), + // Tab is the key a keyboard reaches for to change section; PgUp/PgDn still + // work, but naming both here makes the legend wider than the hint is worth. + HintKey::Shoulders => Resolved::Key("Tab"), HintKey::Adjust => Resolved::Adjust, + HintKey::Up => Resolved::Up, HintKey::Key(t) => Resolved::Key(t), }; } @@ -181,6 +218,7 @@ fn resolved(key: HintKey, style: GlyphStyle) -> Resolved { HintKey::Secondary => Resolved::Badge(Face::Y), HintKey::Shoulders => Resolved::Shoulders, HintKey::Adjust => Resolved::Adjust, + HintKey::Up => Resolved::Up, HintKey::Key(t) => Resolved::Key(t), } } @@ -251,6 +289,18 @@ fn draw_glyph( pen += w + 3.0 * k; } } + Resolved::Up => { + // ▲ — one solid triangle in a badge-sized slot. + let r = BADGE_D * k / 2.0; + let (cx, cyf) = ((x + r) as f32, cy as f32); + let (tw, th) = ((5.5 * k) as f32, (4.5 * k) as f32); + let mut up = Path::new(); + up.move_to((cx, cyf - th)); + up.line_to((cx - tw, cyf + th)); + up.line_to((cx + tw, cyf + th)); + up.close(); + canvas.draw_path(&up, &Paint::new(fg(0.85), None)); + } Resolved::Adjust => { // ◀ ▶ — two small solid triangles. let r = BADGE_D * k / 2.0; diff --git a/crates/pf-console-ui/src/lib.rs b/crates/pf-console-ui/src/lib.rs index 933ae725..94852f10 100644 --- a/crates/pf-console-ui/src/lib.rs +++ b/crates/pf-console-ui/src/lib.rs @@ -22,6 +22,8 @@ pub mod library; #[cfg(any(target_os = "linux", windows))] pub mod model; #[cfg(any(target_os = "linux", windows))] +mod pointer; +#[cfg(any(target_os = "linux", windows))] mod screens; #[cfg(any(target_os = "linux", windows))] mod shell; diff --git a/crates/pf-console-ui/src/model.rs b/crates/pf-console-ui/src/model.rs index ac5b0102..d1d756cd 100644 --- a/crates/pf-console-ui/src/model.rs +++ b/crates/pf-console-ui/src/model.rs @@ -156,6 +156,18 @@ pub enum ConsoleCmd { addr: String, port: u16, }, + /// Rename / re-address a saved host (the host menu's "Edit…"). `key` addresses the + /// row; the fingerprint, pins and MACs already stored against it are kept — this edits + /// a host, it doesn't replace one. + UpdateHost { + key: String, + name: String, + addr: String, + port: u16, + }, + /// Drop a saved host (the host menu's "Forget"). The next connect to that address + /// starts from scratch: no pin, no pairing, no pinned cards. + ForgetHost { key: String }, /// Start the wake-and-wait loop for this saved host. Wake { key: String, then_connect: bool }, /// Stop the wake loop (B on the wake card) and clear its status. diff --git a/crates/pf-console-ui/src/pointer.rs b/crates/pf-console-ui/src/pointer.rs new file mode 100644 index 00000000..c20c5053 --- /dev/null +++ b/crates/pf-console-ui/src/pointer.rs @@ -0,0 +1,100 @@ +//! Pointer and touch input inside the console. +//! +//! The console is a focus UI: a pad moves a cursor and presses A. A pointer brings its +//! own cursor, so every widget resolves a press directly onto whatever is under it and +//! **acts on the press**, not on the release. +//! +//! That is deliberate, not a shortcut. Both the menu list and the two carousels scroll +//! the FOCUSED item toward the centre of the screen, so the thing you pressed has already +//! slid out from under your finger by the time it lifts. A click-on-release rule would +//! have to chase it, and on a touchscreen — where the finger doesn't move but the content +//! does — it would routinely land on the wrong row. Press-to-act has no such race, and +//! the console has no drag gesture for it to compete with. +//! +//! Coordinates are device pixels: the run loop converts (it owns the window and therefore +//! the display scale), and a widget hit-tests the very rect it drew last frame. + +use skia_safe::Rect; + +/// A pointer/touch interaction, in device pixels. +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct Pointer { + pub x: f64, + pub y: f64, + pub kind: PointerKind, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum PointerKind { + /// The primary button went down, or a finger touched the glass — the acting edge. + Press, + /// The primary button or finger came up. Widgets ignore it today; it is carried so a + /// later drag gesture has an edge to close on. + Release, + /// Motion, with or without a button held. + Move, + /// The gesture was abandoned (the pointer left the window). + Cancel, + /// One scroll step; `up` = away from the user. + Scroll { up: bool }, + /// The secondary (right) button went down — the pointer's B. Handled by the shell for + /// every screen at once, so no screen has to remember to offer a way back. + Back, +} + +impl Pointer { + /// Is this the edge widgets act on? + pub(crate) fn press(&self) -> bool { + self.kind == PointerKind::Press + } + + /// Inside `rect`? Half-open, so neighbouring rects can share an edge without both + /// claiming the same pixel. An EMPTY rect never hits — which is what lets a list + /// record `Rect::new_empty()` for rows it culled and keep its indices aligned. + pub(crate) fn hits(&self, rect: Rect) -> bool { + let (x, y) = (self.x as f32, self.y as f32); + x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom + } + + /// The index of the first rect under the pointer. + pub(crate) fn pick(&self, rects: &[Rect]) -> Option { + rects.iter().position(|r| self.hits(*r)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn at(x: f64, y: f64) -> Pointer { + Pointer { + x, + y, + kind: PointerKind::Press, + } + } + + #[test] + fn hit_testing_is_half_open_and_skips_empty_rects() { + let r = Rect::from_xywh(10.0, 10.0, 20.0, 20.0); + assert!(at(10.0, 10.0).hits(r), "the top-left corner is inside"); + assert!( + !at(30.0, 20.0).hits(r), + "the right edge belongs to the next" + ); + assert!(!at(9.0, 20.0).hits(r)); + // A culled row's placeholder must never swallow a press. + assert!(!at(0.0, 0.0).hits(Rect::new_empty())); + } + + #[test] + fn pick_returns_the_first_match() { + let rects = [ + Rect::new_empty(), + Rect::from_xywh(0.0, 0.0, 10.0, 10.0), + Rect::from_xywh(0.0, 0.0, 10.0, 10.0), + ]; + assert_eq!(at(5.0, 5.0).pick(&rects), Some(1)); + assert_eq!(at(50.0, 5.0).pick(&rects), None); + } +} diff --git a/crates/pf-console-ui/src/screens.rs b/crates/pf-console-ui/src/screens.rs index 9a839ffe..09ae9a9c 100644 --- a/crates/pf-console-ui/src/screens.rs +++ b/crates/pf-console-ui/src/screens.rs @@ -5,6 +5,7 @@ pub(crate) mod add_host; pub(crate) mod home; +pub(crate) mod host_options; pub(crate) mod library; pub(crate) mod pair; pub(crate) mod pin_hosts; @@ -13,6 +14,7 @@ pub(crate) mod settings; use crate::glyphs::Hint; use crate::library::LibraryShared; use crate::model::{ConsoleCmd, HostRow}; +use crate::pointer::Pointer; use crate::theme::Fonts; use pf_client_core::gamepad::{MenuEvent, MenuPulse}; use pf_client_core::{gamepad::PadInfo, trust}; @@ -68,6 +70,10 @@ pub(crate) enum Nav { Push(Box), /// Pop this screen; popping the root quits the console. Pop, + /// Swap this screen for another, animated as a push. What "Edit\u{2026}" needs: the host + /// menu has said its piece, and leaving it on the stack would make Back from the editor + /// land on a menu describing the host as it was BEFORE the edit. + Replace(Box), } /// Everything a screen's input handling may ask of the shell, collected per event and @@ -78,6 +84,9 @@ pub(crate) struct Outbox { pub connect: Option, pub cmds: Vec, pub toast: Option, + /// Text for the system clipboard. Rides out to the run loop rather than the command + /// bus because the clipboard belongs to SDL, which the service thread never touches. + pub copy: Option, } impl Outbox { @@ -88,6 +97,30 @@ impl Outbox { pub(crate) fn pop(&mut self) { self.nav = Some(Nav::Pop); } + + pub(crate) fn replace(&mut self, screen: Screen) { + self.nav = Some(Nav::Replace(Box::new(screen))); + } +} + +/// This row's `punktfunk://` link, built from the STORE so it carries the fingerprint and +/// stable id a row doesn't hold — the same builder the desktop shells' "Copy link" uses, +/// so a link is identical whichever surface hands it to you. `None` if the host has left +/// the store since the menu was opened. +pub(crate) fn host_link(row: &HostRow) -> Option { + let known = trust::KnownHosts::load(); + let host = (!row.fp_hex.is_empty()) + .then(|| known.find_by_fp(&row.fp_hex)) + .flatten() + .or_else(|| known.find_by_addr(&row.addr, row.port))?; + Some( + pf_client_core::deeplink::DeepLink::for_host( + host, + None, + row.pin.as_ref().map(|p| p.id.as_str()), + ) + .to_url(), + ) } pub(crate) enum Screen { @@ -97,6 +130,9 @@ pub(crate) enum Screen { AddHost(add_host::AddHostScreen), Pair(pair::PairScreen), PinHosts(pin_hosts::PinHostsScreen), + /// A saved host's own actions (Wake / Copy link / Edit / Forget) — the console's + /// answer to the touch clients' host-card overflow menu. + HostOptions(host_options::HostOptionsScreen), } impl Screen { @@ -113,6 +149,24 @@ impl Screen { Screen::AddHost(s) => s.menu(ev, ctx, fx), Screen::Pair(s) => s.menu(ev, ctx, fx), Screen::PinHosts(s) => s.menu(ev, ctx, fx), + Screen::HostOptions(s) => s.menu(ev, ctx, fx), + } + } + + /// Mouse/touch at a point, in device pixels. `true` = consumed. + /// + /// A screen answers `true` for anything landing on its own furniture even when the + /// press does nothing, so a stray tap can't fall through to a layer underneath; `false` + /// only for the empty backdrop. + pub(crate) fn pointer(&mut self, p: Pointer, ctx: &mut Ctx, fx: &mut Outbox) -> bool { + match self { + Screen::Home(s) => s.pointer(p, ctx, fx), + Screen::Library(s) => s.pointer(p, ctx, fx), + Screen::Settings(s) => s.pointer(p, ctx, fx), + Screen::AddHost(s) => s.pointer(p, ctx, fx), + Screen::Pair(s) => s.pointer(p, ctx, fx), + Screen::PinHosts(s) => s.pointer(p, ctx, fx), + Screen::HostOptions(s) => s.pointer(p, ctx, fx), } } @@ -157,9 +211,10 @@ impl Screen { Screen::Home(_) => "Select a Host".into(), Screen::Library(s) => s.host_name().to_string(), Screen::Settings(_) => "Settings".into(), - Screen::AddHost(_) => "Add Host".into(), + Screen::AddHost(s) => s.title(), Screen::Pair(s) => format!("Pair with {}", s.host_name()), Screen::PinHosts(s) => format!("Pin \u{201c}{}\u{201d}", s.profile_name()), + Screen::HostOptions(s) => s.title(), } } @@ -171,6 +226,7 @@ impl Screen { Screen::AddHost(s) => s.hints(ctx), Screen::Pair(s) => s.hints(ctx), Screen::PinHosts(s) => s.hints(ctx), + Screen::HostOptions(s) => s.hints(ctx), } } @@ -193,6 +249,7 @@ impl Screen { Screen::AddHost(s) => s.render(canvas, rect, k, dt, fonts, ctx), Screen::Pair(s) => s.render(canvas, rect, k, dt, fonts, ctx), Screen::PinHosts(s) => s.render(canvas, rect, k, dt, fonts, ctx), + Screen::HostOptions(s) => s.render(canvas, rect, k, dt, fonts, ctx), } } } diff --git a/crates/pf-console-ui/src/screens/add_host.rs b/crates/pf-console-ui/src/screens/add_host.rs index 71f815a2..dd43e9c6 100644 --- a/crates/pf-console-ui/src/screens/add_host.rs +++ b/crates/pf-console-ui/src/screens/add_host.rs @@ -5,7 +5,8 @@ //! hardware keyboards type straight into the focused field through SDL text input. use crate::glyphs::{Hint, HintKey}; -use crate::model::ConsoleCmd; +use crate::model::{ConsoleCmd, HostRow}; +use crate::pointer::Pointer; use crate::screens::{Ctx, Outbox}; use crate::theme::{fg, Fonts, W}; use crate::widgets::{permits, Charset, KeyMsg, Keyboard, ListMsg, MenuList, RowSpec}; @@ -28,6 +29,10 @@ pub(crate) struct AddHostScreen { address: String, port: String, editing: Option, + /// `Some(host key)` = editing a saved host rather than adding one. The same three + /// fields either way — what changes is the verb, and that the write must UPDATE the + /// stored host instead of appending a second one beside it. + edits: Option, } impl AddHostScreen { @@ -39,9 +44,71 @@ impl AddHostScreen { address: String::new(), port: "9777".into(), editing: None, + edits: None, } } + /// The same screen, prefilled, saving over a host instead of adding one. + pub(crate) fn edit(host: &HostRow) -> AddHostScreen { + AddHostScreen { + name: host.name.clone(), + address: host.addr.clone(), + port: host.port.to_string(), + // A pinned card's key carries its profile past a NUL; the HOST is what's edited. + edits: Some(host.key.split('\0').next().unwrap_or(&host.key).to_string()), + ..AddHostScreen::new() + } + } + + pub(crate) fn title(&self) -> String { + if self.edits.is_some() { + "Edit Host".into() + } else { + "Add Host".into() + } + } + + fn commit_label(&self) -> &'static str { + if self.edits.is_some() { + "Save changes" + } else { + "Add host" + } + } + + /// Mouse/touch. A raised keyboard is modal: it takes anything landing on it, and a + /// press outside closes it rather than reaching the row underneath — which is what a + /// tap outside a keyboard means everywhere else on a touchscreen. + pub(crate) fn pointer(&mut self, p: Pointer, ctx: &mut Ctx, fx: &mut Outbox) -> bool { + if self.editing.is_some() && !ctx.deck { + if !self.keyboard.covers(p) { + if p.press() { + self.editing = None; + return true; + } + return false; + } + let (msg, _) = self.keyboard.pointer(p); + match msg { + KeyMsg::Type(c) => { + self.type_char(c); + } + KeyMsg::Backspace => { + self.backspace(); + } + KeyMsg::Done => self.editing = None, + KeyMsg::None => {} + } + return true; + } + let (msg, pulse) = self.list.pointer(p, FIELDS.len() + 1); + if matches!(msg, ListMsg::None) && pulse.is_none() { + return false; + } + self.activate(msg, fx); + true + } + pub(crate) fn editing(&self) -> bool { self.editing.is_some() } @@ -157,27 +224,58 @@ impl AddHostScreen { let (msg, pulse) = self.list.menu(ev, FIELDS.len() + 1); match msg { ListMsg::Activate => { - if self.list.cursor < FIELDS.len() { - self.editing = Some(FIELDS[self.list.cursor]); - } else if self.can_add() { - fx.cmds.push(ConsoleCmd::SaveHost { - name: self.name.trim().to_string(), - addr: self.address.trim().to_string(), - port: self.port.parse().unwrap_or(9777), - }); - fx.toast = Some(format!("Added {}", self.address.trim())); - fx.pop(); - } else { - // Not addable yet — jump to what's missing instead of a dead press. - self.list.cursor = 1; // the address row - self.editing = Some(Field::Address); - } + self.activate(msg, fx); pulse } _ => pulse, } } + /// The commit row's behaviour, shared by the pad/keyboard path and the pointer's. + fn activate(&mut self, msg: ListMsg, fx: &mut Outbox) { + if !matches!(msg, ListMsg::Activate) { + return; + } + if self.list.cursor < FIELDS.len() { + self.editing = Some(FIELDS[self.list.cursor]); + return; + } + if !self.can_add() { + // Not commitable yet — jump to what's missing instead of a dead press. + self.list.cursor = 1; // the address row + self.editing = Some(Field::Address); + return; + } + let (name, addr) = ( + self.name.trim().to_string(), + self.address.trim().to_string(), + ); + let port = self.port.parse().unwrap_or(9777); + match &self.edits { + Some(key) => { + // Name it by its nickname if it has one, else by the address — the same + // fallback the store applies to an unnamed host. + let label = if name.is_empty() { + addr.clone() + } else { + name.clone() + }; + fx.cmds.push(ConsoleCmd::UpdateHost { + key: key.clone(), + name, + addr, + port, + }); + fx.toast = Some(format!("Saved {label}")); + } + None => { + fx.toast = Some(format!("Added {addr}")); + fx.cmds.push(ConsoleCmd::SaveHost { name, addr, port }); + } + } + fx.pop(); + } + pub(crate) fn hints(&self, ctx: &Ctx) -> Vec { if self.editing.is_some() { if ctx.deck { @@ -270,7 +368,7 @@ impl AddHostScreen { ), field_row("Address", &self.address, "IP or hostname", Field::Address), field_row("Port", &self.port, "9777", Field::Port), - RowSpec::action("Add Host", self.can_add()), + RowSpec::action(self.commit_label(), self.can_add()), ] } } diff --git a/crates/pf-console-ui/src/screens/home.rs b/crates/pf-console-ui/src/screens/home.rs index 7165af33..0a36ec1a 100644 --- a/crates/pf-console-ui/src/screens/home.rs +++ b/crates/pf-console-ui/src/screens/home.rs @@ -9,6 +9,7 @@ use crate::anim::Spring; use crate::glyphs::{Hint, HintKey}; use crate::library::{step_cursor, StepResult, BUMP_C, BUMP_K, BUMP_PX, SPRING_C, SPRING_K}; use crate::model::{ConsoleCmd, HostRow}; +use crate::pointer::{Pointer, PointerKind}; use crate::screens::{ConnectIntent, Ctx, Outbox, Screen}; use crate::theme::{accent, fg, Fonts, PanelStroke, ONLINE_GREEN, W}; use pf_client_core::gamepad::{MenuDir, MenuEvent, MenuPulse}; @@ -29,6 +30,10 @@ pub(crate) struct HomeScreen { bump: Spring, /// Last-seen tile keys — hosts churn under discovery; focus follows the KEY. keys: Vec, + /// Each tile's rect as last drawn, device px, `Rect::new_empty()` for the ones the + /// carousel culled. Scaled to match: side tiles draw at 0.88, and a press near their + /// edge would otherwise pick a neighbour. + geom: Vec, } impl HomeScreen { @@ -38,6 +43,7 @@ impl HomeScreen { anim: Spring::rest(0.0), bump: Spring::rest(0.0), keys: Vec::new(), + geom: Vec::new(), } } @@ -136,10 +142,55 @@ impl HomeScreen { fx.pop(); // popping the root = quit (the shell's rule) None } + // Up on a saved tile opens that host's own menu — Wake / Copy link / Edit / + // Forget. The carousel is horizontal, so up is the one free direction, and it + // is the gesture the Android console already uses for the same menu. + MenuEvent::Move(MenuDir::Up) => match self.focused(ctx.hosts) { + Some(h) if super::host_options::HostOptionsScreen::available(h) => { + fx.push(Screen::HostOptions( + super::host_options::HostOptionsScreen::new(h), + )); + Some(MenuPulse::Confirm) + } + _ => Some(MenuPulse::Boundary), + }, MenuEvent::Move(_) => None, } } + /// Mouse/touch on the carousel. Pressing the CENTRE tile activates it; pressing any + /// other one only brings it to the centre. + /// + /// The asymmetry is the point: the carousel answers a press by sliding, so a rule that + /// also activated would connect to whichever host you merely aimed at — and on this + /// screen activating means starting a session. Bringing it front first is both the + /// safer read and the one a coverflow trains you to expect. + pub(crate) fn pointer(&mut self, p: Pointer, ctx: &mut Ctx, fx: &mut Outbox) -> bool { + self.reconcile(ctx.hosts); + let len = ctx.hosts.len() + 1; + match p.kind { + PointerKind::Scroll { up } => { + self.step(if up { -1 } else { 1 }, len, false); + true + } + // `i < len` because the geometry is a frame old: discovery can shorten the + // carousel between the render that recorded it and this press, and a cursor + // parked past the end would read as the trailing Add Host tile. + PointerKind::Press => match p.pick(&self.geom).filter(|i| *i < len) { + Some(i) if i == self.cursor as usize => { + self.menu(MenuEvent::Confirm, ctx, fx); + true + } + Some(i) => { + self.cursor = i as i32; + true + } + None => false, + }, + _ => false, + } + } + fn step(&mut self, delta: i32, len: usize, clamp: bool) -> Option { match step_cursor(self.cursor, len, delta, clamp) { StepResult::Moved(to) => { @@ -169,6 +220,12 @@ impl HomeScreen { if self.focused(ctx.hosts).is_some_and(|h| h.paired && h.saved) { hints.push(Hint::new(HintKey::Secondary, "Library")); } + if self + .focused(ctx.hosts) + .is_some_and(super::host_options::HostOptionsScreen::available) + { + hints.push(Hint::new(HintKey::Up, "Options")); + } hints.push(Hint::new(HintKey::Tertiary, "Settings")); hints.push(Hint::new(HintKey::Back, "Quit")); hints @@ -200,6 +257,8 @@ impl HomeScreen { let cy = f64::from(rect.top) + f64::from(rect.height()) / 2.0; let len = ctx.hosts.len() + 1; + self.geom.clear(); + self.geom.resize(len, Rect::new_empty()); for i in 0..len { let d = i as f64 - self.anim.pos; if d.abs() > 2.6 { @@ -215,6 +274,12 @@ impl HomeScreen { tile_w as f32, tile_h as f32, ); + self.geom[i] = Rect::from_xywh( + (cx - tile_w * scale / 2.0) as f32, + (cy - tile_h * scale / 2.0) as f32, + (tile_w * scale) as f32, + (tile_h * scale) as f32, + ); canvas.save(); canvas.translate((cx as f32, cy as f32)); canvas.scale((scale as f32, scale as f32)); diff --git a/crates/pf-console-ui/src/screens/host_options.rs b/crates/pf-console-ui/src/screens/host_options.rs new file mode 100644 index 00000000..f8c5d30d --- /dev/null +++ b/crates/pf-console-ui/src/screens/host_options.rs @@ -0,0 +1,370 @@ +//! A saved host's own actions — Wake, Copy link, Edit…, Forget — reached with UP on its +//! carousel tile, and the console's answer to the overflow menu every other client hangs +//! off a host card. +//! +//! Until now the console could add a host and connect to one, and that was all: a renamed +//! machine or a host typed in with a fat-fingered address stayed wrong forever, because +//! the only surfaces that could edit or forget one were the desktop shells. The tile is +//! where a host is, so the tile is where its actions belong. +//! +//! UP is the gesture because the carousel is horizontal — left/right are spoken for and +//! up is free — and because the Android console already does exactly this, so the two +//! consoles are learned once. A pinned profile card offers only Unpin: it is a shortcut, +//! not a second host, and offering to forget the host from it would blur precisely the +//! distinction a pin exists to draw. + +use crate::glyphs::{Hint, HintKey}; +use crate::model::{ConsoleCmd, HostRow}; +use crate::pointer::Pointer; +use crate::screens::{Ctx, Outbox, Screen}; +use crate::theme::{fg, Fonts, W}; +use crate::widgets::{ListMsg, MenuList, RowSpec}; +use pf_client_core::gamepad::{MenuEvent, MenuPulse}; +use skia_safe::{Canvas, Rect}; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Action { + Wake, + CopyLink, + Edit, + Forget, + Unpin, + Cancel, +} + +pub(crate) struct HostOptionsScreen { + /// The row this menu was opened on, by value. Discovery rewrites the carousel every + /// service pass; holding an index or a borrow would let the menu retarget itself onto + /// whichever host slid into that slot, and "Forget" must never be able to do that. + host: HostRow, + list: MenuList, + /// Forget is the one action here with no undo, so the row arms on the first press and + /// only fires on the second. The other clients forget outright; a console is driven by + /// a thumbstick from across a room, which is a good reason to be stricter than they + /// are, and none at all to be looser. + armed: bool, +} + +impl HostOptionsScreen { + pub(crate) fn new(host: &HostRow) -> HostOptionsScreen { + HostOptionsScreen { + host: host.clone(), + list: MenuList::new(), + armed: false, + } + } + + /// Is this row worth opening a menu for at all? Only saved hosts have anything to + /// edit or forget; a discovered-but-unsaved one is not ours to change. + pub(crate) fn available(host: &HostRow) -> bool { + host.saved + } + + pub(crate) fn title(&self) -> String { + match &self.host.pin { + Some(p) => format!("{} \u{b7} {}", self.host.name, p.name), + None => self.host.name.clone(), + } + } + + /// A pinned card's key is the host's with the profile id appended past a NUL (see the + /// service's row builder) — every command here addresses the HOST. + fn host_key(&self) -> &str { + self.host + .key + .split('\0') + .next() + .unwrap_or(self.host.key.as_str()) + } + + fn actions(&self) -> Vec { + if self.host.pin.is_some() { + return vec![Action::Unpin, Action::CopyLink, Action::Cancel]; + } + let mut a = Vec::new(); + // Waking a host that is already answering would just sit there counting seconds. + if self.host.can_wake && !self.host.online { + a.push(Action::Wake); + } + a.extend([ + Action::CopyLink, + Action::Edit, + Action::Forget, + Action::Cancel, + ]); + a + } + + fn label(&self, a: Action) -> String { + match a { + Action::Wake => "Wake host".into(), + Action::CopyLink => "Copy link".into(), + Action::Edit => "Edit\u{2026}".into(), + Action::Forget if self.armed => "Forget \u{2014} press again".into(), + Action::Forget => "Forget".into(), + Action::Unpin => "Unpin card".into(), + Action::Cancel => "Cancel".into(), + } + } + + pub(crate) fn menu( + &mut self, + ev: MenuEvent, + ctx: &mut Ctx, + fx: &mut Outbox, + ) -> Option { + if ev == MenuEvent::Back { + fx.pop(); + return None; + } + let actions = self.actions(); + let (msg, pulse) = self.list.menu(ev, actions.len()); + self.dispatch(msg, pulse, &actions, ctx, fx) + } + + pub(crate) fn pointer(&mut self, p: Pointer, ctx: &mut Ctx, fx: &mut Outbox) -> bool { + let actions = self.actions(); + let (msg, pulse) = self.list.pointer(p, actions.len()); + if matches!(msg, ListMsg::None) && pulse.is_none() { + return false; + } + self.dispatch(msg, pulse, &actions, ctx, fx); + true + } + + fn dispatch( + &mut self, + msg: ListMsg, + pulse: Option, + actions: &[Action], + _ctx: &mut Ctx, + fx: &mut Outbox, + ) -> Option { + let Some(action) = actions.get(self.list.cursor).copied() else { + return pulse; + }; + // Moving off the armed Forget row disarms it: an arming press is about THAT row, + // and leaving it must not leave a live trigger behind for the next visit. + if !matches!(msg, ListMsg::Activate) && action != Action::Forget { + self.armed = false; + } + match msg { + ListMsg::Adjust(_) => Some(MenuPulse::Boundary), + ListMsg::None => pulse, + ListMsg::Activate => { + self.run(action, fx); + pulse + } + } + } + + fn run(&mut self, action: Action, fx: &mut Outbox) { + let key = self.host_key().to_string(); + match action { + Action::Wake => { + fx.cmds.push(ConsoleCmd::Wake { + key, + then_connect: false, + }); + fx.pop(); + } + Action::CopyLink => { + match crate::screens::host_link(&self.host) { + Some(url) => { + fx.copy = Some(url); + fx.toast = Some("Link copied".into()); + } + // Only if the host left the store between opening this menu and now. + None => fx.toast = Some("This host isn't saved any more".into()), + } + fx.pop(); + } + Action::Edit => fx.replace(Screen::AddHost(super::add_host::AddHostScreen::edit( + &self.host, + ))), + Action::Forget if !self.armed => self.armed = true, + Action::Forget => { + fx.cmds.push(ConsoleCmd::ForgetHost { key }); + fx.toast = Some(format!("Forgot {}", self.host.name)); + fx.pop(); + } + Action::Unpin => { + if let Some(p) = &self.host.pin { + fx.cmds.push(ConsoleCmd::SetPin { + key, + profile_id: p.id.clone(), + pin: false, + }); + fx.toast = Some(format!("Unpinned {}", p.name)); + } + fx.pop(); + } + Action::Cancel => fx.pop(), + } + } + + pub(crate) fn hints(&self, _ctx: &Ctx) -> Vec { + vec![ + Hint::new(HintKey::Confirm, "Choose"), + Hint::new(HintKey::Back, "Close"), + ] + } + + pub(crate) fn render( + &mut self, + canvas: &Canvas, + rect: Rect, + k: f64, + dt: f64, + fonts: &Fonts, + _ctx: &mut Ctx, + ) { + // The explainer line, as on Add Host — it says what this menu is FOR, and the air it + // takes is what keeps the first row off the pinned title. + let blurb = if self.host.pin.is_some() { + "This card is a shortcut to one profile on this host. Unpinning it changes \ + nothing about the host or the profile." + } else { + "Manage this saved host." + }; + let cx = f64::from(rect.left) + f64::from(rect.width()) / 2.0; + fonts.centered( + canvas, + blurb, + W::Regular, + 13.0 * k, + fg(0.55), + cx, + f64::from(rect.top) + 2.0 * k, + f64::from(rect.width()) * 0.72, + ); + let list_rect = Rect::from_ltrb( + rect.left, + rect.top + (34.0 * k) as f32, + rect.right, + rect.bottom, + ); + let rows: Vec = self + .actions() + .into_iter() + .map(|a| RowSpec::action(self.label(a), true)) + .collect(); + self.list + .render(canvas, list_rect, &rows, fonts, k, dt, true); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::ProfileChip; + + fn host() -> HostRow { + HostRow { + key: "aa".into(), + name: "Desk".into(), + addr: "10.0.0.5".into(), + port: 9777, + fp_hex: "aa".into(), + paired: true, + saved: true, + online: true, + mgmt_port: 9778, + can_wake: false, + last_used: None, + os: String::new(), + pin: None, + bound_profile: None, + } + } + + fn pinned() -> HostRow { + HostRow { + key: "aa\u{0}prof-1".into(), + pin: Some(ProfileChip { + id: "prof-1".into(), + name: "4K".into(), + accent: None, + }), + ..host() + } + } + + #[test] + fn a_discovered_host_has_no_menu() { + assert!(HostOptionsScreen::available(&host())); + assert!(!HostOptionsScreen::available(&HostRow { + saved: false, + ..host() + })); + } + + #[test] + fn wake_is_offered_only_when_it_would_do_something() { + let awake = HostOptionsScreen::new(&HostRow { + can_wake: true, + online: true, + ..host() + }); + assert!(!awake.actions().contains(&Action::Wake)); + let asleep = HostOptionsScreen::new(&HostRow { + can_wake: true, + online: false, + ..host() + }); + assert!(asleep.actions().contains(&Action::Wake)); + } + + #[test] + fn a_pinned_card_cannot_forget_or_edit_the_host() { + let s = HostOptionsScreen::new(&pinned()); + assert_eq!( + s.actions(), + vec![Action::Unpin, Action::CopyLink, Action::Cancel] + ); + // …and its commands still address the HOST, not the pin's composite key. + assert_eq!(s.host_key(), "aa"); + } + + #[test] + fn forget_needs_two_presses() { + let mut s = HostOptionsScreen::new(&host()); + let actions = s.actions(); + let i = actions.iter().position(|a| *a == Action::Forget).unwrap(); + s.list.cursor = i; + let mut fx = Outbox::default(); + + s.run(Action::Forget, &mut fx); + assert!(fx.cmds.is_empty(), "the first press only arms"); + assert!(s.armed); + assert!(s.label(Action::Forget).contains("press again")); + + s.run(Action::Forget, &mut fx); + assert_eq!( + fx.cmds, + vec![ConsoleCmd::ForgetHost { key: "aa".into() }], + "the second press forgets" + ); + } + + #[test] + fn leaving_the_forget_row_disarms_it() { + let mut s = HostOptionsScreen::new(&host()); + let actions = s.actions(); + s.armed = true; + s.list.cursor = actions.iter().position(|a| *a == Action::Cancel).unwrap(); + let mut ctx_settings = pf_client_core::trust::Settings::default(); + let mut ctx = Ctx { + hosts: &[], + library: &crate::library::LibraryShared::default(), + settings: &mut ctx_settings, + pads: &[], + deck: false, + device_name: "test", + t: 0.0, + }; + let mut fx = Outbox::default(); + s.dispatch(ListMsg::None, None, &actions, &mut ctx, &mut fx); + assert!(!s.armed, "a cursor move off the row cancels the arming"); + } +} diff --git a/crates/pf-console-ui/src/screens/library.rs b/crates/pf-console-ui/src/screens/library.rs index 3f62a2a3..53ba78ec 100644 --- a/crates/pf-console-ui/src/screens/library.rs +++ b/crates/pf-console-ui/src/screens/library.rs @@ -11,6 +11,7 @@ use crate::library::{ RECEDE_DIM, RECEDE_SCALE, ROTATE_DEG, SIDE_SPACING, SPRING_C, SPRING_K, VISIBLE_RANGE, }; use crate::model::{ConsoleCmd, HostRow}; +use crate::pointer::{Pointer, PointerKind}; use crate::screens::{ConnectIntent, Ctx, Outbox}; use crate::theme::{accent, fg, Fonts, W}; use pf_client_core::gamepad::{MenuDir, MenuEvent, MenuPulse}; @@ -30,6 +31,9 @@ pub(crate) struct LibraryScreen { games: Vec, // Navigation: the integer cursor is the authority; the eased position chases it. cursor: i32, + /// Each card's rect as last drawn (axis-aligned, scale applied — the perspective tilt + /// is a few degrees and well inside a finger's slop), empty for culled cards. + geom: Vec, anim: Spring, bump: Spring, /// Decoded posters by game id (decode once; Skia uploads lazily on first draw). @@ -49,6 +53,7 @@ impl LibraryScreen { phase: LibraryPhase::Loading, games: Vec::new(), cursor: 0, + geom: Vec::new(), anim: Spring::rest(0.0), bump: Spring::rest(0.0), art: HashMap::new(), @@ -152,6 +157,43 @@ impl LibraryScreen { } } + /// Mouse/touch on the coverflow. Same rule as the home carousel: the centre card + /// launches, any other one only comes to the front. + pub(crate) fn pointer(&mut self, p: Pointer, ctx: &mut Ctx, fx: &mut Outbox) -> bool { + match p.kind { + PointerKind::Scroll { up } => { + self.step(if up { -1 } else { 1 }, false); + true + } + PointerKind::Press => { + // The cards OVERLAP, and the ones nearest the cursor are drawn on top — + // so among the rects a press falls in, the topmost is the nearest. Picking + // the first by index would hand the press to a card buried underneath. + let hit = self + .geom + .iter() + .enumerate() + // The geometry is a frame old; a library refresh can shorten the shelf + // between the render that recorded it and this press. + .filter(|(i, r)| *i < self.games.len() && p.hits(**r)) + .min_by_key(|(i, _)| (*i as i32 - self.cursor).abs()) + .map(|(i, _)| i); + match hit { + Some(i) if i == self.cursor as usize => { + self.menu(MenuEvent::Confirm, ctx, fx); + true + } + Some(i) => { + self.cursor = i as i32; + true + } + None => false, + } + } + _ => false, + } + } + fn step(&mut self, delta: i32, clamp: bool) -> Option { match step_cursor(self.cursor, self.games.len(), delta, clamp) { StepResult::Moved(to) => { @@ -326,6 +368,8 @@ impl LibraryScreen { // dense side stacks overlap toward the focus. let mut order: Vec = (0..self.games.len()).collect(); order.sort_by_key(|&i| std::cmp::Reverse((i as i32 - self.cursor).abs())); + self.geom.clear(); + self.geom.resize(self.games.len(), Rect::new_empty()); for i in order { let d = i as f64 - pos; @@ -342,6 +386,12 @@ impl LibraryScreen { d.signum() * (FOCUS_GAP + (a - 1.0) * SIDE_SPACING) * k }; let ccx = f64::from(rect.left) + w / 2.0 + offset + bump; + self.geom[i] = Rect::from_xywh( + (ccx - card_w * scale / 2.0) as f32, + (cy - card_h * scale / 2.0) as f32, + (card_w * scale) as f32, + (card_h * scale) as f32, + ); let m = card_matrix(ccx, cy, angle, scale, card_w, card_h, PERSPECTIVE * k); let game = &self.games[i]; diff --git a/crates/pf-console-ui/src/screens/pair.rs b/crates/pf-console-ui/src/screens/pair.rs index 45244d69..833bf9ae 100644 --- a/crates/pf-console-ui/src/screens/pair.rs +++ b/crates/pf-console-ui/src/screens/pair.rs @@ -6,6 +6,7 @@ use crate::glyphs::{Hint, HintKey}; use crate::model::{ConsoleCmd, HostRow, PairPhase}; +use crate::pointer::Pointer; use crate::screens::{ConnectIntent, Ctx, Outbox}; use crate::theme::{fg, Fonts, ERROR, W}; use crate::widgets::{permits, Charset, KeyMsg, Keyboard, ListMsg, MenuList, RowSpec}; @@ -206,6 +207,52 @@ impl PairScreen { } let roles = self.roles(); let (msg, pulse) = self.list.menu(ev, roles.len()); + self.activate(msg, pulse, &roles, ctx, fx) + } + + /// Mouse/touch. The raised keyboard is modal, exactly as on the add-host screen: it + /// takes what lands on it, and a press outside closes it rather than reaching through. + pub(crate) fn pointer(&mut self, p: Pointer, ctx: &mut Ctx, fx: &mut Outbox) -> bool { + if self.editing.is_some() && !ctx.deck { + if !self.keyboard.covers(p) { + if p.press() { + self.editing = None; + return true; + } + return false; + } + let (msg, _) = self.keyboard.pointer(p); + match msg { + KeyMsg::Type(c) => { + self.type_char(c); + } + KeyMsg::Backspace => { + if let Some(f) = self.editing { + self.field_mut(f).pop(); + } + } + KeyMsg::Done => self.editing = None, + KeyMsg::None => {} + } + return true; + } + let roles = self.roles(); + let (msg, pulse) = self.list.pointer(p, roles.len()); + if matches!(msg, ListMsg::None) && pulse.is_none() { + return false; + } + self.activate(msg, pulse, &roles, ctx, fx); + true + } + + fn activate( + &mut self, + msg: ListMsg, + pulse: Option, + roles: &[Role], + ctx: &mut Ctx, + fx: &mut Outbox, + ) -> Option { match msg { ListMsg::Activate => { match roles.get(self.list.cursor) { diff --git a/crates/pf-console-ui/src/screens/pin_hosts.rs b/crates/pf-console-ui/src/screens/pin_hosts.rs index 924b643b..5342151b 100644 --- a/crates/pf-console-ui/src/screens/pin_hosts.rs +++ b/crates/pf-console-ui/src/screens/pin_hosts.rs @@ -7,6 +7,7 @@ use crate::glyphs::{Hint, HintKey}; use crate::model::ConsoleCmd; +use crate::pointer::Pointer; use crate::screens::{Ctx, Outbox}; use crate::theme::{fg, Fonts, W}; use crate::widgets::{ListMsg, MenuList, RowSpec}; @@ -67,6 +68,28 @@ impl PinHostsScreen { } let indices = host_indices(ctx); let (msg, pulse) = self.list.menu(ev, indices.len()); + self.toggle(msg, pulse, &indices, ctx, fx) + } + + pub(crate) fn pointer(&mut self, p: Pointer, ctx: &mut Ctx, fx: &mut Outbox) -> bool { + let indices = host_indices(ctx); + let (msg, pulse) = self.list.pointer(p, indices.len()); + if matches!(msg, ListMsg::None) && pulse.is_none() { + return false; + } + self.toggle(msg, pulse, &indices, ctx, fx); + true + } + + /// One list message against the focused host's pin — shared by both input paths. + fn toggle( + &mut self, + msg: ListMsg, + pulse: Option, + indices: &[usize], + ctx: &mut Ctx, + fx: &mut Outbox, + ) -> Option { let Some(&host_idx) = indices.get(self.list.cursor) else { return pulse; }; diff --git a/crates/pf-console-ui/src/screens/settings.rs b/crates/pf-console-ui/src/screens/settings.rs index 550d29d7..08cf48b6 100644 --- a/crates/pf-console-ui/src/screens/settings.rs +++ b/crates/pf-console-ui/src/screens/settings.rs @@ -8,8 +8,12 @@ //! The rows are split across tabs (see [`TABS`]). They used to be one 30-row scroll with //! inline headers, which on a Deck meant thumbing past Video and Audio to reach the pad //! settings; a tab is one shoulder press, and each tab remembers where its cursor was. +//! A tab is also one Tab keypress, and one click or tap on its pill — the strip shipped +//! reachable by shoulder buttons alone, which left it unusable to everyone holding a +//! mouse or touching the glass. use crate::glyphs::{Hint, HintKey}; +use crate::pointer::Pointer; use crate::screens::{Ctx, Outbox, Screen}; use crate::theme::{fg, Fonts, W}; use crate::widgets::{ListMsg, MenuList, RowSpec, TabStrip, TAB_STRIP_H}; @@ -166,20 +170,26 @@ const CODECS: [(&str, &str); 5] = [ ("pyrowave", "PyroWave (wired LAN)"), ]; // Per-OS hardware rungs, like the shells' pickers: the console ships on Windows too -// (`punktfunk-session --browse`), where "vaapi" is a dead option that ALSO hid the real -// hardware path (d3d11va) — `Decoder::new` has no VAAPI branch there. +// (`punktfunk-session --browse`), where "vaapi" was a dead option that ALSO hid the real +// hardware path — `Decoder::new` has no VAAPI branch there. +// +// The STORED values are the `native-*` rung names since M10 (the libavcodec rungs those +// bare names meant are deleted). The LABELS are unchanged and still true — native Vulkan +// Video is Vulkan Video. A store written by an older client keeps working: the bare names +// migrate on read (`pf_client_core::video`'s `migrate_decoder_pref`), they just will not +// match a preset here, so the picker shows the first entry until the user re-picks. #[cfg(not(windows))] const DECODERS: [(&str, &str); 4] = [ ("auto", "Automatic"), - ("vulkan", "Vulkan Video"), - ("vaapi", "VAAPI"), + ("native-vulkan", "Vulkan Video"), + ("native-vaapi", "VAAPI"), ("software", "Software"), ]; #[cfg(windows)] const DECODERS: [(&str, &str); 4] = [ ("auto", "Automatic"), - ("vulkan", "Vulkan Video"), - ("d3d11va", "Direct3D 11"), + ("native-vulkan", "Vulkan Video"), + ("native-d3d11va", "Direct3D 11"), ("software", "Software"), ]; const AUDIO: [(u8, &str); 3] = [(2, "Stereo"), (6, "5.1"), (8, "7.1")]; @@ -261,12 +271,26 @@ impl SettingsScreen { } } - /// L1/R1 — move one tab, wrapping (the strip is a ring, like A's value cycle), keeping - /// each tab's own cursor. + #[cfg(test)] + pub(crate) fn tab_for_test(&self) -> usize { + self.tab + } + + /// L1/R1 (and Tab/PgUp/PgDn) — move one tab, wrapping (the strip is a ring, like A's + /// value cycle), keeping each tab's own cursor. fn switch_tab(&mut self, delta: i32) -> Option { - self.tab_cursors[self.tab] = self.list.cursor; let n = TABS.len() as i32; - self.tab = (self.tab as i32 + delta).rem_euclid(n) as usize; + self.show_tab((self.tab as i32 + delta).rem_euclid(n) as usize) + } + + /// Show `tab`, parking the cursor the outgoing tab was on. Also the pointer's path in: + /// a press on a pill names a tab outright rather than a direction to step in. + fn show_tab(&mut self, tab: usize) -> Option { + if tab >= TABS.len() { + return None; + } + self.tab_cursors[self.tab] = self.list.cursor; + self.tab = tab; // Clamp the remembered cursor: the Profiles tab's length follows the catalog. let len = self.row_ids().len(); self.list @@ -274,6 +298,22 @@ impl SettingsScreen { Some(MenuPulse::Move) } + /// Mouse/touch. The strip is checked first — its pills sit above the list and a press + /// there is never meant for a row. + pub(crate) fn pointer(&mut self, p: Pointer, ctx: &mut Ctx, fx: &mut Outbox) -> bool { + if let Some(tab) = self.strip.pointer(p) { + self.show_tab(tab); + return true; + } + let ids = self.row_ids(); + let (msg, pulse) = self.list.pointer(p, ids.len()); + if matches!(msg, ListMsg::None) && pulse.is_none() { + return false; + } + self.apply_row(msg, pulse, &ids, ctx, fx); + true + } + pub(crate) fn menu( &mut self, ev: MenuEvent, @@ -291,6 +331,19 @@ impl SettingsScreen { } let ids = self.row_ids(); let (msg, pulse) = self.list.menu(ev, ids.len()); + self.apply_row(msg, pulse, &ids, ctx, fx) + } + + /// What a list message means on the focused row — shared by the pad/keyboard path and + /// the pointer's, so a click and an A press can never drift apart. + fn apply_row( + &mut self, + msg: ListMsg, + pulse: Option, + ids: &[RowId], + ctx: &mut Ctx, + fx: &mut Outbox, + ) -> Option { // The Profiles rows navigate instead of editing the settings file. match ids[self.list.cursor] { RowId::Profile(i) => { @@ -505,7 +558,17 @@ fn row_spec(id: RowId, ctx: &Ctx, profiles: &[(String, String)]) -> RowSpec { label_for(&COMPOSITORS, &s.compositor).into(), ), RowId::Codec => (None, "Video codec", label_for(&CODECS, &s.codec).into()), - RowId::Decoder => (None, "Decoder", label_for(&DECODERS, &s.decoder).into()), + // Migrated on the way in: a pre-M10 store holds `vulkan`/`vaapi`/`d3d11va`, + // which name no preset here and would otherwise render as "—". + RowId::Decoder => ( + None, + "Decoder", + label_for( + &DECODERS, + &pf_client_core::video::migrate_decoder_pref(&s.decoder), + ) + .into(), + ), RowId::Hdr => (None, "10-bit HDR", on_off(s.hdr_enabled).into()), RowId::Chroma444 => (None, "Full chroma (4:4:4)", on_off(s.enable_444).into()), RowId::PresentPriority => ( @@ -621,7 +684,7 @@ fn detail(id: RowId) -> &'static str { "Which compositor drives the virtual output — honored only if available on the host." } RowId::Codec => "A preference — the host falls back if it can't encode this one.", - RowId::Decoder => "Automatic prefers Vulkan Video, then VAAPI, then software.", + RowId::Decoder => "Automatic picks the best hardware decoder for this GPU, then software.", RowId::Hdr => { "HDR10 — engages when the host sends HDR content and this display supports it." } @@ -769,7 +832,13 @@ fn adjust(id: RowId, delta: i32, wrap: bool, ctx: &mut Ctx) -> bool { } RowId::Compositor => step_str(&COMPOSITORS, &mut s.compositor, delta, wrap), RowId::Codec => step_str(&CODECS, &mut s.codec, delta, wrap), - RowId::Decoder => step_str(&DECODERS, &mut s.decoder, delta, wrap), + RowId::Decoder => { + // …and on the way in here too, or stepping from a legacy value would start + // from "not found" and jump to the first/last entry instead of the neighbour + // of what the user actually has. + s.decoder = pf_client_core::video::migrate_decoder_pref(&s.decoder); + step_str(&DECODERS, &mut s.decoder, delta, wrap) + } RowId::Hdr => toggle(&mut s.hdr_enabled, delta, wrap), RowId::Chroma444 => toggle(&mut s.enable_444, delta, wrap), RowId::PresentPriority => { @@ -910,6 +979,163 @@ mod tests { (Settings::default(), Vec::new()) } + /// Point the settings store at a throwaway HOME. `apply_row` rebases on the FILE + /// before a mutating press and saves after it, so a test driving that path against the + /// real `$HOME` would rewrite the developer's own console settings. + fn fake_home() { + use std::sync::OnceLock; + static HOME: OnceLock = OnceLock::new(); + let dir = HOME.get_or_init(|| { + let dir = std::env::temp_dir().join(format!("pf-settings-test-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + dir + }); + std::env::set_var("HOME", dir); + } + + /// Render the screen once so its strip and list carry real geometry, then hand back a + /// pointer aimed at the centre of `rect`. Hit-testing reads what was DRAWN, so a test + /// that skipped the render would be testing nothing. + fn rendered(screen: &mut SettingsScreen) -> f64 { + let fonts = crate::theme::build_fonts().unwrap(); + let (w, h) = (1280i32, 800i32); + let mut surface = skia_safe::surfaces::raster_n32_premul((w, h)).unwrap(); + let (mut settings, pads) = ctx_parts(); + let library = crate::library::LibraryShared::default(); + let mut ctx = Ctx { + hosts: &[], + library: &library, + settings: &mut settings, + pads: &pads, + deck: false, + device_name: "t", + t: 0.0, + }; + let k = f64::from(h) / 800.0; + screen.render( + surface.canvas(), + Rect::from_ltrb(0.0, 64.0, w as f32, h as f32 - 86.0), + k, + 1.0 / 60.0, + &fonts, + &mut ctx, + ); + k + } + + fn press(r: Rect) -> Pointer { + Pointer { + x: f64::from(r.center_x()), + y: f64::from(r.center_y()), + kind: crate::pointer::PointerKind::Press, + } + } + + fn with_ctx(f: impl FnOnce(&mut Ctx)) { + let (mut settings, pads) = ctx_parts(); + let library = crate::library::LibraryShared::default(); + let mut ctx = Ctx { + hosts: &[], + library: &library, + settings: &mut settings, + pads: &pads, + deck: false, + device_name: "t", + t: 0.0, + }; + f(&mut ctx); + } + + /// The bug this all started from: the tabs answered the shoulder buttons and nothing + /// else, so a mouse or a touchscreen could not change section at all. + #[test] + fn a_press_on_a_pill_selects_that_tab() { + let mut s = SettingsScreen::with_profiles(Vec::new()); + rendered(&mut s); + assert_eq!(s.tab, 0); + for target in [3, 1, TABS.len() - 1, 0] { + let pill = s.strip.pill(target).expect("the strip drew every pill"); + with_ctx(|ctx| { + let mut fx = Outbox::default(); + assert!(s.pointer(press(pill), ctx, &mut fx), "the pill took it"); + }); + assert_eq!(s.tab, target, "pressing pill {target} selects it"); + // Selecting a tab re-lays the strip; re-render so the next pick is current. + rendered(&mut s); + } + } + + /// …and each tab still keeps its own cursor when a POINTER is what switched it. + #[test] + fn a_pressed_tab_restores_that_tabs_cursor() { + let mut s = SettingsScreen::with_profiles(Vec::new()); + rendered(&mut s); + s.list.cursor = 2; + let second = s.strip.pill(1).unwrap(); + with_ctx(|ctx| { + let mut fx = Outbox::default(); + s.pointer(press(second), ctx, &mut fx); + }); + assert_eq!(s.list.cursor, 0, "a fresh tab starts at its own top"); + rendered(&mut s); + let first = s.strip.pill(0).unwrap(); + with_ctx(|ctx| { + let mut fx = Outbox::default(); + s.pointer(press(first), ctx, &mut fx); + }); + assert_eq!(s.list.cursor, 2, "coming back lands where it was left"); + } + + /// A press on a row focuses AND activates it — one click changes the value, the way a + /// row that is its own control should behave. + #[test] + fn a_press_on_a_row_focuses_and_cycles_it() { + fake_home(); + let mut s = SettingsScreen::with_profiles(Vec::new()); + rendered(&mut s); + // Row 0 of the leading tab is Resolution, whose first step is Native → Match + // window: one field, one unambiguous effect to assert on. + assert_eq!(s.row_ids()[0], RowId::Resolution); + let first = s.list.row_rect(0).expect("the list drew its rows"); + let (mut settings, pads) = ctx_parts(); + settings.save(); // seat the fake HOME's file — `apply_row` rebases on it + let library = crate::library::LibraryShared::default(); + let mut ctx = Ctx { + hosts: &[], + library: &library, + settings: &mut settings, + pads: &pads, + deck: false, + device_name: "t", + t: 0.0, + }; + let mut fx = Outbox::default(); + assert!(!ctx.settings.match_window); + assert!(s.pointer(press(first), &mut ctx, &mut fx)); + assert_eq!(s.list.cursor, 0, "the pressed row takes focus"); + assert!( + ctx.settings.match_window, + "one press both focuses the row and cycles its value" + ); + } + + /// A press that lands on neither a pill nor a row is refused, so the shell can let it + /// fall through rather than swallowing every stray click. + #[test] + fn a_press_on_empty_space_is_not_consumed() { + let mut s = SettingsScreen::with_profiles(Vec::new()); + rendered(&mut s); + with_ctx(|ctx| { + let mut fx = Outbox::default(); + let p = Pointer { + x: 4.0, + y: 780.0, + kind: crate::pointer::PointerKind::Press, + }; + assert!(!s.pointer(p, ctx, &mut fx)); + }); + } + #[test] fn adjust_clamps_and_activate_wraps() { let (mut settings, pads) = ctx_parts(); diff --git a/crates/pf-console-ui/src/shell.rs b/crates/pf-console-ui/src/shell.rs index ab146c41..81af9205 100644 --- a/crates/pf-console-ui/src/shell.rs +++ b/crates/pf-console-ui/src/shell.rs @@ -13,6 +13,7 @@ use crate::anim::Progress; use crate::glyphs::GlyphStyle; use crate::library::{mesh_sksl, palette, LibraryShared}; use crate::model::{ConsoleBus, ConsoleCmd, ConsoleShared, HostRow, PairPhase, WakeStatus}; +use crate::pointer::{Pointer, PointerKind}; use crate::screens::{Bg, ConnectIntent, Ctx, Nav, Outbox, Screen}; use anyhow::{anyhow, Result}; use pf_client_core::gamepad::{MenuDir, MenuEvent, MenuPulse, PadInfo}; @@ -72,6 +73,10 @@ pub(crate) struct Shell { deck: bool, pub(crate) in_stream: bool, connecting: Option, + /// The last host title a connect was raised for, kept past the connect itself so + /// [`Self::session_reconnecting`] can name the host it is re-dialing — that flow + /// raises no `Launch` of its own and therefore never passes a title through. + last_connect_title: Option, wake: Option, /// True while `wake` is the shell's own optimistic placeholder — raised the instant a /// screen queues `ConsoleCmd::Wake` (see [`Self::apply`]), before the service thread has @@ -102,6 +107,10 @@ pub(crate) struct Shell { glyphs: GlyphStyle, chip: Option, pads: Vec, + /// The settled top screen's hint-bar hit boxes, republished every frame by + /// [`Shell::render`]. The legend is the console's only on-screen statement of what the + /// face buttons do; for a pointer, which has none, it IS the button bar. + hint_rects: Vec<(crate::glyphs::HintKey, Rect)>, t0: Instant, last_frame: Option, } @@ -136,6 +145,7 @@ impl Shell { deck: opts.deck, in_stream: false, connecting: None, + last_connect_title: None, wake: None, wake_optimistic: false, toast: None, @@ -147,6 +157,7 @@ impl Shell { glyphs: GlyphStyle::Keyboard, chip: None, pads: Vec::new(), + hint_rects: Vec::new(), t0: Instant::now(), last_frame: None, }) @@ -171,6 +182,7 @@ impl Shell { pub(crate) fn set_connecting(&mut self, title: Option) { match title { Some(title) => { + self.last_connect_title = Some(title.clone()); self.connecting = Some(Connecting { title, canceling: false, @@ -201,6 +213,38 @@ impl Shell { } } + /// The stream stopped and the client is dialing again on its own (M8's codec + /// fallback). Says what changed — the picture is about to come back as a different + /// codec and silence would read as a glitch — and raises the connecting modal. + /// + /// The modal is not cosmetic. Nothing raises a `Launch` for this retry (the run loop + /// starts the pump itself), so without it the shell would be in a state no other flow + /// produces: not streaming, not connecting, and a live pump behind the console. All + /// three gates would open at once — menu events flowing, the console drawn + /// full-screen over a frozen picture, and no modal interlock — and pressing A would + /// launch a SECOND session on top of the running one. This is also what gives B + /// somewhere to go: the modal's Back raises `CancelConnect`, which the run loop + /// applies to the retry's pump exactly as it does to a first dial. + /// + /// `appear = 1.0`: the takeover is already the thing on screen (the retry follows a + /// live stream), so fading it in would read as a flash rather than a transition. + pub(crate) fn session_reconnecting(&mut self, msg: &str) { + self.in_stream = false; + self.connecting = Some(Connecting { + // The host this session was dialed to. `None` only if the shell never raised + // the connect itself (a `--connect` run has no console at all, so it never + // reaches here) — name the codec change instead of an empty string. + title: self + .last_connect_title + .clone() + .unwrap_or_else(|| "the host".to_string()), + canceling: false, + appear: 1.0, + request_access: false, + }); + self.show_toast(msg.to_string()); + } + fn show_toast(&mut self, text: String) { self.toast = Some(Toast { text, at: self.t() }); } @@ -378,10 +422,81 @@ impl Shell { pulse } + /// Mouse and touch, in device pixels. `true` = consumed. + /// + /// The precedence mirrors [`Self::handle_menu`] exactly, and for the same reasons: a + /// modal card owns input while it is up, and a screen in motion takes none at all. The + /// one addition is the hint bar, which sits above the screens because a pointer has no + /// face buttons and the legend is where those actions live. + pub(crate) fn pointer(&mut self, p: Pointer) -> bool { + self.sync(); + // The right button is the pointer's B, everywhere — including on the modal cards, + // where Back is the only thing that answers at all. + // + // With ONE exception: B at the root quits the launcher, and a right-click is far + // easier to fire by accident than a thumb on B. Quitting stays an explicit act — + // the legend's "Quit" is clickable, and that is the pointer's way out. + if p.kind == PointerKind::Back { + if self.stack.len() > 1 || self.connecting.is_some() || self.wake.is_some() { + self.handle_menu(MenuEvent::Back); + } + return true; + } + // A modal swallows the rest: clicking "past" a connect takeover onto the library + // behind it would start a second session, which is the same hole the menu path + // closes by returning early here. + if self.connecting.is_some() || self.wake.is_some() { + return true; + } + if !matches!(self.motion, Motion::None) { + return true; + } + if p.press() { + if let Some((key, _)) = self.hint_rects.iter().find(|(_, r)| p.hits(*r)) { + // Only the face-button hints are actions. Shoulders and Adjust describe a + // DIRECTION, and the thing they steer — the tab strip, a row's value — is + // already under the pointer's finger; inventing a side for a click here + // would just be a worse way to press what it can already press. + let ev = match key { + crate::glyphs::HintKey::Confirm => Some(MenuEvent::Confirm), + crate::glyphs::HintKey::Back => Some(MenuEvent::Back), + crate::glyphs::HintKey::Secondary => Some(MenuEvent::Secondary), + crate::glyphs::HintKey::Tertiary => Some(MenuEvent::Tertiary), + _ => None, + }; + if let Some(ev) = ev { + self.handle_menu(ev); + } + return true; + } + } + + let mut fx = Outbox::default(); + let consumed = { + let mut ctx = Ctx { + hosts: &self.hosts, + library: &self.library, + settings: &mut self.settings, + pads: &self.pads, + deck: self.deck, + device_name: &self.device_name, + t: self.t0.elapsed().as_secs_f64(), + }; + self.stack + .last_mut() + .expect("non-empty stack") + .pointer(p, &mut ctx, &mut fx) + }; + self.apply(fx); + consumed + } + /// The keyboard fallback — the console is fully drivable with no pad. Arrows and /// Enter/Esc map onto menu events; Y/X mirror the pad's Secondary/Tertiary /// (suppressed while editing, where letters are text). - pub(crate) fn key(&mut self, sc: sdl3::keyboard::Scancode, repeat: bool) -> bool { + /// + /// `shift` only matters for Tab, whose two directions are one key. + pub(crate) fn key(&mut self, sc: sdl3::keyboard::Scancode, shift: bool, repeat: bool) -> bool { use sdl3::keyboard::Scancode as S; if self.editing() { if let Some(top) = self.stack.last_mut() { @@ -401,6 +516,12 @@ impl Shell { S::Escape | S::Backspace if !repeat => MenuEvent::Back, S::PageUp if !repeat => MenuEvent::JumpBack, S::PageDown if !repeat => MenuEvent::JumpForward, + // Tab is what a keyboard reaches for to change section, and the settings tabs + // were otherwise on PgUp/PgDn alone — a binding the legend only ever spells out + // when NO pad is attached, so with a controller plugged in there was nothing to + // discover. Shift+Tab goes back, as everywhere else. + S::Tab if !repeat && shift => MenuEvent::JumpBack, + S::Tab if !repeat => MenuEvent::JumpForward, S::Y if !repeat && !editing => MenuEvent::Secondary, S::X if !repeat && !editing => MenuEvent::Tertiary, _ => return false, @@ -445,6 +566,9 @@ impl Shell { if let Some(text) = fx.toast { self.show_toast(text); } + if let Some(text) = fx.copy { + self.actions.push_back(OverlayAction::CopyText(text)); + } if let Some(intent) = fx.connect { self.start_connect(intent); } @@ -459,6 +583,14 @@ impl Shell { self.stack.push(*screen); self.motion = Motion::Push(Progress::new(TRANSITION_S)); } + Nav::Replace(screen) => { + // Swap under the SAME push choreography: the outgoing screen is dropped + // rather than parked, so Back from the incoming one lands where the + // replaced screen was reached from. + self.stack.pop(); + self.stack.push(*screen); + self.motion = Motion::Push(Progress::new(TRANSITION_S)); + } Nav::Pop => { if self.stack.len() > 1 { let leaving = self.stack.pop().expect("len > 1"); diff --git a/crates/pf-console-ui/src/shell/overlays.rs b/crates/pf-console-ui/src/shell/overlays.rs index 4a042d8c..49075830 100644 --- a/crates/pf-console-ui/src/shell/overlays.rs +++ b/crates/pf-console-ui/src/shell/overlays.rs @@ -219,7 +219,7 @@ impl Shell { fonts, hints, self.glyphs, - cx - probe.0 / 2.0, + cx - probe.size.0 / 2.0, h - 34.0 * k, k, ); diff --git a/crates/pf-console-ui/src/shell/render.rs b/crates/pf-console-ui/src/shell/render.rs index a5728d0a..e9c96e08 100644 --- a/crates/pf-console-ui/src/shell/render.rs +++ b/crates/pf-console-ui/src/shell/render.rs @@ -112,6 +112,11 @@ impl Shell { // A modal card owns B/A while it's up — the screen's legend would lie. show_hints: self.connecting.is_none() && self.wake.is_none(), }; + // Only a SETTLED top screen publishes clickable hint boxes. Mid-transition every + // layer is slid and scaled inside a `save_layer`, so the rects a `paint` reports + // aren't where the pixels are — and the shell drops pointer input during a + // transition anyway, exactly as it drops menu events. + self.hint_rects.clear(); match (&mut self.motion, motion_p) { (Motion::Push(_), Some(raw)) => { let p = ease_out_cubic(raw); @@ -141,7 +146,7 @@ impl Shell { } _ => { let n = self.stack.len(); - env.paint(&mut self.stack[n - 1], 1.0, 0.0, 1.0); + self.hint_rects = env.paint(&mut self.stack[n - 1], 1.0, 0.0, 1.0); } } @@ -204,8 +209,15 @@ struct LayerEnv<'a> { impl LayerEnv<'_> { /// One screen composited as a unit: `alpha` fade, `dy` vertical slide, `scale` /// about the screen center — its pinned title and hint bar ride inside the layer, - /// so chrome travels with content through a transition. - fn paint(&mut self, screen: &mut Screen, alpha: f64, dy: f64, scale: f64) { + /// so chrome travels with content through a transition. Returns the hint bar's hit + /// boxes, which only the caller can know are worth keeping (see `Shell::render`). + fn paint( + &mut self, + screen: &mut Screen, + alpha: f64, + dy: f64, + scale: f64, + ) -> Vec<(crate::glyphs::HintKey, Rect)> { let canvas = self.canvas; canvas.save_layer_alpha_f(None, alpha.clamp(0.0, 1.0) as f32); canvas.translate((0.0, dy as f32)); @@ -234,7 +246,7 @@ impl LayerEnv<'_> { self.w * 0.7, ); screen.render(canvas, self.content, self.k, self.dt, self.fonts, &mut ctx); - if self.show_hints { + let rects = if self.show_hints { let hints = screen.hints(&ctx); hint_bar( canvas, @@ -244,8 +256,12 @@ impl LayerEnv<'_> { 18.0 * self.k, self.h - 18.0 * self.k, self.k, - ); - } + ) + .rects + } else { + Vec::new() + }; canvas.restore(); + rects } } diff --git a/crates/pf-console-ui/src/shell/tests.rs b/crates/pf-console-ui/src/shell/tests.rs index 0c0af972..7752f2c6 100644 --- a/crates/pf-console-ui/src/shell/tests.rs +++ b/crates/pf-console-ui/src/shell/tests.rs @@ -171,6 +171,71 @@ fn wake_gates_input_in_the_same_press() { /// this nothing in the normal gate ever ran the tab strip's layout arithmetic or a settings /// screen's rows — a bad index there would only surface on a Deck. CPU raster: the SkSL /// backdrop, the layers and the text all run without a GPU. +/// Tab / Shift+Tab change section. The strip shipped on the shoulder buttons and +/// PgUp/PgDn only, and the legend names PgUp/PgDn solely when NO pad is attached — so with +/// a controller plugged in a keyboard user had no way in, and no way to find one. +#[test] +fn tab_and_shift_tab_change_section() { + use sdl3::keyboard::Scancode; + let (mut s, _console, _library) = shell(vec![Screen::Home(HomeScreen::new())]); + s.handle_menu(MenuEvent::Tertiary); // X → Settings + s.motion = Motion::None; // skip the push transition, which drops input + let tab = |s: &Shell| match s.stack.last() { + Some(Screen::Settings(st)) => st.tab_for_test(), + _ => panic!("the settings screen is on top"), + }; + assert_eq!(tab(&s), 0); + assert!(s.key(Scancode::Tab, false, false), "Tab is consumed"); + assert_eq!(tab(&s), 1, "Tab goes forward"); + assert!(s.key(Scancode::Tab, true, false)); + assert_eq!(tab(&s), 0, "Shift+Tab goes back"); + // …and it wraps backwards off the first tab, exactly as the shoulders do. + s.key(Scancode::Tab, true, false); + assert_eq!(tab(&s), crate::screens::settings::TAB_COUNT - 1); + // A key repeat must not run through the strip a section per frame held. + let before = tab(&s); + s.key(Scancode::Tab, false, true); + assert_eq!(tab(&s), before, "held Tab doesn't skip sections"); +} + +/// A right-click is Back on every screen, so a pointer always has a way out. +#[test] +fn a_secondary_press_goes_back() { + let (mut s, _console, _library) = shell(vec![Screen::Home(HomeScreen::new())]); + s.handle_menu(MenuEvent::Tertiary); // X → Settings + s.motion = Motion::None; + assert_eq!(s.stack.len(), 2); + assert!(s.pointer(crate::pointer::Pointer { + x: 10.0, + y: 10.0, + kind: crate::pointer::PointerKind::Back, + })); + // The pop runs through the same transition a B press does. + assert!(matches!(s.motion, Motion::Pop { .. })); +} + +/// Up on a saved tile opens that host's menu; a discovered-but-unsaved one has none. +#[test] +fn up_opens_host_options_for_saved_tiles_only() { + let (mut s, _console, _library) = shell(vec![Screen::Home(HomeScreen::new())]); + s.handle_menu(MenuEvent::Move(MenuDir::Up)); + assert!( + matches!(s.stack.last(), Some(Screen::HostOptions(_))), + "the first tile is a saved host" + ); + s.motion = Motion::None; + s.handle_menu(MenuEvent::Back); + s.motion = Motion::None; + // The third fixture host is discovered-only (`saved: false`). + s.handle_menu(MenuEvent::Move(MenuDir::Right)); + s.handle_menu(MenuEvent::Move(MenuDir::Right)); + s.handle_menu(MenuEvent::Move(MenuDir::Up)); + assert!( + matches!(s.stack.last(), Some(Screen::Home(_))), + "an unsaved host has nothing to edit or forget" + ); +} + #[test] fn every_settings_tab_rasters() { let fonts = crate::theme::build_fonts().unwrap(); @@ -243,6 +308,13 @@ fn dump_console_screens() { let (mut s, console, library) = shell(vec![Screen::Home(HomeScreen::new())]); dump(&mut s, 40, 8, "01-home", true); + // The host menu — Up on the focused saved tile. The home frame above carries the new + // ▲ Options hint that leads here, so the two are worth eyeballing together. + s.handle_menu(MenuEvent::Move(MenuDir::Up)); + dump(&mut s, 40, 8, "01b-host-options", true); + s.handle_menu(MenuEvent::Back); + dump(&mut s, 20, 8, "_settle0", true); + // Mid-push into Settings (the transition still): a couple of fast frames land // the capture around p ≈ 0.4 — both layers visible. s.handle_menu(MenuEvent::Tertiary); diff --git a/crates/pf-console-ui/src/skia_overlay.rs b/crates/pf-console-ui/src/skia_overlay.rs index ea2c48cc..86ad4e25 100644 --- a/crates/pf-console-ui/src/skia_overlay.rs +++ b/crates/pf-console-ui/src/skia_overlay.rs @@ -8,6 +8,7 @@ //! OSD, capture hint, the auto-fading start banner). use crate::model::{ConsoleBus, ConsoleShared, HostRow}; +use crate::pointer::{Pointer, PointerKind}; use crate::screens::Screen; use crate::shell::{ConsoleOptions, Shell}; use crate::theme::{match_first_family, Fonts}; @@ -16,7 +17,8 @@ use ash::vk as avk; use ash::vk::Handle as _; use pf_client_core::gamepad::{MenuEvent, MenuPulse}; use pf_presenter::overlay::{ - FrameCtx, Overlay, OverlayAction, OverlayFrame, SessionPhase, SharedDevice, + FrameCtx, Overlay, OverlayAction, OverlayFrame, PointerButton, PointerInput, SessionPhase, + SharedDevice, }; use skia_safe::gpu::vk as skvk; use skia_safe::gpu::{self, DirectContext, SurfaceOrigin}; @@ -294,7 +296,8 @@ impl Overlay for SkiaOverlay { if keymod.intersects(Mod::LCTRLMOD | Mod::RCTRLMOD | Mod::LALTMOD | Mod::RALTMOD) { return false; } - shell.key(*sc, *repeat) + let shift = keymod.intersects(Mod::LSHIFTMOD | Mod::RSHIFTMOD); + shell.key(*sc, shift, *repeat) } sdl3::event::Event::TextInput { text, .. } => { shell.text_input(text); @@ -312,6 +315,48 @@ impl Overlay for SkiaOverlay { } } + fn handle_pointer(&mut self, input: PointerInput) -> bool { + if !self.console_visible() { + return false; + } + let Some(shell) = &mut self.shell else { + return false; + }; + // `Up` of the secondary button is dropped rather than mapped: `Down` already sent + // Back, and a second event would pop two screens per right-click. + let (x, y, kind) = match input { + PointerInput::Move { x, y } => (x, y, PointerKind::Move), + PointerInput::Down { + x, + y, + button: PointerButton::Primary, + } => (x, y, PointerKind::Press), + PointerInput::Down { + x, + y, + button: PointerButton::Secondary, + } => (x, y, PointerKind::Back), + PointerInput::Up { + x, + y, + button: PointerButton::Primary, + } => (x, y, PointerKind::Release), + PointerInput::Up { .. } => return true, + PointerInput::Wheel { x, y, dy } => { + if dy == 0.0 { + return true; + } + (x, y, PointerKind::Scroll { up: dy > 0.0 }) + } + PointerInput::Cancel => (0.0, 0.0, PointerKind::Cancel), + }; + shell.pointer(Pointer { + x: f64::from(x), + y: f64::from(y), + kind, + }) + } + fn take_action(&mut self) -> Option { self.shell.as_mut().and_then(|s| s.take_action()) } @@ -333,6 +378,15 @@ impl Overlay for SkiaOverlay { shell.session_ended(reason); self.streaming_since = None; } + // The stream stopped but a new dial is already in flight: toast WHY (the + // codec changed under the user) AND raise the connecting takeover, because + // nothing else will — the run loop starts the retry's pump directly rather + // than through a `Launch`, so the shell would otherwise sit in a state where + // a menu press could start a second session over the running one. + SessionPhase::Reconnecting(msg) => { + shell.session_reconnecting(msg); + self.streaming_since = None; + } } } diff --git a/crates/pf-console-ui/src/widgets.rs b/crates/pf-console-ui/src/widgets.rs index 916888cd..3f481802 100644 --- a/crates/pf-console-ui/src/widgets.rs +++ b/crates/pf-console-ui/src/widgets.rs @@ -6,6 +6,7 @@ use crate::anim::{approach, Spring, TRAY_C, TRAY_K}; use crate::library::{BUMP_C, BUMP_K}; +use crate::pointer::{Pointer, PointerKind}; use crate::theme::{accent, fg, Fonts, PanelStroke, W}; use pf_client_core::gamepad::{MenuDir, MenuEvent, MenuPulse}; use skia_safe::{Canvas, Paint, Path, RRect, Rect}; @@ -87,6 +88,10 @@ pub(crate) struct MenuList { /// Next render, seat the scroll and the focus ease instantly instead of chasing — see /// [`MenuList::jump_to`]. snap: bool, + /// Each row's rect as the last frame actually drew it, device px — what a pointer hit- + /// tests against. One entry per row, `Rect::new_empty()` for rows scrolled out of view, + /// so an index into this is an index into `rows`. + geom: Vec, } impl MenuList { @@ -97,6 +102,7 @@ impl MenuList { scroll: 0.0, focus: Vec::new(), snap: true, + geom: Vec::new(), } } @@ -121,6 +127,33 @@ impl MenuList { } } + /// A row's drawn rect, for tests that assert on what a press can actually reach. + #[cfg(test)] + pub(crate) fn row_rect(&self, i: usize) -> Option { + self.geom.get(i).copied().filter(|r| !r.is_empty()) + } + + /// Route a pointer. A press picks the row under it, focuses it AND activates it — + /// one click does what the pad needs a move plus an A for, which is what a mouse user + /// expects of a row that IS its control ("click Resolution, resolution changes"). + /// Because activation wraps, every value stays reachable by clicking alone. + /// + /// A press in the list's empty margin is swallowed, not passed on: it must not fall + /// through to whatever the screen draws behind the list. + pub(crate) fn pointer(&mut self, p: Pointer, len: usize) -> (ListMsg, Option) { + match p.kind { + PointerKind::Scroll { up } => (ListMsg::None, self.step(if up { -1 } else { 1 }, len)), + PointerKind::Press => match p.pick(&self.geom) { + Some(i) if i < len => { + self.cursor = i; + (ListMsg::Activate, Some(MenuPulse::Confirm)) + } + _ => (ListMsg::None, None), + }, + _ => (ListMsg::None, None), + } + } + fn step(&mut self, delta: i32, len: usize) -> Option { let target = self.cursor as i32 + delta; if len == 0 || target < 0 || target >= len as i32 { @@ -192,6 +225,8 @@ impl MenuList { canvas.save(); canvas.clip_rect(rect, None, true); + self.geom.clear(); + self.geom.resize(rows.len(), Rect::new_empty()); for (i, row) in rows.iter().enumerate() { let f = self.focus[i]; let top = f64::from(rect.top) + tops[i] * k - self.scroll + self.bump.pos * k; @@ -220,6 +255,9 @@ impl MenuList { canvas.scale((scale as f32, scale as f32)); canvas.translate((-cx as f32, -cy as f32)); let r = Rect::from_xywh(x0 as f32, top as f32, row_w as f32, (ROW_H * k) as f32); + // The untransformed rect: the focus scale is a 2 % breath about the centre, far + // inside the slop a finger brings, and clicking must not depend on the ease. + self.geom[i] = r; let stroke = if row.caret { PanelStroke::Brand(0.7) } else { @@ -309,11 +347,31 @@ pub(crate) struct TabStrip { /// Chased highlight geometry `(x, width)` in device px. `None` until the first render, /// so a freshly opened screen doesn't animate its highlight in from x = 0. indicator: Option<(f64, f64)>, + /// Each pill's rect as last drawn, device px — the strip is the one part of a settings + /// screen a pointer can reach directly, so it hit-tests against what it drew. + pills: Vec, } impl TabStrip { pub(crate) fn new() -> TabStrip { - TabStrip { indicator: None } + TabStrip { + indicator: None, + pills: Vec::new(), + } + } + + /// A pill's drawn rect, for tests that assert on what a press can actually reach. + #[cfg(test)] + pub(crate) fn pill(&self, i: usize) -> Option { + self.pills.get(i).copied() + } + + /// The tab a press landed on, if any. Pills are small, so the hit box is the full + /// strip height rather than the drawn pill — a tap that lands just above or below the + /// text still selects, which on a touchscreen is the difference between working and + /// not. + pub(crate) fn pointer(&self, p: Pointer) -> Option { + p.press().then(|| p.pick(&self.pills)).flatten() } /// Draw the pills centered in `rect`'s top band. Returns nothing — the caller already @@ -368,10 +426,19 @@ impl TabStrip { ); let baseline = top + pill_h / 2.0 + size * 0.36; + self.pills.clear(); for (i, label) in labels.iter().enumerate() { // Fade each label toward white by how much the highlight actually covers it, so // the two labels a sliding highlight passes between light up together. let pill_x = x; + // Full-height hit box (see `TabStrip::pointer`), and only ever grown from the + // pill's own span so two neighbours can't both claim a press. + self.pills.push(Rect::from_xywh( + pill_x as f32, + rect.top, + widths[i] as f32, + rect.height().max((pill_h + 4.0 * k) as f32), + )); let overlap = (pill_x + widths[i]).min(ix + iw) - pill_x.max(ix); let covered = (overlap / widths[i]).clamp(0.0, 1.0) as f32; let tw = f64::from(fonts.measure(label, W::SemiBold, size)); @@ -479,6 +546,9 @@ pub(crate) struct Keyboard { /// Tray slide-in (0 hidden → 1 seated), the Swift `.spring(0.32, 0.86)`. tray: Spring, key_flash: f64, + /// Each key's rect and identity as last drawn — the tray slides, so hit-testing has to + /// read the drawn geometry rather than recompute a seated layout. + keys: Vec<(Rect, Key)>, } impl Keyboard { @@ -488,9 +558,47 @@ impl Keyboard { col: 0, tray: Spring::rest(0.0), key_flash: 0.0, + keys: Vec::new(), } } + /// Route a pointer at the tray. A press types the key under it and moves the key + /// cursor there, so a pad can carry on from wherever a finger left off. A press that + /// lands on the tray but between keys is swallowed — the tray is modal, and a stray + /// tap must not reach the list behind it. + pub(crate) fn pointer(&mut self, p: Pointer) -> (KeyMsg, Option) { + if !p.press() { + return (KeyMsg::None, None); + } + let Some(i) = p.pick(&self.keys.iter().map(|(r, _)| *r).collect::>()) else { + return (KeyMsg::None, None); + }; + let key = self.keys[i].1; + // Re-seat the cursor from the key's identity, not the draw index: `key_rows` is the + // one layout authority and the two must not be able to drift apart. + if let Some((r, c)) = key_rows() + .iter() + .enumerate() + .find_map(|(r, row)| row.iter().position(|k| *k == key).map(|c| (r, c))) + { + self.row = r; + self.col = c; + } + self.key_flash = 1.0; + match key { + Key::Char(c) => (KeyMsg::Type(c), None), + Key::Space => (KeyMsg::Type(' '), None), + Key::Backspace => (KeyMsg::Backspace, None), + Key::Done => (KeyMsg::Done, Some(MenuPulse::Confirm)), + } + } + + /// Does `p` land on the tray at all? The screen asks before routing, so a press + /// outside a raised keyboard can dismiss it instead of falling through to the list. + pub(crate) fn covers(&self, p: Pointer) -> bool { + self.keys.iter().any(|(r, _)| p.hits(*r)) + } + /// Route a menu event; the SCREEN applies `Type`/`Backspace` to its field (charset /// checks included — a refusal comes back as a boundary pulse from the screen). pub(crate) fn menu(&mut self, ev: MenuEvent) -> (KeyMsg, Option) { @@ -559,7 +667,7 @@ impl Keyboard { /// `seat` (0..1). The caller clips nothing — the tray rises from below the screen. #[allow(clippy::too_many_arguments)] pub(crate) fn render( - &self, + &mut self, canvas: &Canvas, fonts: &Fonts, w: f64, @@ -568,6 +676,7 @@ impl Keyboard { k: f64, ) { let rows = key_rows(); + self.keys.clear(); let tray_w = (560.0 * k).min(w - 32.0 * k); let tray_h = Self::tray_height() * k; let x0 = (w - tray_w) / 2.0; @@ -593,6 +702,7 @@ impl Keyboard { let x = x0 + pad + c as f64 * (key_w + gap); let focused = r == self.row && c == self.col; let kr = Rect::from_xywh(x as f32, y as f32, key_w as f32, key_h as f32); + self.keys.push((kr, *key)); let fill = if focused { let mut b = accent(1.0); if self.key_flash > 0.02 { diff --git a/crates/pf-dxvadec/Cargo.toml b/crates/pf-dxvadec/Cargo.toml new file mode 100644 index 00000000..fa8692fe --- /dev/null +++ b/crates/pf-dxvadec/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "pf-dxvadec" +description = "Native D3D11VA (DXVA) H.264/HEVC/AV1 decode for the Windows clients (M5, M7): the hand-declared DXVA buffer layouts plus AuPlan → picparams/qmatrix/slice-control (AV1: tile-control) conversion — the CPU-testable half; the ID3D11VideoDecoder plumbing lives in pf-client-core’s video_d3d11_native (design/client-native-decode.md §3.4)" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true + +[dependencies] +# Direct dependency on the vendored parser crate, not just pf-bitstream: the picture-parameter +# conversion consumes the parser's `Sps`/`Pps` types wholesale (pf-bitstream re-exports only +# `Level`/`SliceHeader`), and the same path means the one crate instance the workspace already +# builds — no duplicate types. +cros-codecs = { path = "../pf-bitstream/vendor/cros-codecs" } +pf-bitstream = { path = "../pf-bitstream" } +# For `SlotMap`/`SlotError` ONLY — see this crate's lib.rs for why the DPB slot ledger is +# borrowed from the Vulkan crate rather than duplicated or moved. +pf-vkdecode = { path = "../pf-vkdecode" } +tracing = "0.1" + +[lints] +workspace = true diff --git a/crates/pf-dxvadec/layout-probe-av1.c b/crates/pf-dxvadec/layout-probe-av1.c new file mode 100644 index 00000000..57be31ee --- /dev/null +++ b/crates/pf-dxvadec/layout-probe-av1.c @@ -0,0 +1,179 @@ +/* + * Layout probe for the hand-declared DXVA AV1 structures in `src/dxva_av1.rs`. + * + * `src/dxva.rs`'s module docs explain why these layouts are written by hand: + * windows-rs generates nothing from `dxva.h`, so the structs are transcribed — + * and that file is the most safety-critical in the backend, because nothing in it + * is type-checked against Windows. A field at the wrong offset is not a compile + * error, it is a driver reading a reference index where a quantiser should be. + * + * AV1 is the first codec here whose declaration we can measure against the + * AUTHORITATIVE header rather than against libavcodec's mirror: `DXVA_PicParams_AV1` + * ships in the Windows SDK's own `dxva.h` (present in 10.0.26100.0 and 10.0.28000.0 + * on the .173 box). That is the declaration the driver was compiled against, so it + * outranks any second-hand copy. + * + * Run it on a Windows box with the SDK (no FFmpeg, no GPU work, no stream): + * + * cmd /c ""C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvars64.bat" ^ + * && cl /nologo /W3 layout-probe-av1.c /Fe:probe-av1.exe && probe-av1.exe" + * + * Every number it prints is pinned as a `const` assertion in `src/dxva_av1.rs`, so a + * transcription mistake is a compile error. The bit-field section exists because C + * bit-field allocation order is ABI-defined rather than standardised: it PROVES + * MSVC's least-significant-bit-first order rather than assuming it. + */ +#include +#include +#include +#include + +#define S(t) printf("size %-34s %zu align %zu\n", #t, sizeof(t), __alignof(t)) +#define O(t, f) printf("off %-24s %-34s %zu\n", #t, #f, offsetof(t, f)) + +int main(void) { + S(DXVA_PicEntry_AV1); + S(DXVA_PicParams_AV1); + S(DXVA_Tile_AV1); + S(DXVA_Status_AV1); + + O(DXVA_PicParams_AV1, width); + O(DXVA_PicParams_AV1, height); + O(DXVA_PicParams_AV1, max_width); + O(DXVA_PicParams_AV1, max_height); + O(DXVA_PicParams_AV1, CurrPicTextureIndex); + O(DXVA_PicParams_AV1, superres_denom); + O(DXVA_PicParams_AV1, bitdepth); + O(DXVA_PicParams_AV1, seq_profile); + + O(DXVA_PicParams_AV1, tiles); + O(DXVA_PicParams_AV1, tiles.cols); + O(DXVA_PicParams_AV1, tiles.rows); + O(DXVA_PicParams_AV1, tiles.context_update_id); + O(DXVA_PicParams_AV1, tiles.widths); + O(DXVA_PicParams_AV1, tiles.heights); + + O(DXVA_PicParams_AV1, coding); + O(DXVA_PicParams_AV1, format); + O(DXVA_PicParams_AV1, primary_ref_frame); + O(DXVA_PicParams_AV1, order_hint); + O(DXVA_PicParams_AV1, order_hint_bits); + O(DXVA_PicParams_AV1, frame_refs); + O(DXVA_PicParams_AV1, RefFrameMapTextureIndex); + + O(DXVA_PicParams_AV1, loop_filter); + O(DXVA_PicParams_AV1, loop_filter.filter_level); + O(DXVA_PicParams_AV1, loop_filter.filter_level_u); + O(DXVA_PicParams_AV1, loop_filter.filter_level_v); + O(DXVA_PicParams_AV1, loop_filter.sharpness_level); + O(DXVA_PicParams_AV1, loop_filter.ref_deltas); + O(DXVA_PicParams_AV1, loop_filter.mode_deltas); + O(DXVA_PicParams_AV1, loop_filter.delta_lf_res); + O(DXVA_PicParams_AV1, loop_filter.frame_restoration_type); + O(DXVA_PicParams_AV1, loop_filter.log2_restoration_unit_size); + + O(DXVA_PicParams_AV1, quantization); + O(DXVA_PicParams_AV1, quantization.base_qindex); + O(DXVA_PicParams_AV1, quantization.y_dc_delta_q); + O(DXVA_PicParams_AV1, quantization.u_dc_delta_q); + O(DXVA_PicParams_AV1, quantization.v_dc_delta_q); + O(DXVA_PicParams_AV1, quantization.u_ac_delta_q); + O(DXVA_PicParams_AV1, quantization.v_ac_delta_q); + O(DXVA_PicParams_AV1, quantization.qm_y); + O(DXVA_PicParams_AV1, quantization.qm_u); + O(DXVA_PicParams_AV1, quantization.qm_v); + + O(DXVA_PicParams_AV1, cdef); + O(DXVA_PicParams_AV1, cdef.y_strengths); + O(DXVA_PicParams_AV1, cdef.uv_strengths); + O(DXVA_PicParams_AV1, interp_filter); + + O(DXVA_PicParams_AV1, segmentation); + O(DXVA_PicParams_AV1, segmentation.feature_mask); + O(DXVA_PicParams_AV1, segmentation.feature_data); + + O(DXVA_PicParams_AV1, film_grain); + O(DXVA_PicParams_AV1, film_grain.grain_seed); + O(DXVA_PicParams_AV1, film_grain.scaling_points_y); + O(DXVA_PicParams_AV1, film_grain.num_y_points); + O(DXVA_PicParams_AV1, film_grain.scaling_points_cb); + O(DXVA_PicParams_AV1, film_grain.num_cb_points); + O(DXVA_PicParams_AV1, film_grain.scaling_points_cr); + O(DXVA_PicParams_AV1, film_grain.num_cr_points); + O(DXVA_PicParams_AV1, film_grain.ar_coeffs_y); + O(DXVA_PicParams_AV1, film_grain.ar_coeffs_cb); + O(DXVA_PicParams_AV1, film_grain.ar_coeffs_cr); + O(DXVA_PicParams_AV1, film_grain.cb_mult); + O(DXVA_PicParams_AV1, film_grain.cb_luma_mult); + O(DXVA_PicParams_AV1, film_grain.cr_mult); + O(DXVA_PicParams_AV1, film_grain.cr_luma_mult); + O(DXVA_PicParams_AV1, film_grain.cb_offset); + O(DXVA_PicParams_AV1, film_grain.cr_offset); + + O(DXVA_PicParams_AV1, Reserved32Bits); + O(DXVA_PicParams_AV1, StatusReportFeedbackNumber); + + O(DXVA_Tile_AV1, DataOffset); + O(DXVA_Tile_AV1, DataSize); + O(DXVA_Tile_AV1, row); + O(DXVA_Tile_AV1, column); + O(DXVA_Tile_AV1, anchor_frame); + + /* + * Bit-field allocation order. MSVC packs from the least significant bit of the + * storage unit upward in declaration order — proved here rather than assumed, + * because getting it backwards puts every tool flag in the wrong place and the + * picture merely decodes wrong. + */ + { + DXVA_PicParams_AV1 p; + + memset(&p, 0, sizeof(p)); + p.coding.use_128x128_superblock = 1; + printf("bits coding.use_128x128_superblock=1 -> 0x%08x\n", p.coding.CodingParamToolFlags); + memset(&p, 0, sizeof(p)); + p.coding.tx_mode = 3; + printf("bits coding.tx_mode=3 -> 0x%08x\n", p.coding.CodingParamToolFlags); + memset(&p, 0, sizeof(p)); + p.coding.reference_frame_update = 1; + printf("bits coding.reference_frame_update=1 -> 0x%08x\n", p.coding.CodingParamToolFlags); + + memset(&p, 0, sizeof(p)); + p.format.frame_type = 3; + printf("bits format.frame_type=3 -> 0x%02x\n", p.format.FormatAndPictureInfoFlags); + memset(&p, 0, sizeof(p)); + p.format.mono_chrome = 1; + printf("bits format.mono_chrome=1 -> 0x%02x\n", p.format.FormatAndPictureInfoFlags); + + memset(&p, 0, sizeof(p)); + p.loop_filter.delta_lf_present = 1; + printf("bits loop_filter.delta_lf_present=1 -> 0x%02x\n", p.loop_filter.ControlFlags); + + memset(&p, 0, sizeof(p)); + p.quantization.delta_q_res = 3; + printf("bits quantization.delta_q_res=3 -> 0x%02x\n", p.quantization.ControlFlags); + + memset(&p, 0, sizeof(p)); + p.cdef.bits = 3; + printf("bits cdef.bits=3 -> 0x%02x\n", p.cdef.ControlFlags); + memset(&p, 0, sizeof(p)); + p.cdef.y_strengths[0].secondary = 3; + printf("bits cdef.y_strengths[0].secondary=3 -> 0x%02x\n", p.cdef.y_strengths[0].combined); + + memset(&p, 0, sizeof(p)); + p.segmentation.temporal_update = 1; + printf("bits segmentation.temporal_update=1 -> 0x%02x\n", p.segmentation.ControlFlags); + memset(&p, 0, sizeof(p)); + p.segmentation.feature_mask[0].globalmv = 1; + printf("bits segmentation.feature_mask[0].globalmv=1 -> 0x%02x\n", + p.segmentation.feature_mask[0].mask); + + memset(&p, 0, sizeof(p)); + p.film_grain.ar_coeff_shift_minus6 = 3; + printf("bits film_grain.ar_coeff_shift_minus6=3 -> 0x%04x\n", p.film_grain.ControlFlags); + memset(&p, 0, sizeof(p)); + p.film_grain.matrix_coeff_is_identity = 1; + printf("bits film_grain.matrix_coeff_is_identity=1 -> 0x%04x\n", p.film_grain.ControlFlags); + } + return 0; +} diff --git a/crates/pf-dxvadec/src/config.rs b/crates/pf-dxvadec/src/config.rs new file mode 100644 index 00000000..1ab91250 --- /dev/null +++ b/crates/pf-dxvadec/src/config.rs @@ -0,0 +1,428 @@ +//! The decoder-creation decisions, extracted from the Windows plumbing so they +//! are testable everywhere: which DXVA profile a stream needs, which +//! `D3D11_VIDEO_DECODER_CONFIG` to pick out of the ones a driver offers, how big +//! the decode surfaces must be, and how many of them there are. +//! +//! None of this touches D3D11. `video_d3d11_native.rs` enumerates and creates; +//! everything it *chooses* is decided here, against values it read from the +//! driver — the same split `pf-vkdecode`'s [`caps`](pf_vkdecode::caps) uses for +//! the Vulkan rung, and for the same reason: on a box we cannot run, a decision +//! table that has unit tests is the only part of decoder creation that can be +//! known-good before it ships. + +/// A DXVA decode profile: the GUID `ID3D11VideoDevice::GetVideoDecoderProfile` +/// returns and `D3D11_VIDEO_DECODER_DESC::Guid` takes. +/// +/// The GUID is a `u128` rather than a `windows_core::GUID` so this module builds +/// on every host; the Windows side converts with `GUID::from_u128`, exactly as +/// `video_d3d11.rs` already does for the same four constants. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DxvaProfile { + /// For logs and errors — a field report saying "no HEVC Main10 profile" is + /// worth a great deal more than one saying "no 107af0e0-…". + pub name: &'static str, + pub guid: u128, + /// The DXGI format the decode surfaces must carry for this profile: + /// `DXGI_FORMAT_NV12` (103) for 8-bit, `DXGI_FORMAT_P010` (104) for 10-bit. + /// The raw code point, for the same host-portability reason as the GUID — + /// and `i32` rather than `u32` because that is exactly what windows-rs's + /// `DXGI_FORMAT` is (a plain type alias, not a newtype), so the Windows side + /// passes this value through with no cast and no chance of a sign surprise. + pub dxgi_format: i32, +} + +/// `DXGI_FORMAT_NV12`. +pub const DXGI_FORMAT_NV12: i32 = 103; +/// `DXGI_FORMAT_P010`. +pub const DXGI_FORMAT_P010: i32 = 104; + +/// `D3D11_DECODER_PROFILE_H264_VLD_NOFGT` — H.264 VLD, no film grain. The one +/// H.264 profile every vendor exposes; the film-grain variants are for a tool no +/// punktfunk host encodes with. +pub const H264_VLD_NOFGT: DxvaProfile = DxvaProfile { + name: "H.264 VLD NoFGT", + guid: 0x1b81be68_a0c7_11d3_b984_00c04f2e73c5, + dxgi_format: DXGI_FORMAT_NV12, +}; + +/// `D3D11_DECODER_PROFILE_HEVC_VLD_MAIN` — HEVC Main (8-bit 4:2:0). +pub const HEVC_VLD_MAIN: DxvaProfile = DxvaProfile { + name: "HEVC Main", + guid: 0x5b11d51b_2f4c_4452_bcc3_09f2a1160cc0, + dxgi_format: DXGI_FORMAT_NV12, +}; + +/// `D3D11_DECODER_PROFILE_HEVC_VLD_MAIN10` — HEVC Main 10 (10-bit 4:2:0), the +/// HDR profile. +pub const HEVC_VLD_MAIN10: DxvaProfile = DxvaProfile { + name: "HEVC Main10", + guid: 0x107af0e0_ef1a_4d19_aba8_67a163073d13, + dxgi_format: DXGI_FORMAT_P010, +}; + +/// `D3D11_DECODER_PROFILE_AV1_VLD_PROFILE0` — AV1 Profile 0 (4:2:0, 8 **or** 10 +/// bits). The same GUID `video_d3d11.rs` already hands the FFmpeg rung. +/// +/// AV1 numbers its profiles by CHROMA SAMPLING, not by depth: Profile 0 is 4:2:0 +/// at 8 and 10 bits both, so [`AV1_VLD_PROFILE0_10BIT`] below repeats this GUID +/// with the other surface format rather than naming a second profile. (Profile 1 +/// is 4:4:4 and Profile 2 is 4:2:2/12-bit; neither has a rung here — see +/// [`profile_for`].) +pub const AV1_VLD_PROFILE0: DxvaProfile = DxvaProfile { + name: "AV1 Profile 0", + guid: 0xb8be4ccb_cf53_46ba_8d59_d6b8a6da5d2a, + dxgi_format: DXGI_FORMAT_NV12, +}; + +/// AV1 Profile 0 decoding TEN-bit 4:2:0 into P010 — the same profile GUID as +/// [`AV1_VLD_PROFILE0`], a different surface format. +/// +/// Two constants rather than one plus a format argument because the format is +/// what `CheckVideoDecoderFormat` is asked about and what the pool is allocated +/// with: a profile whose GUID is supported at NV12 and not at P010 is a real +/// answer a driver can give, and the caller must be able to ask the question. +pub const AV1_VLD_PROFILE0_10BIT: DxvaProfile = DxvaProfile { + name: "AV1 Profile 0 (10-bit)", + guid: 0xb8be4ccb_cf53_46ba_8d59_d6b8a6da5d2a, + dxgi_format: DXGI_FORMAT_P010, +}; + +/// Which codec this decoder was built for. The negotiated codec picks it once, at +/// construction — the same shape as `video_vk_native`'s `NativeCodec`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Codec { + H264, + H265, + Av1, +} + +/// The profile a stream of this codec, chroma format and bit depth needs, or +/// `None` when the combination is outside what this backend decodes. +/// +/// Refusals here are the ladder's cheap exit: they happen before a decoder +/// exists, so the caller falls through to the FFmpeg rung with a clean stream +/// rather than burning the opening IDR. What is deliberately refused: +/// +/// * anything but 4:2:0 — the DXVA 4:4:4 RExt profiles exist, but the hand-off +/// path this backend feeds is a fixed-function `VideoProcessorBlt` whose 4:4:4 +/// input support is not a thing we have ever measured; +/// * H.264 above 8-bit — `High10` has no mainstream DXVA profile GUID, and no +/// punktfunk host emits it; +/// * HEVC above 10-bit; +/// * AV1 above 10-bit (Profile 2's 12-bit) and AV1 monochrome — an AV1 sequence +/// with `mono_chrome` set reads as `chroma_format_idc` 0 here and is refused +/// with every other non-4:2:0 shape. +pub fn profile_for(codec: Codec, chroma_format_idc: u8, bit_depth: u8) -> Option { + if chroma_format_idc != 1 { + return None; + } + match (codec, bit_depth) { + (Codec::H264, 8) => Some(H264_VLD_NOFGT), + (Codec::H265, 8) => Some(HEVC_VLD_MAIN), + (Codec::H265, 10) => Some(HEVC_VLD_MAIN10), + (Codec::Av1, 8) => Some(AV1_VLD_PROFILE0), + (Codec::Av1, 10) => Some(AV1_VLD_PROFILE0_10BIT), + _ => None, + } +} + +/// The `ConfigBitstreamRaw` value that means "short-format slice control" for a +/// codec. +/// +/// The two specs number this differently and it is the single most important +/// number in decoder-config selection, because it decides which slice-control +/// STRUCT the driver is going to read: +/// +/// * H.264: `1` = long format (`DXVA_Slice_H264_Long`), `2` = short format +/// (`DXVA_Slice_H264_Short`); +/// * HEVC: `1` = short format (`DXVA_Slice_HEVC_Short`) — the only format the +/// HEVC spec defines; +/// * AV1: `1`, and there is no second value. The AV1 DXVA specification defines +/// one slice-control record (`DXVA_Tile_AV1`) and no long form, so `1` is not +/// "the short one" so much as "the only one". +/// +/// This backend implements short format only, for every codec: the long format +/// additionally carries the derived reference lists and the prediction weight +/// tables per slice, which is a second derivation of everything the picture +/// parameters already say, with a second chance to get it wrong. A device that +/// offers no short-format config is refused and the ladder answers with the +/// FFmpeg rung, which implements both. +/// +/// The AV1 value is not a guess: libavcodec's own +/// `dxva_get_decoder_configuration` (`dxva2.c`, n8.1) scores +/// `ConfigBitstreamRaw == 1` for EVERY codec and additionally accepts `2` only +/// `if (avctx->codec_id == AV_CODEC_ID_H264)`. Anything else it `continue`s past, +/// so a device offering AV1 at some other value is a device libavcodec's D3D11VA +/// hwaccel refuses too. +pub const fn short_slice_config(codec: Codec) -> u32 { + match codec { + Codec::H264 => 2, + Codec::H265 | Codec::Av1 => 1, + } +} + +/// One driver-offered decoder config, reduced to the three facts selection needs. +/// The Windows side fills this from a `D3D11_VIDEO_DECODER_CONFIG`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ConfigFacts { + /// `D3D11_VIDEO_DECODER_CONFIG::ConfigBitstreamRaw`. + pub bitstream_raw: u32, + /// Is `guidConfigBitstreamEncryption` the all-zero "no encryption" GUID? + pub no_encryption: bool, + /// `ConfigMinRenderTargetBuffCount` — the driver's own floor on how many + /// decode surfaces must exist. Honoured by [`pool_size`]. + pub min_render_target_buffers: u16, +} + +/// Pick a decoder config: the first short-format one, preferring an unencrypted +/// bitstream. +/// +/// Returns the INDEX into `configs` so the caller can hand back the driver's own +/// `D3D11_VIDEO_DECODER_CONFIG` untouched — re-synthesising a config from these +/// three fields would drop the dozen `Config*` members a driver may care about. +/// +/// `None` means the device offers no short-format config for this profile, which +/// is a refusal, not a fallback: submitting short-format slice records against a +/// long-format config is exactly the kind of mismatch that decodes to garbage +/// instead of failing. +pub fn pick_config(codec: Codec, configs: &[ConfigFacts]) -> Option { + let want = short_slice_config(codec); + let mut best: Option<(usize, u8)> = None; + for (index, cfg) in configs.iter().enumerate() { + if cfg.bitstream_raw != want { + continue; + } + // Unencrypted beats encrypted; among equals the first wins, so the + // driver's own preference order is preserved. + let score = u8::from(cfg.no_encryption); + if best.is_none_or(|(_, best_score)| score > best_score) { + best = Some((index, score)); + } + } + best.map(|(index, _)| index) +} + +/// The macroblock/CTB alignment a codec's decode surfaces need. +/// +/// H.264 is macroblock-aligned (16). HEVC asks for 128: the DXVA HEVC spec +/// requires surfaces aligned to 128 luma samples so every coding feature has room +/// to work in, and libavcodec's `ff_dxva2_common_frame_params` applies exactly +/// this. Getting it wrong is not a validation failure — it is the class of bug +/// that shows up as smeared bottom rows, which this codebase has already paid for +/// once on the CSC side. +/// +/// **AV1 is 128 too**, and that is the same function's answer rather than an +/// analogy: `ff_dxva2_common_frame_params` tests +/// `avctx->codec_id == AV_CODEC_ID_HEVC || avctx->codec_id == AV_CODEC_ID_AV1` in +/// ONE condition. (AV1's own superblock is 64 or 128 samples, so 128 also covers +/// the largest of them, but the reason it is written here is the measured one.) +pub const fn surface_alignment(codec: Codec) -> u32 { + match codec { + Codec::H264 => 16, + Codec::H265 | Codec::Av1 => 128, + } +} + +/// Round a coded dimension up to the codec's surface alignment. +pub const fn align_surface(value: u32, codec: Codec) -> u32 { + let align = surface_alignment(codec); + value.div_ceil(align) * align +} + +/// How many decode surfaces the pool holds: one per DPB slot, or the driver's own +/// minimum when that is larger. +/// +/// `dpb_slots` is the [`crate::SlotMap`] capacity (`max_dpb_frames + 1`) — the +/// exact number of pictures that can be simultaneously live, because a DXVA +/// surface IS the picture: unlike the Vulkan rung, whose picture pool is +/// deliberately decoupled from its DPB slots, here the surface index in +/// `RefFrameList` is the slot index, so one surface per slot is not a choice but +/// the data model. +/// +/// There is deliberately no spare on top. Surface indices come from the slot map +/// and the map's capacity IS `dpb_slots`, so a `dpb_slots + 1`th surface is one +/// no submission could ever name — a frame of VRAM (a hundred megabytes at 4K) +/// bought for a picture that cannot exist. Nor is one needed for the hand-off: +/// its `VideoProcessorBlt` is queued on the same immediate context as the decode, +/// so the ordering is the driver's to keep, not ours to buy with an extra +/// allocation. +/// +/// `driver_min` is the config's `ConfigMinRenderTargetBuffCount`, honoured because +/// some drivers genuinely refuse to decode into a smaller pool — and it is the +/// one honest route to a pool larger than the slot map, because it is the driver +/// asking rather than us guessing. +pub fn pool_size(dpb_slots: usize, driver_min: u16) -> u32 { + // A DXVA surface index is seven bits (`DXVA_PicEntry::Index7Bits`), so 127 is + // the hard ceiling regardless of what a driver asks for. + (dpb_slots.max(usize::from(driver_min)).min(127)) as u32 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_profile_table_covers_exactly_the_shapes_this_backend_decodes() { + assert_eq!(profile_for(Codec::H264, 1, 8), Some(H264_VLD_NOFGT)); + assert_eq!(profile_for(Codec::H265, 1, 8), Some(HEVC_VLD_MAIN)); + assert_eq!(profile_for(Codec::H265, 1, 10), Some(HEVC_VLD_MAIN10)); + // A Main10 stream decodes into P010, not NV12 — the surface format is + // part of the profile choice, not a separate decision the caller makes. + assert_eq!( + profile_for(Codec::H265, 1, 10).map(|p| p.dxgi_format), + Some(DXGI_FORMAT_P010) + ); + assert_eq!( + profile_for(Codec::H265, 1, 8).map(|p| p.dxgi_format), + Some(DXGI_FORMAT_NV12) + ); + // AV1 Profile 0 covers 8 AND 10 bits under ONE GUID, so the pair differs + // only in the surface format — the one thing that must NOT be shared, + // since it is what the pool is allocated with. + assert_eq!(profile_for(Codec::Av1, 1, 8), Some(AV1_VLD_PROFILE0)); + assert_eq!(profile_for(Codec::Av1, 1, 10), Some(AV1_VLD_PROFILE0_10BIT)); + assert_eq!(AV1_VLD_PROFILE0.guid, AV1_VLD_PROFILE0_10BIT.guid); + assert_eq!(AV1_VLD_PROFILE0.dxgi_format, DXGI_FORMAT_NV12); + assert_eq!(AV1_VLD_PROFILE0_10BIT.dxgi_format, DXGI_FORMAT_P010); + // The GUID `video_d3d11.rs` hands the FFmpeg rung for AV1 + // (`PROFILE_AV1_VLD_PROFILE0`), transcribed here so a typo in one of the + // two is a failing test rather than a rung that quietly never engages. + assert_eq!( + AV1_VLD_PROFILE0.guid, + 0xb8be4ccb_cf53_46ba_8d59_d6b8a6da5d2a + ); + } + + #[test] + fn shapes_outside_the_envelope_are_refused_rather_than_approximated() { + // 4:4:4 and 4:2:2 have no rung here at all. + assert_eq!(profile_for(Codec::H264, 3, 8), None); + assert_eq!(profile_for(Codec::H265, 3, 10), None); + assert_eq!(profile_for(Codec::H265, 2, 8), None); + // H.264 High10 has no mainstream DXVA profile. + assert_eq!(profile_for(Codec::H264, 1, 10), None); + // 12-bit HEVC likewise. + assert_eq!(profile_for(Codec::H265, 1, 12), None); + // AV1: Profile 1 (4:4:4) and Profile 2 (4:2:2 / 12-bit) have no rung + // here, and neither does monochrome — which the AV1 planner reports as + // `chroma_format_idc` 0, i.e. it lands in the same refusal as 4:4:4 + // rather than being mistaken for 4:2:0. + assert_eq!(profile_for(Codec::Av1, 3, 8), None); + assert_eq!(profile_for(Codec::Av1, 3, 10), None); + assert_eq!(profile_for(Codec::Av1, 1, 12), None); + assert_eq!(profile_for(Codec::Av1, 0, 8), None); + } + + #[test] + fn short_slice_control_is_2_for_h264_and_1_for_hevc_and_av1() { + // The one number whose two spellings would silently swap the slice + // struct a driver reads. `2` is H.264's and H.264's alone — libavcodec's + // own config scoring accepts it `if (codec_id == AV_CODEC_ID_H264)` and + // takes `1` everywhere else. + assert_eq!(short_slice_config(Codec::H264), 2); + assert_eq!(short_slice_config(Codec::H265), 1); + assert_eq!(short_slice_config(Codec::Av1), 1); + } + + #[test] + fn config_selection_takes_a_short_format_config_and_prefers_no_encryption() { + let configs = [ + ConfigFacts { + bitstream_raw: 1, // H.264 long format — unusable here + no_encryption: true, + min_render_target_buffers: 0, + }, + ConfigFacts { + bitstream_raw: 2, + no_encryption: false, + min_render_target_buffers: 0, + }, + ConfigFacts { + bitstream_raw: 2, + no_encryption: true, + min_render_target_buffers: 0, + }, + ]; + assert_eq!(pick_config(Codec::H264, &configs), Some(2)); + // For HEVC the same array reads the other way round: 1 IS short format + // there, and 2 means nothing. AV1 reads it the HEVC way. + assert_eq!(pick_config(Codec::H265, &configs), Some(0)); + assert_eq!(pick_config(Codec::Av1, &configs), Some(0)); + } + + #[test] + fn a_device_with_no_short_format_config_is_refused_not_downgraded() { + let long_only = [ConfigFacts { + bitstream_raw: 1, + no_encryption: true, + min_render_target_buffers: 0, + }]; + assert_eq!(pick_config(Codec::H264, &long_only), None); + assert_eq!(pick_config(Codec::H264, &[]), None); + } + + #[test] + fn among_equal_configs_the_drivers_own_order_wins() { + let configs = [ + ConfigFacts { + bitstream_raw: 2, + no_encryption: true, + min_render_target_buffers: 0, + }, + ConfigFacts { + bitstream_raw: 2, + no_encryption: true, + min_render_target_buffers: 0, + }, + ]; + assert_eq!(pick_config(Codec::H264, &configs), Some(0)); + } + + #[test] + fn surfaces_are_macroblock_aligned_for_h264_and_128_aligned_for_hevc() { + assert_eq!(align_surface(1920, Codec::H264), 1920); + assert_eq!(align_surface(1080, Codec::H264), 1088); + assert_eq!(align_surface(1920, Codec::H265), 1920); + assert_eq!(align_surface(1080, Codec::H265), 1152); + // The 3840x2400 shape that produced the green bar on Intel: 2400 is + // already a multiple of 128, so nothing moves. + assert_eq!(align_surface(3840, Codec::H265), 3840); + assert_eq!(align_surface(2400, Codec::H265), 2432); + assert_eq!(align_surface(2432, Codec::H265), 2432); + // AV1 shares HEVC's granule (`ff_dxva2_common_frame_params` tests the two + // codec ids in one condition), so the 320x240 conformance vector decodes + // into a 384x256 surface and the chroma plane starts 256 rows down — the + // geometry the parity readback has to use. + assert_eq!(align_surface(320, Codec::Av1), 384); + assert_eq!(align_surface(240, Codec::Av1), 256); + assert_eq!(align_surface(1920, Codec::Av1), 1920); + assert_eq!(align_surface(1080, Codec::Av1), 1152); + } + + #[test] + fn an_av1_pool_is_the_eight_reference_slots_plus_the_current_picture() { + // AV1's DPB depth is a CONSTANT of the codec (`NUM_REF_FRAMES` = 8), not + // an SPS field, so the pool is always nine surfaces — which is also + // libavcodec's `num_surfaces = 1 + 8` for `AV_CODEC_ID_AV1`. A driver + // asking for more still wins. + assert_eq!(pool_size(9, 0), 9); + assert_eq!(pool_size(9, 16), 16); + } + + #[test] + fn the_pool_holds_exactly_one_surface_per_dpb_slot_and_no_unaddressable_spare() { + // A surface index comes from the slot map, whose capacity is this number: + // one more would be a surface no submission could ever name. + assert_eq!(pool_size(17, 0), 17); + assert_eq!(pool_size(2, 0), 2); + } + + #[test] + fn the_pool_honours_a_drivers_own_minimum_and_the_seven_bit_index_ceiling() { + assert_eq!(pool_size(3, 12), 12); + assert_eq!(pool_size(3, 2), 3); + // Seven-bit surface indices cap the pool no matter who asks. + assert_eq!(pool_size(200, 0), 127); + assert_eq!(pool_size(3, 4000), 127); + } +} diff --git a/crates/pf-dxvadec/src/descriptors.rs b/crates/pf-dxvadec/src/descriptors.rs new file mode 100644 index 00000000..7d5ed39e --- /dev/null +++ b/crates/pf-dxvadec/src/descriptors.rs @@ -0,0 +1,549 @@ +//! The buffer DESCRIPTORS one `ID3D11VideoContext::SubmitDecoderBuffers` call +//! carries: which buffers are in the set at all, and the four +//! `D3D11_VIDEO_DECODER_BUFFER_DESC` fields whose values are a DECISION rather +//! than a pointer the driver handed back. +//! +//! # Why this is a module of its own +//! +//! Review 13 found four defects in this backend. **Two of the three structural +//! ones lived here rather than in the picture parameters**: an HEVC +//! quantization-matrix buffer submitted unconditionally (so a driver was handed a +//! matrix of zeros on every stream that disables scaling lists), and a +//! `NumMBsInBuffer` left at 0 where libavcodec's H.264 path writes +//! `mb_width * mb_height` — on the exact call (`SubmitDecoderBuffers`) that this +//! codebase has already seen an Intel driver reject a hand-built variant on. +//! +//! Neither is visible in the picture parameters, neither is visible in a smoke +//! test, and — before this module — neither was visible to any gate this program +//! runs, because the descriptors were built inside `cfg(windows)` code that no CI +//! leg compiles. That is the whole reason the values live here: a descriptor set +//! is a pure function of the conversion's output plus the packer's output, so it +//! can be asserted on any host, on every leg, over every AU of the vendored +//! vectors. +//! +//! # ⚠ The Windows layer still builds its own for H.264 and HEVC — rewire them +//! +//! `pf-client-core`'s `video_d3d11_native.rs` was rewired for **AV1** +//! (`fill_and_submit_av1` builds its submission from [`descriptors_av1`] and +//! cross-checks every `DataSize` against what its writers actually wrote), and +//! that is what this module was written for. Its H.264 and HEVC arm +//! (`fill_and_submit_slices` + the private `buffer_desc`) still constructs the +//! same four descriptors itself and should be rewired the same way. Until it is, +//! the two must be read together: this module is the SPEC and the tests are its +//! proof, and a divergence between them is a defect in the Windows file. The +//! ordering, the values and the presence rule below are exactly what that file +//! does today, transcribed — not a new invention. +//! +//! # The values, and where each comes from +//! +//! `CompressedBufferType` (D3D11's `BufferType`) code points, from windows-rs at +//! the workspace's pinned rev (`acb5a1a`, +//! `crates/libs/windows/src/Windows/Win32/d3d11/mod.rs`) — the same numbers +//! DXVA2's `DXVA2_*BufferType` enumeration uses: +//! +//! | buffer | code point | +//! |---|---| +//! | picture parameters | 0 | +//! | inverse quantization matrix | 4 | +//! | slice control | 5 | +//! | bitstream | 6 | +//! +//! **Order**: picture parameters, quantization matrices, bitstream, slice +//! control. libavcodec's `ff_dxva2_common_end_frame` fills its four-entry +//! descriptor array in exactly that order and submits the array as filled; a +//! driver is entitled to care, and matching the path every Windows player +//! exercises costs nothing. +//! +//! **`DataOffset`** is 0 on every buffer, for both sides: each buffer is written +//! from its own mapping's byte 0. (libavcodec `memset`s the descriptor and never +//! writes the field.) +//! +//! **`DataSize`** is the number of bytes actually written: the whole +//! hand-declared struct for the picture parameters and the quantization matrices, +//! the packer's PADDED size for the bitstream ([`crate::pack::Packed::data_size`], +//! a multiple of [`crate::dxva::BITSTREAM_ALIGN`]), and `slices * +//! size_of::()` for the slice control — **ten** bytes per +//! record, not twelve. That number is a measured fact rather than a derivation +//! (`dxva.rs`'s alignment section carries the measurement), and the slice-control +//! `DataSize` is where it is observable from outside: 20 bytes for a two-slice +//! H.264 picture, 10 for a one-segment HEVC one. +//! +//! **`NumMBsInBuffer` is codec-ASYMMETRIC, and that is not an accident to be +//! tidied up:** +//! +//! * H.264 — `mb_width * mb_height` on the BITSTREAM and SLICE_CONTROL +//! descriptors ([`crate::pic::DecodePlanDxva::mb_count`]); +//! * HEVC — 0 on the same two. HEVC has no macroblocks and the field has no CTB +//! spelling; +//! * **AV1 — 0 on all three**, and neither a tile count nor a superblock count. +//! `dxva2_av1.c`'s `commit_bitstream_and_slice_buffer` writes a literal +//! `dsc11->NumMBsInBuffer = 0` on the bitstream descriptor and passes a literal +//! `0` as `ff_dxva2_commit_buffer`'s `mb_count` for the tile buffer; +//! * picture parameters and quantization matrices — 0 in every codec. +//! +//! That asymmetry is libavcodec's, read out of an **FFmpeg n8.1** tree: +//! `dxva2_h264.c:307` computes `const unsigned mb_count = h->mb_width * +//! h->mb_height` and writes it on the bitstream descriptor (`:412` D3D11, `:425` +//! DXVA2) and passes it for the slice-control commit (`:440-442`); +//! `dxva2_hevc.c` writes a literal 0 in the same three places (`:338`, `:349`, +//! `:359-361`); and `dxva2.c` passes a literal 0 for the two parameter buffers. +//! Setting a CTB count on the HEVC path would be a fresh divergence in the other +//! direction, which is why it is spelled out here rather than left to symmetry. +//! +//! # Presence: the quantization matrix is codec-asymmetric too +//! +//! * **H.264: always submitted.** `dxva2_h264.c:513-516` passes `&ctx_pic->qm` +//! with `sizeof(qm)` unconditionally, and the PPS's lists are always meaningful +//! (the vendored parser has already applied Table 7-2's fallback rules, so a PPS +//! that codes no matrix carries the SPS's or the flat default). +//! * **HEVC: submitted only when the sequence enables scaling lists.** +//! `dxva2_hevc.c:417` takes `int scale = ctx_pic->pp.dwCodingParamToolFlags & 1` +//! — bit 0 is `scaling_list_enabled_flag` — and `:423-426` passes `NULL`/0 when +//! it is clear; the generic layer then submits an IQ-matrix buffer only `if +//! (qm_size > 0)` (`dxva2.c` ~962), with `NumMBsInBuffer` 0. +//! [`crate::pic_h265::DecodePlanDxvaH265::qmatrix`] is `None` in exactly that +//! case, so presence here is `qmatrix.is_some()` and nothing else. Handing a +//! driver a matrix the picture parameters just told it to ignore is a bet on the +//! driver ignoring it too — and with the vendored parser leaving an uncoded list +//! all-zero, the losing side of that bet is every residual dequantizing to +//! nothing. +//! +//! * **AV1: never.** `dxva2_av1_end_frame` calls `ff_dxva2_common_end_frame` with +//! `NULL, 0` for the matrix pair, and the generic layer's `if (qm_size > 0)` +//! then skips the buffer entirely — so an AV1 submission is THREE buffers, +//! always. AV1's quantiser matrices are selected by index +//! (`qm_y`/`qm_u`/`qm_v` in `DXVA_PicParams_AV1::quantization`) out of tables +//! the decoder already has, not transmitted; there is no matrix to send. +//! +//! ⚠ The flag test is NECESSARY but not SUFFICIENT. HEVC 7.4.5 says that with +//! `scaling_list_enabled_flag` set and NO scaling-list data in either parameter +//! set, the Table 7-5/7-6 DEFAULT lists apply. FFmpeg's parser seeds those +//! defaults; the vendored cros-codecs parser leaves an uncoded SPS's lists ALL +//! ZERO. So "submit iff the flag" is only half the rule, and the other half lives +//! in [`crate::pic_h265`]'s `quantization_matrices`, which reads the PPS's copy +//! (which that parser DOES default-fill) unless the SPS is the only side that +//! coded any. All three cases are named CPU tests — two in `pic_h265.rs` for the +//! contents, three in `tests/libav_picparams_parity.rs` for the submission fact. +//! +//! # Provenance +//! +//! The libavcodec file:line references above were read out of an FFmpeg n8.1 tree +//! by this work package's coordinator, not out of this repository — there is no +//! FFmpeg source in the worktree, so nothing here can verify them, and a capture is +//! the authority. The buffer ORDER is the one claim with no line reference: it is +//! what `video_d3d11_native.rs` already submits and what +//! `ff_dxva2_common_end_frame` fills its array in, and the harness's descriptor +//! comparison is what will confirm it. + +use std::mem::size_of; + +use crate::dxva::PicParamsH264; +use crate::dxva::PicParamsHevc; +use crate::dxva::QmatrixH264; +use crate::dxva::QmatrixHevc; +use crate::dxva::SliceH264Short; +use crate::dxva::SliceHevcShort; +use crate::dxva_av1::PicParamsAv1; +use crate::dxva_av1::TileAv1; +use crate::pack::Packed; +use crate::pack_av1::PackedAv1; +use crate::pic::DecodePlanDxva; +use crate::pic_h265::DecodePlanDxvaH265; + +/// `D3D11_VIDEO_DECODER_BUFFER_PICTURE_PARAMETERS`. +pub const BUFFER_PICTURE_PARAMETERS: u32 = 0; +/// `D3D11_VIDEO_DECODER_BUFFER_INVERSE_QUANTIZATION_MATRIX`. +pub const BUFFER_INVERSE_QUANTIZATION_MATRIX: u32 = 4; +/// `D3D11_VIDEO_DECODER_BUFFER_SLICE_CONTROL`. +pub const BUFFER_SLICE_CONTROL: u32 = 5; +/// `D3D11_VIDEO_DECODER_BUFFER_BITSTREAM`. +pub const BUFFER_BITSTREAM: u32 = 6; + +/// One buffer of a submission, reduced to the fields a caller DECIDES. +/// +/// Deliberately not a `D3D11_VIDEO_DECODER_BUFFER_DESC`: that structure has +/// fourteen members, of which ten are either for a mode this backend does not use +/// (`BufferIndex`, `FirstMBaddress`, `Width`/`Height`/`Stride` — motion-compensation +/// buffers), or for protected content (`pIV`, `IVSize`, `PartialEncryption`, +/// `EncryptedBlockInfo`), or reserved. All ten are zero on every buffer this +/// backend submits, which the Windows layer expresses as `..Default::default()`; +/// the four here are the ones that carry a decision, and therefore the ones a +/// comparison against libavcodec is about. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BufferDescriptor { + /// `BufferType` — one of this module's `BUFFER_*` code points. (The DXVA + /// specs and libavcodec's DXVA2 path call the same field + /// `CompressedBufferType`.) + pub buffer_type: u32, + /// `DataOffset` — 0 on every buffer of every submission (module docs). + pub data_offset: u32, + /// `DataSize` — bytes written into the driver's mapping. + pub data_size: u32, + /// `NumMBsInBuffer` — codec-asymmetric; see the module docs. + pub num_mbs_in_buffer: u32, +} + +impl BufferDescriptor { + /// A descriptor with `DataOffset` 0, which is the only value this backend + /// ever submits. + const fn new(buffer_type: u32, data_size: u32, num_mbs_in_buffer: u32) -> BufferDescriptor { + BufferDescriptor { + buffer_type, + data_offset: 0, + data_size, + num_mbs_in_buffer, + } + } +} + +/// The slice-control buffer's `DataSize`: `n` short-format records back to back, +/// exactly as [`crate::dxva::slice_bytes`] lays them out. +/// +/// Saturating rather than panicking on the (unreachable) overflow: a `u32` holds +/// 429 million ten-byte records, and an AU that produced more has already been +/// refused by the packer. +fn slice_control_size(record_size: usize, records: usize) -> u32 { + u32::try_from(record_size.saturating_mul(records)).unwrap_or(u32::MAX) +} + +/// The descriptor set of one H.264 submission, in libavcodec's order. +/// +/// Four buffers, always: the quantization matrices travel on every H.264 picture +/// (module docs). +pub fn descriptors_h264(plan: &DecodePlanDxva, packed: &Packed) -> Vec { + let mb_count = plan.mb_count; + vec![ + BufferDescriptor::new( + BUFFER_PICTURE_PARAMETERS, + size_of::() as u32, + 0, + ), + BufferDescriptor::new( + BUFFER_INVERSE_QUANTIZATION_MATRIX, + size_of::() as u32, + 0, + ), + BufferDescriptor::new(BUFFER_BITSTREAM, packed.data_size, mb_count), + BufferDescriptor::new( + BUFFER_SLICE_CONTROL, + slice_control_size(size_of::(), packed.records.len()), + mb_count, + ), + ] +} + +/// The descriptor set of one HEVC submission, in libavcodec's order. +/// +/// THREE buffers when the sequence disables scaling lists (which is every +/// punktfunk HEVC stream and the vendored vector with it), four when it enables +/// them — and `NumMBsInBuffer` is 0 on all of them (module docs). +pub fn descriptors_h265(plan: &DecodePlanDxvaH265, packed: &Packed) -> Vec { + let mut out = Vec::with_capacity(4); + out.push(BufferDescriptor::new( + BUFFER_PICTURE_PARAMETERS, + size_of::() as u32, + 0, + )); + if plan.qmatrix.is_some() { + out.push(BufferDescriptor::new( + BUFFER_INVERSE_QUANTIZATION_MATRIX, + size_of::() as u32, + 0, + )); + } + out.push(BufferDescriptor::new(BUFFER_BITSTREAM, packed.data_size, 0)); + out.push(BufferDescriptor::new( + BUFFER_SLICE_CONTROL, + slice_control_size(size_of::(), packed.records.len()), + 0, + )); + out +} + +/// The descriptor set of one AV1 submission, in libavcodec's order. +/// +/// **THREE buffers, always**, and `NumMBsInBuffer` 0 on every one of them (module +/// docs). The slice-control buffer carries `DXVA_Tile_AV1` records — sixteen bytes +/// each, one per TILE — where the other two codecs carry ten-byte slice records. +/// +/// The bitstream `DataSize` is the packer's PADDED figure, which for AV1 is the +/// only place the padding is accounted at all: no tile record grows by it +/// ([`mod@crate::pack_av1`]). +pub fn descriptors_av1(packed: &PackedAv1) -> Vec { + vec![ + BufferDescriptor::new( + BUFFER_PICTURE_PARAMETERS, + size_of::() as u32, + 0, + ), + BufferDescriptor::new(BUFFER_BITSTREAM, packed.data_size, 0), + BufferDescriptor::new( + BUFFER_SLICE_CONTROL, + slice_control_size(size_of::(), packed.tiles.len()), + 0, + ), + ] +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dxva::PicEntry; + use crate::pack::SliceRecord; + use crate::pic::DxvaRef; + + /// A conversion result with nothing in it but the two fields the descriptors + /// read. Built by hand rather than planned from a vector: this module's job is + /// the descriptor SET, and the whole-stream evidence (250 H.264 + 250 HEVC AUs + /// through the real planners) is in `tests/libav_picparams_parity.rs`. + fn h264_plan(mb_count: u32) -> DecodePlanDxva { + DecodePlanDxva { + pic_params: PicParamsH264::zeroed(), + qmatrix: QmatrixH264::zeroed(), + slice_ranges: Vec::new(), + setup_slot: 0, + setup_id: 1, + setup_is_reference: true, + refs: Vec::::new(), + mb_count, + } + } + + fn h265_plan(qmatrix: Option) -> DecodePlanDxvaH265 { + DecodePlanDxvaH265 { + pic_params: PicParamsHevc::zeroed(), + qmatrix, + slice_ranges: Vec::new(), + setup_slot: 0, + setup_id: 1, + setup_is_reference: true, + refs: Vec::new(), + } + } + + /// `n` slices packed into `data_size` bytes; the record contents do not matter + /// here, only how many there are. + fn packed(slices: usize, data_size: u32) -> Packed { + Packed { + records: (0..slices) + .map(|i| SliceRecord { + location: i as u32 * 64, + bytes: 64, + }) + .collect(), + data_size, + } + } + + #[test] + fn the_buffer_type_code_points_are_the_ones_windows_rs_declares() { + // From the workspace's pinned windows-rs rev (`acb5a1a`), + // `crates/libs/windows/src/Windows/Win32/d3d11/mod.rs`: + // D3D11_VIDEO_DECODER_BUFFER_PICTURE_PARAMETERS = 0, + // …_INVERSE_QUANTIZATION_MATRIX = 4, …_SLICE_CONTROL = 5, …_BITSTREAM = 6. + // Nothing else in this crate can catch a transposed pair, and a + // transposition would hand the driver a bitstream where it expects slice + // control. + assert_eq!(BUFFER_PICTURE_PARAMETERS, 0); + assert_eq!(BUFFER_INVERSE_QUANTIZATION_MATRIX, 4); + assert_eq!(BUFFER_SLICE_CONTROL, 5); + assert_eq!(BUFFER_BITSTREAM, 6); + } + + #[test] + fn an_h264_submission_carries_four_buffers_in_libavcodecs_order() { + let descs = descriptors_h264(&h264_plan(300), &packed(2, 512)); + assert_eq!( + descs.iter().map(|d| d.buffer_type).collect::>(), + vec![ + BUFFER_PICTURE_PARAMETERS, + BUFFER_INVERSE_QUANTIZATION_MATRIX, + BUFFER_BITSTREAM, + BUFFER_SLICE_CONTROL, + ] + ); + assert_eq!(descs[0].data_size, 1040); + assert_eq!(descs[1].data_size, 224); + assert_eq!(descs[2].data_size, 512); + assert_eq!(descs[3].data_size, 2 * 10, "two ten-byte short records"); + } + + #[test] + fn only_the_h264_bitstream_and_slice_control_buffers_carry_a_macroblock_count() { + // Review 13's defect, in the smallest form that can express it: the field + // is 0 on the two parameter buffers and mb_width*mb_height on the two the + // hardware parses. + let descs = descriptors_h264(&h264_plan(300), &packed(1, 256)); + assert_eq!(descs[0].num_mbs_in_buffer, 0, "picture parameters"); + assert_eq!(descs[1].num_mbs_in_buffer, 0, "quantization matrices"); + assert_eq!(descs[2].num_mbs_in_buffer, 300, "bitstream"); + assert_eq!(descs[3].num_mbs_in_buffer, 300, "slice control"); + } + + #[test] + fn an_hevc_submission_omits_the_quantization_matrix_buffer_when_there_is_none() { + let descs = descriptors_h265(&h265_plan(None), &packed(1, 384)); + assert_eq!( + descs.iter().map(|d| d.buffer_type).collect::>(), + vec![ + BUFFER_PICTURE_PARAMETERS, + BUFFER_BITSTREAM, + BUFFER_SLICE_CONTROL, + ], + "a submission with no matrix must not carry an empty matrix buffer" + ); + assert!(descs + .iter() + .all(|d| d.buffer_type != BUFFER_INVERSE_QUANTIZATION_MATRIX)); + } + + #[test] + fn an_hevc_submission_carries_the_quantization_matrix_buffer_when_there_is_one() { + let descs = descriptors_h265(&h265_plan(Some(QmatrixHevc::zeroed())), &packed(3, 640)); + assert_eq!( + descs.iter().map(|d| d.buffer_type).collect::>(), + vec![ + BUFFER_PICTURE_PARAMETERS, + BUFFER_INVERSE_QUANTIZATION_MATRIX, + BUFFER_BITSTREAM, + BUFFER_SLICE_CONTROL, + ] + ); + assert_eq!(descs[0].data_size, 232); + assert_eq!(descs[1].data_size, 1000); + assert_eq!(descs[2].data_size, 640); + assert_eq!(descs[3].data_size, 3 * 10, "three ten-byte short records"); + } + + #[test] + fn the_hevc_descriptors_carry_no_macroblock_count_at_all() { + // The asymmetry, asserted rather than assumed: libavcodec's HEVC path + // writes 0 where its H.264 path writes mb_width*mb_height, and a CTB count + // here would be a divergence in the other direction. + for descs in [ + descriptors_h265(&h265_plan(None), &packed(1, 256)), + descriptors_h265(&h265_plan(Some(QmatrixHevc::zeroed())), &packed(4, 1024)), + ] { + for desc in descs { + assert_eq!( + desc.num_mbs_in_buffer, 0, + "buffer type {} carries a macroblock count", + desc.buffer_type + ); + } + } + } + + #[test] + fn every_descriptor_starts_at_byte_zero_of_its_own_buffer() { + let h264 = descriptors_h264(&h264_plan(1), &packed(2, 256)); + let h265 = descriptors_h265(&h265_plan(Some(QmatrixHevc::zeroed())), &packed(2, 256)); + for desc in h264.into_iter().chain(h265) { + assert_eq!(desc.data_offset, 0); + } + } + + #[test] + fn the_slice_control_size_is_one_short_format_record_per_slice() { + // TEN bytes per record is the SHORT format, packed — measured against + // libavcodec on hardware, not derived from the field types (a `#[repr(C)]` + // `{u32, u32, u16}` would be twelve). The long format's record is an order of + // magnitude larger, so this size is also the check that the records match the + // `ConfigBitstreamRaw` this backend asks for. + assert_eq!(size_of::(), 10); + assert_eq!(size_of::(), 10); + for slices in [1usize, 2, 5, 68] { + let h264 = descriptors_h264(&h264_plan(1), &packed(slices, 4096)); + assert_eq!(h264[3].data_size, 10 * slices as u32); + let h265 = descriptors_h265(&h265_plan(None), &packed(slices, 4096)); + assert_eq!(h265[2].data_size, 10 * slices as u32); + } + } + + /// `n` tiles packed into `data_size` bytes. + fn packed_av1(tiles: usize, data_size: u32) -> PackedAv1 { + PackedAv1 { + tiles: (0..tiles) + .map(|i| TileAv1 { + data_offset: i as u32 * 64, + data_size: 64, + row: 0, + column: i as u16, + ..Default::default() + }) + .collect(), + data_size, + } + } + + #[test] + fn an_av1_submission_carries_three_buffers_and_never_a_quantization_matrix() { + // `dxva2_av1_end_frame` passes `NULL, 0` for the matrix pair, so the + // generic layer's `if (qm_size > 0)` never fires. A fourth buffer here + // would be a matrix AV1 does not transmit at all. + let descs = descriptors_av1(&packed_av1(1, 384)); + assert_eq!( + descs.iter().map(|d| d.buffer_type).collect::>(), + vec![ + BUFFER_PICTURE_PARAMETERS, + BUFFER_BITSTREAM, + BUFFER_SLICE_CONTROL, + ] + ); + assert_eq!(descs[0].data_size, 912, "DXVA_PicParams_AV1, measured"); + assert_eq!(descs[1].data_size, 384); + assert_eq!(descs[2].data_size, 16, "one sixteen-byte DXVA_Tile_AV1"); + } + + #[test] + fn the_av1_descriptors_carry_no_macroblock_count_at_all() { + // The third spelling of the asymmetry: H.264 writes mb_width*mb_height, + // HEVC writes 0, AV1 writes 0 — and specifically NOT a tile count, which + // is the symmetric-looking value there is now a plausible field for. + for tiles in [1usize, 4, 64] { + for desc in descriptors_av1(&packed_av1(tiles, 4096)) { + assert_eq!( + desc.num_mbs_in_buffer, 0, + "buffer type {} carries a macroblock count", + desc.buffer_type + ); + assert_eq!(desc.data_offset, 0); + } + } + } + + #[test] + fn the_av1_tile_buffer_is_sixteen_bytes_per_tile_not_ten() { + // The slice-control buffer is the one place a codec's record SIZE is + // observable from outside, and AV1's record is a different structure from + // the other two: `DXVA_Tile_AV1` is 16 bytes (measured against the Windows + // SDK's `dxva.h`), where `DXVA_Slice_*_Short` is 10. + assert_eq!(size_of::(), 16); + for tiles in [1usize, 2, 8, 64] { + let descs = descriptors_av1(&packed_av1(tiles, 4096)); + assert_eq!(descs[2].data_size, 16 * tiles as u32); + } + } + + #[test] + fn a_reference_entry_in_the_plan_does_not_reach_the_descriptors() { + // A guard on the shape of this module rather than on a value: descriptors + // are a function of SIZES and the macroblock count, so nothing about the + // reference list may leak into them. (Also keeps `DxvaRef` in the test's + // vocabulary, so the plan built above stays a realistic one.) + let mut plan = h264_plan(300); + plan.refs.push(DxvaRef { + slot: 2, + id: 7, + is_long_term: true, + top_field_order_cnt: 4, + bottom_field_order_cnt: 4, + frame_num_or_lt_idx: 1, + }); + plan.pic_params.RefFrameList[0] = PicEntry::new(2, true); + assert_eq!( + descriptors_h264(&plan, &packed(1, 256)), + descriptors_h264(&h264_plan(300), &packed(1, 256)) + ); + } +} diff --git a/crates/pf-dxvadec/src/dxva.rs b/crates/pf-dxvadec/src/dxva.rs new file mode 100644 index 00000000..82b5ba61 --- /dev/null +++ b/crates/pf-dxvadec/src/dxva.rs @@ -0,0 +1,1292 @@ +//! The DXVA buffer layouts, hand-declared. +//! +//! # Why these are written out by hand +//! +//! `ID3D11VideoDecoder` is fed C structures from `dxva.h` — `DXVA_PicParams_H264`, +//! `DXVA_PicParams_HEVC`, their quantization matrices and their slice-control +//! records. **windows-rs does not generate any of them**, verified against the +//! pinned rev (`acb5a1a`): the whole `d3d11` header module is present — the +//! interfaces (`ID3D11VideoDevice::CreateVideoDecoder`, +//! `ID3D11VideoContext::{GetDecoderBuffer, ReleaseDecoderBuffer, +//! SubmitDecoderBuffers, DecoderBeginFrame, DecoderEndFrame}`), the descriptors +//! (`D3D11_VIDEO_DECODER_{DESC,CONFIG,BUFFER_DESC}`) and the buffer-type constants +//! — but `dxva.h` itself is not part of the Win32 metadata, so a grep for +//! `DXVA_PicParams_*`/`DXVA_Slice_*`/`DXVA_Qmatrix_*` across the entire generated +//! tree returns nothing. FFmpeg is in the same position on MinGW and does the same +//! thing (its `compat/` mirrors and `libavcodec/dxva2_*.c`), so this module is the +//! standard answer, not a shortcut. +//! +//! # Why this is the most safety-critical file in the backend +//! +//! Nothing here is type-checked against Windows. A field at the wrong offset, a +//! bitfield packed from the wrong end, a `CHAR` declared `u8` — none of that is a +//! compile error; it is a driver reading garbage where a QP or a reference index +//! should be, which surfaces as silent picture corruption on someone else's screen. +//! Three defences, all of them in this file: +//! +//! 1. **Every struct's size AND every field's offset** is asserted at compile time +//! against the value derived from the C declaration reproduced in each struct's +//! doc comment (`const _: () = { … }` blocks, the same technique +//! `video_d3d11.rs` uses to pin libav's `AVD3D11VA*Context` ABI). A field +//! inserted, reordered or mis-typed cannot build. +//! 2. **Bitfield words are never expressed as Rust "bitfields"** (Rust has none): +//! each packed word is a plain integer with named `set_*` builders whose bit +//! positions are written as literals next to the C declaration they come from. +//! MSVC allocates C bitfields from the least significant bit of the storage +//! unit upward in declaration order, which is what the literals encode, and +//! which the tests below check against independently-derived expected words. +//! 3. **No `unsafe` reaches the layout.** Every struct is constructed field by +//! field from a `const fn zeroed()` (real zeros, not `mem::zeroed`), so this +//! module compiles and its tests run on macOS and Linux exactly as on Windows. +//! The one unsafe in the crate is [`as_bytes`], and it is fenced behind a +//! sealed trait that only these `#[repr(C)]` PODs implement. +//! +//! # Alignment — and the one place natural alignment is WRONG +//! +//! `dxva.h` declares these as wire-format structures under **1-byte packing**, not +//! under MSVC's default. For five of the six that is indistinguishable from natural +//! alignment, because every member happens to sit at a naturally-aligned offset and +//! every total is already a multiple of 4: `DXVA_PicParams_H264` is 1040, +//! `DXVA_PicParams_HEVC` 232, the two quantization matrices 224 and 1000 — all +//! confirmed against libavcodec's runtime `sizeof` in the n8.1 capture described in +//! `tests/libav_picparams_parity.rs`. +//! +//! The slice-control records are the exception and the reason this section exists. +//! `{UINT, UINT, USHORT}` is **10 bytes packed and 12 under natural alignment**, and +//! an earlier revision of this file declared them plain `#[repr(C)]` — asserting 12 +//! with "2 bytes tail padding" in the comment, which was a guess dressed as a proof. +//! Measured on hardware (RTX 4090, patched FFmpeg n8.1, both vendored vectors, 250 +//! AUs each): the H.264 slice-control buffer's `DataSize` is 20 on a stream with two +//! slices per picture, and the HEVC one's is 10 on a stream with one slice segment +//! per picture. Two codecs, two slice counts, one answer — 10. +//! +//! What the mistake costs, so it is never re-introduced: record 0's fields land at +//! 0/4/8 either way, so a SINGLE-slice stream decodes correctly and the two extra +//! bytes are trailing slop nobody reads. From the second record on, every field is +//! displaced by two bytes per preceding record, so the driver reads a slice offset +//! built from half of one field and half of the next. punktfunk hosts do emit +//! multi-slice streams. +//! +//! Hence: **the packed structs carry `#[repr(C, packed)]`** and the proofs below +//! pin `align_of` as well as `size_of`, plus — for every struct — that its size is +//! exactly its last member's offset plus that member's size, which is the assertion +//! that would have caught this one. Interior padding was already impossible (the +//! per-field offset asserts see it); it was TAIL padding that got in. +//! +//! Sources: the DXVA specifications "DirectX Video Acceleration Specification for +//! H.264/AVC Decoding" (§4.2 `DXVA_PicParams_H264`, §4.4 `DXVA_Qmatrix_H264`, §4.6 +//! `DXVA_Slice_H264_Short`) and "DirectX Video Acceleration Specification for +//! HEVC/H.265 Decoding" (§4.1 `DXVA_PicParams_HEVC`, §4.2 `DXVA_Qmatrix_HEVC`, +//! §4.3 `DXVA_Slice_HEVC_Short`), cross-read against mingw-w64's `dxva.h`. + +// The field names are the DXVA specs' names, character for character. Renaming +// them to snake_case would make every line of the conversion modules — and every +// review of it against the spec text or against libavcodec's `dxva2_*.c` — a +// translation exercise, which is exactly the kind of friction that lets a +// mis-assigned field survive a reading. `windows-rs` makes the same choice for +// every generated Win32 struct. +#![allow(non_snake_case)] + +use std::mem::align_of; +use std::mem::offset_of; +use std::mem::size_of; + +/// The `bPicEntry` sentinel for an unused `RefFrameList`/`RefPicList` slot, and +/// for an unused `RefPicSet*` index-array entry: `Index7Bits = 0x7F`, +/// `AssociatedFlag = 1`. Both DXVA specs name `0xFF` explicitly, and it is what +/// every shipping decoder pads with. +pub const UNUSED_ENTRY: u8 = 0xFF; + +/// The bitstream buffer's size granule. The DXVA specs require the submitted +/// bitstream data size to be a multiple of 128 bytes, zero-padded at the end, +/// with the padding charged to the last slice's `SliceBytesInBuffer`. +pub const BITSTREAM_ALIGN: usize = 128; + +/// `DXVA_PicEntry_H264` / `DXVA_PicEntry_HEVC` — one byte, identical in both specs: +/// +/// ```c +/// typedef struct _DXVA_PicEntry_H264 { +/// union { +/// struct { +/// UCHAR Index7Bits : 7; +/// UCHAR AssociatedFlag : 1; +/// }; +/// UCHAR bPicEntry; +/// }; +/// } DXVA_PicEntry_H264; /* 1 byte */ +/// ``` +/// +/// `Index7Bits` is the **uncompressed surface index** — for D3D11VA, the +/// `ArraySlice` of the decode texture array the picture lives in, which is +/// exactly the DPB slot index this backend's [`crate::SlotMap`] hands out. +/// `AssociatedFlag` means "bottom field" on `CurrPic` and "long-term reference" +/// on a `RefFrameList`/`RefPicList` entry. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(C)] +pub struct PicEntry(pub u8); + +impl PicEntry { + /// An unused entry (`0xFF`). + pub const UNUSED: PicEntry = PicEntry(UNUSED_ENTRY); + + /// A surface index plus the entry's associated flag. + /// + /// `index` is masked to seven bits rather than checked: the callers pass DPB + /// slot indices bounded by an envelope-gated 17-slot map, so the mask is + /// unreachable — and a `debug_assert` says so without giving a corrupt + /// stream a way to panic a release client. + pub const fn new(index: u8, associated: bool) -> PicEntry { + debug_assert!(index < 0x80, "a surface index must fit seven bits"); + PicEntry((index & 0x7F) | ((associated as u8) << 7)) + } + + /// The surface index (`Index7Bits`). + pub const fn index(self) -> u8 { + self.0 & 0x7F + } + + /// The associated flag (bottom-field / long-term, per field). + pub const fn associated(self) -> bool { + self.0 & 0x80 != 0 + } +} + +// --------------------------------------------------------------------------- +// H.264 +// --------------------------------------------------------------------------- + +/// `DXVA_PicParams_H264` — the H.264 picture-parameters buffer +/// (`D3D11_VIDEO_DECODER_BUFFER_PICTURE_PARAMETERS`). +/// +/// ```c +/// typedef struct _DXVA_PicParams_H264 { +/// USHORT wFrameWidthInMbsMinus1; /* 0 */ +/// USHORT wFrameHeightInMbsMinus1; /* 2 */ +/// DXVA_PicEntry_H264 CurrPic; /* 4 */ +/// UCHAR num_ref_frames; /* 5 */ +/// union { struct { ...15 bitfields... }; USHORT wBitFields; }; /* 6 */ +/// UCHAR bit_depth_luma_minus8; /* 8 */ +/// UCHAR bit_depth_chroma_minus8; /* 9 */ +/// USHORT Reserved16Bits; /* 10 */ +/// UINT StatusReportFeedbackNumber; /* 12 */ +/// DXVA_PicEntry_H264 RefFrameList[16]; /* 16 */ +/// INT CurrFieldOrderCnt[2]; /* 32 */ +/// INT FieldOrderCntList[16][2]; /* 40 */ +/// CHAR pic_init_qs_minus26; /* 168 */ +/// CHAR chroma_qp_index_offset; /* 169 */ +/// CHAR second_chroma_qp_index_offset; /* 170 */ +/// UCHAR ContinuationFlag; /* 171 */ +/// CHAR pic_init_qp_minus26; /* 172 */ +/// UCHAR num_ref_idx_l0_active_minus1; /* 173 */ +/// UCHAR num_ref_idx_l1_active_minus1; /* 174 */ +/// UCHAR Reserved8BitsA; /* 175 */ +/// USHORT FrameNumList[16]; /* 176 */ +/// UINT UsedForReferenceFlags; /* 208 */ +/// USHORT NonExistingFrameFlags; /* 212 */ +/// USHORT frame_num; /* 214 */ +/// UCHAR log2_max_frame_num_minus4; /* 216 */ +/// UCHAR pic_order_cnt_type; /* 217 */ +/// UCHAR log2_max_pic_order_cnt_lsb_minus4; /* 218 */ +/// UCHAR delta_pic_order_always_zero_flag; /* 219 */ +/// UCHAR direct_8x8_inference_flag; /* 220 */ +/// UCHAR entropy_coding_mode_flag; /* 221 */ +/// UCHAR pic_order_present_flag; /* 222 */ +/// UCHAR num_slice_groups_minus1; /* 223 */ +/// UCHAR slice_group_map_type; /* 224 */ +/// UCHAR deblocking_filter_control_present_flag; /* 225 */ +/// UCHAR redundant_pic_cnt_present_flag; /* 226 */ +/// UCHAR Reserved8BitsB; /* 227 */ +/// USHORT slice_group_change_rate_minus1; /* 228 */ +/// UCHAR SliceGroupMap[810]; /* 230 */ +/// } DXVA_PicParams_H264; /* 1040 bytes */ +/// ``` +/// +/// `CHAR` is signed on MSVC for these members (the spec calls them signed +/// quantities: QP deltas and chroma offsets are negative in real streams), hence +/// `i8` where the C says `CHAR` and `u8` where it says `UCHAR`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct PicParamsH264 { + pub wFrameWidthInMbsMinus1: u16, + pub wFrameHeightInMbsMinus1: u16, + pub CurrPic: PicEntry, + pub num_ref_frames: u8, + /// The packed bitfield word — build it with [`H264BitFields`]. + pub wBitFields: u16, + pub bit_depth_luma_minus8: u8, + pub bit_depth_chroma_minus8: u8, + pub Reserved16Bits: u16, + pub StatusReportFeedbackNumber: u32, + pub RefFrameList: [PicEntry; 16], + pub CurrFieldOrderCnt: [i32; 2], + pub FieldOrderCntList: [[i32; 2]; 16], + pub pic_init_qs_minus26: i8, + pub chroma_qp_index_offset: i8, + pub second_chroma_qp_index_offset: i8, + pub ContinuationFlag: u8, + pub pic_init_qp_minus26: i8, + pub num_ref_idx_l0_active_minus1: u8, + pub num_ref_idx_l1_active_minus1: u8, + pub Reserved8BitsA: u8, + pub FrameNumList: [u16; 16], + pub UsedForReferenceFlags: u32, + pub NonExistingFrameFlags: u16, + pub frame_num: u16, + pub log2_max_frame_num_minus4: u8, + pub pic_order_cnt_type: u8, + pub log2_max_pic_order_cnt_lsb_minus4: u8, + pub delta_pic_order_always_zero_flag: u8, + pub direct_8x8_inference_flag: u8, + pub entropy_coding_mode_flag: u8, + pub pic_order_present_flag: u8, + pub num_slice_groups_minus1: u8, + pub slice_group_map_type: u8, + pub deblocking_filter_control_present_flag: u8, + pub redundant_pic_cnt_present_flag: u8, + pub Reserved8BitsB: u8, + pub slice_group_change_rate_minus1: u16, + /// Flexible-macroblock-ordering map. Always all-zero here: this backend + /// refuses a stream with slice groups outright (see + /// [`crate::pic::PlanToDxvaError::SliceGroups`]) rather than submit a map it + /// did not derive. + pub SliceGroupMap: [u8; 810], +} + +impl PicParamsH264 { + /// An all-zero picture-parameters buffer. Written out as real zeros rather + /// than `mem::zeroed` so the whole crate stays free of unsafe construction — + /// and so a field added to the struct without a value here is a compile + /// error, not a silently-zero field. + pub const fn zeroed() -> PicParamsH264 { + PicParamsH264 { + wFrameWidthInMbsMinus1: 0, + wFrameHeightInMbsMinus1: 0, + CurrPic: PicEntry(0), + num_ref_frames: 0, + wBitFields: 0, + bit_depth_luma_minus8: 0, + bit_depth_chroma_minus8: 0, + Reserved16Bits: 0, + StatusReportFeedbackNumber: 0, + RefFrameList: [PicEntry(0); 16], + CurrFieldOrderCnt: [0; 2], + FieldOrderCntList: [[0; 2]; 16], + pic_init_qs_minus26: 0, + chroma_qp_index_offset: 0, + second_chroma_qp_index_offset: 0, + ContinuationFlag: 0, + pic_init_qp_minus26: 0, + num_ref_idx_l0_active_minus1: 0, + num_ref_idx_l1_active_minus1: 0, + Reserved8BitsA: 0, + FrameNumList: [0; 16], + UsedForReferenceFlags: 0, + NonExistingFrameFlags: 0, + frame_num: 0, + log2_max_frame_num_minus4: 0, + pic_order_cnt_type: 0, + log2_max_pic_order_cnt_lsb_minus4: 0, + delta_pic_order_always_zero_flag: 0, + direct_8x8_inference_flag: 0, + entropy_coding_mode_flag: 0, + pic_order_present_flag: 0, + num_slice_groups_minus1: 0, + slice_group_map_type: 0, + deblocking_filter_control_present_flag: 0, + redundant_pic_cnt_present_flag: 0, + Reserved8BitsB: 0, + slice_group_change_rate_minus1: 0, + SliceGroupMap: [0; 810], + } + } +} + +/// The fifteen bitfields packed into [`PicParamsH264::wBitFields`], as a builder. +/// +/// ```c +/// USHORT field_pic_flag : 1; /* bit 0 */ +/// USHORT MbaffFrameFlag : 1; /* bit 1 */ +/// USHORT residual_colour_transform_flag : 1; /* bit 2 */ +/// USHORT sp_for_switch_flag : 1; /* bit 3 */ +/// USHORT chroma_format_idc : 2; /* bits 4-5 */ +/// USHORT RefPicFlag : 1; /* bit 6 */ +/// USHORT constrained_intra_pred_flag : 1; /* bit 7 */ +/// USHORT weighted_pred_flag : 1; /* bit 8 */ +/// USHORT weighted_bipred_idc : 2; /* bits 9-10 */ +/// USHORT MbsConsecutiveFlag : 1; /* bit 11 */ +/// USHORT frame_mbs_only_flag : 1; /* bit 12 */ +/// USHORT transform_8x8_mode_flag : 1; /* bit 13 */ +/// USHORT MinLumaBipredSize8x8Flag : 1; /* bit 14 */ +/// USHORT IntraPicFlag : 1; /* bit 15 */ +/// ``` +/// +/// `field_pic_flag`, `MbaffFrameFlag` and `residual_colour_transform_flag` are +/// always zero for this backend: pf-bitstream's envelope gate rejects interlaced +/// and separate-colour-plane streams before a plan exists, so writing them from a +/// parsed value would only be a way to smuggle a stream past that gate. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct H264BitFields { + pub chroma_format_idc: u8, + pub ref_pic_flag: bool, + pub constrained_intra_pred_flag: bool, + pub weighted_pred_flag: bool, + pub weighted_bipred_idc: u8, + pub frame_mbs_only_flag: bool, + pub transform_8x8_mode_flag: bool, + /// `MinLumaBipredSize8x8Flag` — level 3.1 and above (the DXVA spec defines it + /// as `level_idc >= 31`, and libavcodec's DXVA path writes exactly that). + pub min_luma_bipred_size_8x8: bool, + /// Every slice of the AU is I or SI. + pub intra_pic_flag: bool, +} + +impl H264BitFields { + /// Pack to the `wBitFields` word. Two-bit members are masked, not checked: + /// `chroma_format_idc` and `weighted_bipred_idc` are both spec-bounded to + /// 0..=3 and the planner's envelope gate has already refused anything else, + /// so a mask can only ever be a no-op — but it is a no-op that cannot panic + /// on a hostile stream. + pub const fn pack(self) -> u16 { + // `MbsConsecutiveFlag` (bit 11) is hard-1: it means "macroblocks are in + // raster order within a slice", which is true for everything that is not + // flexible macroblock ordering — and FMO is refused before this runs. + ((self.chroma_format_idc as u16 & 0x3) << 4) + | ((self.ref_pic_flag as u16) << 6) + | ((self.constrained_intra_pred_flag as u16) << 7) + | ((self.weighted_pred_flag as u16) << 8) + | ((self.weighted_bipred_idc as u16 & 0x3) << 9) + | (1 << 11) + | ((self.frame_mbs_only_flag as u16) << 12) + | ((self.transform_8x8_mode_flag as u16) << 13) + | ((self.min_luma_bipred_size_8x8 as u16) << 14) + | ((self.intra_pic_flag as u16) << 15) + } +} + +/// `DXVA_Qmatrix_H264` — the H.264 inverse-quantization matrix buffer +/// (`D3D11_VIDEO_DECODER_BUFFER_INVERSE_QUANTIZATION_MATRIX`). +/// +/// ```c +/// typedef struct _DXVA_Qmatrix_H264 { +/// UCHAR bScalingLists4x4[6][16]; /* 0 */ +/// UCHAR bScalingLists8x8[2][64]; /* 96 */ +/// } DXVA_Qmatrix_H264; /* 224 bytes */ +/// ``` +/// +/// Entry `[i][j]` is `ScalingList4x4[i][j]` / `ScalingList8x8[i][j]` **in the +/// order the bitstream codes them** (7.3.2.1.1.1), which is the zig-zag order — +/// not the raster order the inverse-scan produces. The vendored parser stores +/// them in exactly that coded order (`parse_scaling_list` writes `scaling_list[j]` +/// for the j-th coded coefficient), so the conversion is a straight copy. +/// +/// Only the first two 8x8 lists travel: DXVA carries `Intra Y` and `Inter Y` +/// (H.264 list indices 0 and 3), because 8x8 chroma lists exist only in 4:4:4 +/// profiles, which this backend does not decode. +/// +/// ⚠ Old ATI/AMD UVD parts wanted these lists in RASTER order instead +/// (libavcodec's `FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG`). No such workaround is +/// implemented here and none is expected to be needed: punktfunk hosts encode with +/// flat scaling lists, so the buffer is all-defaults on every stream this client +/// will ever see. If a field report ever shows blockiness that tracks a non-flat +/// SPS/PPS matrix on old AMD, this comment is the place to start. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct QmatrixH264 { + pub bScalingLists4x4: [[u8; 16]; 6], + pub bScalingLists8x8: [[u8; 64]; 2], +} + +impl QmatrixH264 { + /// An all-zero quantization-matrix buffer. + pub const fn zeroed() -> QmatrixH264 { + QmatrixH264 { + bScalingLists4x4: [[0; 16]; 6], + bScalingLists8x8: [[0; 64]; 2], + } + } +} + +/// `DXVA_Slice_H264_Short` — one entry of the H.264 slice-control buffer +/// (`D3D11_VIDEO_DECODER_BUFFER_SLICE_CONTROL`), short format. +/// +/// ```c +/// typedef struct _DXVA_Slice_H264_Short { +/// UINT BSNALunitDataLocation; /* 0 */ +/// UINT SliceBytesInBuffer; /* 4 */ +/// USHORT wBadSliceChopping; /* 8 */ +/// } DXVA_Slice_H264_Short; /* 10 bytes — PACKED, no tail padding */ +/// ``` +/// +/// **Ten bytes, not twelve** — `#[repr(C, packed)]`, and the single most important +/// number in this file after the picture-parameters offsets. See the module docs' +/// alignment section for the hardware measurement it comes from and for what +/// getting it wrong does to every record after the first. +/// +/// Short format only. The long format (`DXVA_Slice_H264_Long`) additionally +/// carries the derived reference lists and the prediction weight tables, which +/// this backend does not build — a device offering only long-format configs is +/// refused at decoder creation and the ladder answers with the FFmpeg rung, which +/// does implement both. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[repr(C, packed)] +pub struct SliceH264Short { + /// Byte offset of the slice's **start code** within the bitstream buffer. + pub BSNALunitDataLocation: u32, + /// Start code + NALU bytes; the last slice of an AU also carries the + /// buffer's trailing 128-byte alignment padding. + pub SliceBytesInBuffer: u32, + /// 0 — a whole slice, in one buffer. The nonzero values describe a slice + /// split across bitstream buffers, which this backend never does. + pub wBadSliceChopping: u16, +} + +// --------------------------------------------------------------------------- +// HEVC +// --------------------------------------------------------------------------- + +/// `DXVA_PicParams_HEVC` — the HEVC picture-parameters buffer. +/// +/// ```c +/// typedef struct _DXVA_PicParams_HEVC { +/// USHORT PicWidthInMinCbsY; /* 0 */ +/// USHORT PicHeightInMinCbsY; /* 2 */ +/// union { ...8 bitfields...; USHORT wFormatAndSequenceInfoFlags; }; /* 4 */ +/// DXVA_PicEntry_HEVC CurrPic; /* 6 */ +/// UCHAR sps_max_dec_pic_buffering_minus1; /* 7 */ +/// UCHAR log2_min_luma_coding_block_size_minus3; /* 8 */ +/// UCHAR log2_diff_max_min_luma_coding_block_size; /* 9 */ +/// UCHAR log2_min_transform_block_size_minus2; /* 10 */ +/// UCHAR log2_diff_max_min_transform_block_size; /* 11 */ +/// UCHAR max_transform_hierarchy_depth_inter; /* 12 */ +/// UCHAR max_transform_hierarchy_depth_intra; /* 13 */ +/// UCHAR num_short_term_ref_pic_sets; /* 14 */ +/// UCHAR num_long_term_ref_pics_sps; /* 15 */ +/// UCHAR num_ref_idx_l0_default_active_minus1; /* 16 */ +/// UCHAR num_ref_idx_l1_default_active_minus1; /* 17 */ +/// CHAR init_qp_minus26; /* 18 */ +/// UCHAR ucNumDeltaPocsOfRefRpsIdx; /* 19 */ +/// USHORT wNumBitsForShortTermRPSInSlice; /* 20 */ +/// USHORT ReservedBits2; /* 22 */ +/// union { ...; UINT32 dwCodingParamToolFlags; }; /* 24 */ +/// union { ...; UINT32 dwCodingSettingPicturePropertyFlags; }; /* 28 */ +/// CHAR pps_cb_qp_offset; /* 32 */ +/// CHAR pps_cr_qp_offset; /* 33 */ +/// UCHAR num_tile_columns_minus1; /* 34 */ +/// UCHAR num_tile_rows_minus1; /* 35 */ +/// USHORT column_width_minus1[19]; /* 36 */ +/// USHORT row_height_minus1[21]; /* 74 */ +/// UCHAR diff_cu_qp_delta_depth; /* 116 */ +/// CHAR pps_beta_offset_div2; /* 117 */ +/// CHAR pps_tc_offset_div2; /* 118 */ +/// UCHAR log2_parallel_merge_level_minus2; /* 119 */ +/// INT CurrPicOrderCntVal; /* 120 */ +/// DXVA_PicEntry_HEVC RefPicList[15]; /* 124 */ +/// UCHAR ReservedBits5; /* 139 */ +/// INT PicOrderCntValList[15]; /* 140 */ +/// UCHAR RefPicSetStCurrBefore[8]; /* 200 */ +/// UCHAR RefPicSetStCurrAfter[8]; /* 208 */ +/// UCHAR RefPicSetLtCurr[8]; /* 216 */ +/// USHORT ReservedBits6; /* 224 */ +/// USHORT ReservedBits7; /* 226 */ +/// UINT StatusReportFeedbackNumber; /* 228 */ +/// } DXVA_PicParams_HEVC; /* 232 bytes */ +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct PicParamsHevc { + pub PicWidthInMinCbsY: u16, + pub PicHeightInMinCbsY: u16, + /// Packed word — build it with [`HevcFormatFlags`]. + pub wFormatAndSequenceInfoFlags: u16, + pub CurrPic: PicEntry, + pub sps_max_dec_pic_buffering_minus1: u8, + pub log2_min_luma_coding_block_size_minus3: u8, + pub log2_diff_max_min_luma_coding_block_size: u8, + pub log2_min_transform_block_size_minus2: u8, + pub log2_diff_max_min_transform_block_size: u8, + pub max_transform_hierarchy_depth_inter: u8, + pub max_transform_hierarchy_depth_intra: u8, + pub num_short_term_ref_pic_sets: u8, + pub num_long_term_ref_pics_sps: u8, + pub num_ref_idx_l0_default_active_minus1: u8, + pub num_ref_idx_l1_default_active_minus1: u8, + pub init_qp_minus26: i8, + pub ucNumDeltaPocsOfRefRpsIdx: u8, + pub wNumBitsForShortTermRPSInSlice: u16, + pub ReservedBits2: u16, + /// Packed word — build it with [`HevcToolFlags`]. + pub dwCodingParamToolFlags: u32, + /// Packed word — build it with [`HevcPictureFlags`]. + pub dwCodingSettingPicturePropertyFlags: u32, + pub pps_cb_qp_offset: i8, + pub pps_cr_qp_offset: i8, + pub num_tile_columns_minus1: u8, + pub num_tile_rows_minus1: u8, + pub column_width_minus1: [u16; 19], + pub row_height_minus1: [u16; 21], + pub diff_cu_qp_delta_depth: u8, + pub pps_beta_offset_div2: i8, + pub pps_tc_offset_div2: i8, + pub log2_parallel_merge_level_minus2: u8, + pub CurrPicOrderCntVal: i32, + pub RefPicList: [PicEntry; 15], + pub ReservedBits5: u8, + pub PicOrderCntValList: [i32; 15], + pub RefPicSetStCurrBefore: [u8; 8], + pub RefPicSetStCurrAfter: [u8; 8], + pub RefPicSetLtCurr: [u8; 8], + pub ReservedBits6: u16, + pub ReservedBits7: u16, + pub StatusReportFeedbackNumber: u32, +} + +impl PicParamsHevc { + /// An all-zero picture-parameters buffer. + pub const fn zeroed() -> PicParamsHevc { + PicParamsHevc { + PicWidthInMinCbsY: 0, + PicHeightInMinCbsY: 0, + wFormatAndSequenceInfoFlags: 0, + CurrPic: PicEntry(0), + sps_max_dec_pic_buffering_minus1: 0, + log2_min_luma_coding_block_size_minus3: 0, + log2_diff_max_min_luma_coding_block_size: 0, + log2_min_transform_block_size_minus2: 0, + log2_diff_max_min_transform_block_size: 0, + max_transform_hierarchy_depth_inter: 0, + max_transform_hierarchy_depth_intra: 0, + num_short_term_ref_pic_sets: 0, + num_long_term_ref_pics_sps: 0, + num_ref_idx_l0_default_active_minus1: 0, + num_ref_idx_l1_default_active_minus1: 0, + init_qp_minus26: 0, + ucNumDeltaPocsOfRefRpsIdx: 0, + wNumBitsForShortTermRPSInSlice: 0, + ReservedBits2: 0, + dwCodingParamToolFlags: 0, + dwCodingSettingPicturePropertyFlags: 0, + pps_cb_qp_offset: 0, + pps_cr_qp_offset: 0, + num_tile_columns_minus1: 0, + num_tile_rows_minus1: 0, + column_width_minus1: [0; 19], + row_height_minus1: [0; 21], + diff_cu_qp_delta_depth: 0, + pps_beta_offset_div2: 0, + pps_tc_offset_div2: 0, + log2_parallel_merge_level_minus2: 0, + CurrPicOrderCntVal: 0, + RefPicList: [PicEntry(0); 15], + ReservedBits5: 0, + PicOrderCntValList: [0; 15], + RefPicSetStCurrBefore: [0; 8], + RefPicSetStCurrAfter: [0; 8], + RefPicSetLtCurr: [0; 8], + ReservedBits6: 0, + ReservedBits7: 0, + StatusReportFeedbackNumber: 0, + } + } +} + +/// [`PicParamsHevc::wFormatAndSequenceInfoFlags`], as a builder. +/// +/// ```c +/// USHORT chroma_format_idc : 2; /* bits 0-1 */ +/// USHORT separate_colour_plane_flag : 1; /* bit 2 */ +/// USHORT bit_depth_luma_minus8 : 3; /* bits 3-5 */ +/// USHORT bit_depth_chroma_minus8 : 3; /* bits 6-8 */ +/// USHORT log2_max_pic_order_cnt_lsb_minus4 : 4; /* bits 9-12 */ +/// USHORT NoPicReorderingFlag : 1; /* bit 13 */ +/// USHORT NoBiPredFlag : 1; /* bit 14 */ +/// USHORT ReservedBits1 : 1; /* bit 15 */ +/// ``` +/// +/// `NoPicReorderingFlag`/`NoBiPredFlag` are hardware hints, not stream facts, and +/// stay 0 — the same choice libavcodec's DXVA HEVC path makes. Claiming them would +/// let a driver take a shortcut this decoder cannot guarantee is safe for a stream +/// whose SPS it re-reads per AU. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct HevcFormatFlags { + pub chroma_format_idc: u8, + pub separate_colour_plane_flag: bool, + pub bit_depth_luma_minus8: u8, + pub bit_depth_chroma_minus8: u8, + pub log2_max_pic_order_cnt_lsb_minus4: u8, +} + +impl HevcFormatFlags { + /// Pack to the `wFormatAndSequenceInfoFlags` word. Widths are masked for the + /// same reason as [`H264BitFields::pack`]: spec-bounded inputs, no panic path. + pub const fn pack(self) -> u16 { + (self.chroma_format_idc as u16 & 0x3) + | ((self.separate_colour_plane_flag as u16) << 2) + | ((self.bit_depth_luma_minus8 as u16 & 0x7) << 3) + | ((self.bit_depth_chroma_minus8 as u16 & 0x7) << 6) + | ((self.log2_max_pic_order_cnt_lsb_minus4 as u16 & 0xF) << 9) + } +} + +/// [`PicParamsHevc::dwCodingParamToolFlags`], as a builder. +/// +/// ```c +/// UINT32 scaling_list_enabled_flag : 1; /* bit 0 */ +/// UINT32 amp_enabled_flag : 1; /* bit 1 */ +/// UINT32 sample_adaptive_offset_enabled_flag : 1; /* bit 2 */ +/// UINT32 pcm_enabled_flag : 1; /* bit 3 */ +/// UINT32 pcm_sample_bit_depth_luma_minus1 : 4; /* bits 4-7 */ +/// UINT32 pcm_sample_bit_depth_chroma_minus1 : 4; /* bits 8-11 */ +/// UINT32 log2_min_pcm_luma_coding_block_size_minus3 : 2; /* bits 12-13 */ +/// UINT32 log2_diff_max_min_pcm_luma_coding_block_size : 2; /* bits 14-15 */ +/// UINT32 pcm_loop_filter_disabled_flag : 1; /* bit 16 */ +/// UINT32 long_term_ref_pics_present_flag : 1; /* bit 17 */ +/// UINT32 sps_temporal_mvp_enabled_flag : 1; /* bit 18 */ +/// UINT32 strong_intra_smoothing_enabled_flag : 1; /* bit 19 */ +/// UINT32 dependent_slice_segments_enabled_flag : 1; /* bit 20 */ +/// UINT32 output_flag_present_flag : 1; /* bit 21 */ +/// UINT32 num_extra_slice_header_bits : 3; /* bits 22-24 */ +/// UINT32 sign_data_hiding_enabled_flag : 1; /* bit 25 */ +/// UINT32 cabac_init_present_flag : 1; /* bit 26 */ +/// UINT32 ReservedBits3 : 5; /* bits 27-31 */ +/// ``` +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct HevcToolFlags { + pub scaling_list_enabled_flag: bool, + pub amp_enabled_flag: bool, + pub sample_adaptive_offset_enabled_flag: bool, + pub pcm_enabled_flag: bool, + pub pcm_sample_bit_depth_luma_minus1: u8, + pub pcm_sample_bit_depth_chroma_minus1: u8, + pub log2_min_pcm_luma_coding_block_size_minus3: u8, + pub log2_diff_max_min_pcm_luma_coding_block_size: u8, + pub pcm_loop_filter_disabled_flag: bool, + pub long_term_ref_pics_present_flag: bool, + pub sps_temporal_mvp_enabled_flag: bool, + pub strong_intra_smoothing_enabled_flag: bool, + pub dependent_slice_segments_enabled_flag: bool, + pub output_flag_present_flag: bool, + pub num_extra_slice_header_bits: u8, + pub sign_data_hiding_enabled_flag: bool, + pub cabac_init_present_flag: bool, +} + +impl HevcToolFlags { + /// Pack to the `dwCodingParamToolFlags` word. + pub const fn pack(self) -> u32 { + (self.scaling_list_enabled_flag as u32) + | ((self.amp_enabled_flag as u32) << 1) + | ((self.sample_adaptive_offset_enabled_flag as u32) << 2) + | ((self.pcm_enabled_flag as u32) << 3) + | ((self.pcm_sample_bit_depth_luma_minus1 as u32 & 0xF) << 4) + | ((self.pcm_sample_bit_depth_chroma_minus1 as u32 & 0xF) << 8) + | ((self.log2_min_pcm_luma_coding_block_size_minus3 as u32 & 0x3) << 12) + | ((self.log2_diff_max_min_pcm_luma_coding_block_size as u32 & 0x3) << 14) + | ((self.pcm_loop_filter_disabled_flag as u32) << 16) + | ((self.long_term_ref_pics_present_flag as u32) << 17) + | ((self.sps_temporal_mvp_enabled_flag as u32) << 18) + | ((self.strong_intra_smoothing_enabled_flag as u32) << 19) + | ((self.dependent_slice_segments_enabled_flag as u32) << 20) + | ((self.output_flag_present_flag as u32) << 21) + | ((self.num_extra_slice_header_bits as u32 & 0x7) << 22) + | ((self.sign_data_hiding_enabled_flag as u32) << 25) + | ((self.cabac_init_present_flag as u32) << 26) + } +} + +/// [`PicParamsHevc::dwCodingSettingPicturePropertyFlags`], as a builder. +/// +/// ```c +/// UINT32 constrained_intra_pred_flag : 1; /* bit 0 */ +/// UINT32 transform_skip_enabled_flag : 1; /* bit 1 */ +/// UINT32 cu_qp_delta_enabled_flag : 1; /* bit 2 */ +/// UINT32 pps_slice_chroma_qp_offsets_present_flag : 1; /* bit 3 */ +/// UINT32 weighted_pred_flag : 1; /* bit 4 */ +/// UINT32 weighted_bipred_flag : 1; /* bit 5 */ +/// UINT32 transquant_bypass_enabled_flag : 1; /* bit 6 */ +/// UINT32 tiles_enabled_flag : 1; /* bit 7 */ +/// UINT32 entropy_coding_sync_enabled_flag : 1; /* bit 8 */ +/// UINT32 uniform_spacing_flag : 1; /* bit 9 */ +/// UINT32 loop_filter_across_tiles_enabled_flag : 1; /* bit 10 */ +/// UINT32 pps_loop_filter_across_slices_enabled_flag : 1; /* bit 11 */ +/// UINT32 deblocking_filter_override_enabled_flag : 1; /* bit 12 */ +/// UINT32 pps_deblocking_filter_disabled_flag : 1; /* bit 13 */ +/// UINT32 lists_modification_present_flag : 1; /* bit 14 */ +/// UINT32 slice_segment_header_extension_present_flag : 1; /* bit 15 */ +/// UINT32 IrapPicFlag : 1; /* bit 16 */ +/// UINT32 IdrPicFlag : 1; /* bit 17 */ +/// UINT32 IntraPicFlag : 1; /* bit 18 */ +/// UINT32 ReservedBits4 : 13; /* bits 19-31 */ +/// ``` +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct HevcPictureFlags { + pub constrained_intra_pred_flag: bool, + pub transform_skip_enabled_flag: bool, + pub cu_qp_delta_enabled_flag: bool, + pub pps_slice_chroma_qp_offsets_present_flag: bool, + pub weighted_pred_flag: bool, + pub weighted_bipred_flag: bool, + pub transquant_bypass_enabled_flag: bool, + pub tiles_enabled_flag: bool, + pub entropy_coding_sync_enabled_flag: bool, + pub uniform_spacing_flag: bool, + pub loop_filter_across_tiles_enabled_flag: bool, + pub pps_loop_filter_across_slices_enabled_flag: bool, + pub deblocking_filter_override_enabled_flag: bool, + pub pps_deblocking_filter_disabled_flag: bool, + pub lists_modification_present_flag: bool, + pub slice_segment_header_extension_present_flag: bool, + pub irap_pic_flag: bool, + pub idr_pic_flag: bool, + pub intra_pic_flag: bool, +} + +impl HevcPictureFlags { + /// Pack to the `dwCodingSettingPicturePropertyFlags` word. + pub const fn pack(self) -> u32 { + (self.constrained_intra_pred_flag as u32) + | ((self.transform_skip_enabled_flag as u32) << 1) + | ((self.cu_qp_delta_enabled_flag as u32) << 2) + | ((self.pps_slice_chroma_qp_offsets_present_flag as u32) << 3) + | ((self.weighted_pred_flag as u32) << 4) + | ((self.weighted_bipred_flag as u32) << 5) + | ((self.transquant_bypass_enabled_flag as u32) << 6) + | ((self.tiles_enabled_flag as u32) << 7) + | ((self.entropy_coding_sync_enabled_flag as u32) << 8) + | ((self.uniform_spacing_flag as u32) << 9) + | ((self.loop_filter_across_tiles_enabled_flag as u32) << 10) + | ((self.pps_loop_filter_across_slices_enabled_flag as u32) << 11) + | ((self.deblocking_filter_override_enabled_flag as u32) << 12) + | ((self.pps_deblocking_filter_disabled_flag as u32) << 13) + | ((self.lists_modification_present_flag as u32) << 14) + | ((self.slice_segment_header_extension_present_flag as u32) << 15) + | ((self.irap_pic_flag as u32) << 16) + | ((self.idr_pic_flag as u32) << 17) + | ((self.intra_pic_flag as u32) << 18) + } +} + +/// `DXVA_Qmatrix_HEVC` — the HEVC inverse-quantization matrix buffer. +/// +/// ```c +/// typedef struct _DXVA_Qmatrix_HEVC { +/// UCHAR ucScalingLists0[6][16]; /* 0 */ +/// UCHAR ucScalingLists1[6][64]; /* 96 */ +/// UCHAR ucScalingLists2[6][64]; /* 480 */ +/// UCHAR ucScalingLists3[2][64]; /* 864 */ +/// UCHAR ucScalingListDCCoefSizeID2[6]; /* 992 */ +/// UCHAR ucScalingListDCCoefSizeID3[2]; /* 998 */ +/// } DXVA_Qmatrix_HEVC; /* 1000 bytes */ +/// ``` +/// +/// `ucScalingLists{0,1,2,3}` are sizeIds 0..3 (4x4, 8x8, 16x16, 32x32), each +/// `[matrixId][coefficient]` in coded (diagonal-scan) order. sizeId 3 has only two +/// matrices — HEVC codes matrixId 0 and 3 for 32x32 — so `ucScalingLists3[k]` is +/// the parser's `scaling_list_32x32[k * 3]`, and likewise for its DC coefficients. +/// The DC entries are `scaling_list_dc_coef_minus8 + 8`, i.e. the ScalingFactor DC +/// value itself, not the coded delta. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct QmatrixHevc { + pub ucScalingLists0: [[u8; 16]; 6], + pub ucScalingLists1: [[u8; 64]; 6], + pub ucScalingLists2: [[u8; 64]; 6], + pub ucScalingLists3: [[u8; 64]; 2], + pub ucScalingListDCCoefSizeID2: [u8; 6], + pub ucScalingListDCCoefSizeID3: [u8; 2], +} + +impl QmatrixHevc { + /// An all-zero quantization-matrix buffer. + pub const fn zeroed() -> QmatrixHevc { + QmatrixHevc { + ucScalingLists0: [[0; 16]; 6], + ucScalingLists1: [[0; 64]; 6], + ucScalingLists2: [[0; 64]; 6], + ucScalingLists3: [[0; 64]; 2], + ucScalingListDCCoefSizeID2: [0; 6], + ucScalingListDCCoefSizeID3: [0; 2], + } + } +} + +/// `DXVA_Slice_HEVC_Short` — one entry of the HEVC slice-control buffer. Byte-for +/// byte the H.264 short record; declared separately because the two specs define +/// them separately and a future spec revision is free to diverge. +/// +/// ```c +/// typedef struct _DXVA_Slice_HEVC_Short { +/// UINT BSNALunitDataLocation; /* 0 */ +/// UINT SliceBytesInBuffer; /* 4 */ +/// USHORT wBadSliceChopping; /* 8 */ +/// } DXVA_Slice_HEVC_Short; /* 10 bytes — PACKED, no tail padding */ +/// ``` +/// +/// Ten bytes for the same reason as [`SliceH264Short`], and measured independently: +/// the HEVC capture's slice-control `DataSize` is 10 on a vector with exactly one +/// slice segment per picture. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[repr(C, packed)] +pub struct SliceHevcShort { + pub BSNALunitDataLocation: u32, + pub SliceBytesInBuffer: u32, + pub wBadSliceChopping: u16, +} + +// --------------------------------------------------------------------------- +// Layout proofs +// --------------------------------------------------------------------------- + +// Sizes and offsets, asserted at COMPILE time against the C declarations +// reproduced above. This is the whole defence against a silently mis-declared +// buffer: nothing else in the pipeline can tell a wrong offset from a right one, +// because the driver accepts either and only the picture differs. +// +// Three kinds of assertion, and the third is new because the first two missed a +// real defect (the slice records' 12-vs-10; see the module docs): +// +// 1. `size_of` per struct, against the C total. +// 2. `offset_of` per FIELD, which is what makes interior padding or a reordering +// impossible. +// 3. **size == last field's offset + last field's own size**, per struct — the +// assertion that catches TAIL padding, which is exactly what (1) and (2) cannot +// see: a struct whose declared total is itself wrong satisfies both. Every one of +// these buffers is a wire format with no padding anywhere, and this is where that +// is stated as a proof rather than a comment. +const _: () = { + assert!(size_of::() == 1); + + assert!(size_of::() == 1040); + assert!(offset_of!(PicParamsH264, wFrameWidthInMbsMinus1) == 0); + assert!(offset_of!(PicParamsH264, wFrameHeightInMbsMinus1) == 2); + assert!(offset_of!(PicParamsH264, CurrPic) == 4); + assert!(offset_of!(PicParamsH264, num_ref_frames) == 5); + assert!(offset_of!(PicParamsH264, wBitFields) == 6); + assert!(offset_of!(PicParamsH264, bit_depth_luma_minus8) == 8); + assert!(offset_of!(PicParamsH264, bit_depth_chroma_minus8) == 9); + assert!(offset_of!(PicParamsH264, Reserved16Bits) == 10); + assert!(offset_of!(PicParamsH264, StatusReportFeedbackNumber) == 12); + assert!(offset_of!(PicParamsH264, RefFrameList) == 16); + assert!(offset_of!(PicParamsH264, CurrFieldOrderCnt) == 32); + assert!(offset_of!(PicParamsH264, FieldOrderCntList) == 40); + assert!(offset_of!(PicParamsH264, pic_init_qs_minus26) == 168); + assert!(offset_of!(PicParamsH264, chroma_qp_index_offset) == 169); + assert!(offset_of!(PicParamsH264, second_chroma_qp_index_offset) == 170); + assert!(offset_of!(PicParamsH264, ContinuationFlag) == 171); + assert!(offset_of!(PicParamsH264, pic_init_qp_minus26) == 172); + assert!(offset_of!(PicParamsH264, num_ref_idx_l0_active_minus1) == 173); + assert!(offset_of!(PicParamsH264, num_ref_idx_l1_active_minus1) == 174); + assert!(offset_of!(PicParamsH264, Reserved8BitsA) == 175); + assert!(offset_of!(PicParamsH264, FrameNumList) == 176); + assert!(offset_of!(PicParamsH264, UsedForReferenceFlags) == 208); + assert!(offset_of!(PicParamsH264, NonExistingFrameFlags) == 212); + assert!(offset_of!(PicParamsH264, frame_num) == 214); + assert!(offset_of!(PicParamsH264, log2_max_frame_num_minus4) == 216); + assert!(offset_of!(PicParamsH264, pic_order_cnt_type) == 217); + assert!(offset_of!(PicParamsH264, log2_max_pic_order_cnt_lsb_minus4) == 218); + assert!(offset_of!(PicParamsH264, delta_pic_order_always_zero_flag) == 219); + assert!(offset_of!(PicParamsH264, direct_8x8_inference_flag) == 220); + assert!(offset_of!(PicParamsH264, entropy_coding_mode_flag) == 221); + assert!(offset_of!(PicParamsH264, pic_order_present_flag) == 222); + assert!(offset_of!(PicParamsH264, num_slice_groups_minus1) == 223); + assert!(offset_of!(PicParamsH264, slice_group_map_type) == 224); + assert!(offset_of!(PicParamsH264, deblocking_filter_control_present_flag) == 225); + assert!(offset_of!(PicParamsH264, redundant_pic_cnt_present_flag) == 226); + assert!(offset_of!(PicParamsH264, Reserved8BitsB) == 227); + assert!(offset_of!(PicParamsH264, slice_group_change_rate_minus1) == 228); + assert!(offset_of!(PicParamsH264, SliceGroupMap) == 230); + + assert!(size_of::() == 224); + assert!(offset_of!(QmatrixH264, bScalingLists4x4) == 0); + assert!(offset_of!(QmatrixH264, bScalingLists8x8) == 96); + + assert!(size_of::() == 10); + assert!(align_of::() == 1); + assert!(offset_of!(SliceH264Short, BSNALunitDataLocation) == 0); + assert!(offset_of!(SliceH264Short, SliceBytesInBuffer) == 4); + assert!(offset_of!(SliceH264Short, wBadSliceChopping) == 8); + + assert!(size_of::() == 232); + assert!(offset_of!(PicParamsHevc, PicWidthInMinCbsY) == 0); + assert!(offset_of!(PicParamsHevc, PicHeightInMinCbsY) == 2); + assert!(offset_of!(PicParamsHevc, wFormatAndSequenceInfoFlags) == 4); + assert!(offset_of!(PicParamsHevc, CurrPic) == 6); + assert!(offset_of!(PicParamsHevc, sps_max_dec_pic_buffering_minus1) == 7); + assert!(offset_of!(PicParamsHevc, log2_min_luma_coding_block_size_minus3) == 8); + assert!(offset_of!(PicParamsHevc, log2_diff_max_min_luma_coding_block_size) == 9); + assert!(offset_of!(PicParamsHevc, log2_min_transform_block_size_minus2) == 10); + assert!(offset_of!(PicParamsHevc, log2_diff_max_min_transform_block_size) == 11); + assert!(offset_of!(PicParamsHevc, max_transform_hierarchy_depth_inter) == 12); + assert!(offset_of!(PicParamsHevc, max_transform_hierarchy_depth_intra) == 13); + assert!(offset_of!(PicParamsHevc, num_short_term_ref_pic_sets) == 14); + assert!(offset_of!(PicParamsHevc, num_long_term_ref_pics_sps) == 15); + assert!(offset_of!(PicParamsHevc, num_ref_idx_l0_default_active_minus1) == 16); + assert!(offset_of!(PicParamsHevc, num_ref_idx_l1_default_active_minus1) == 17); + assert!(offset_of!(PicParamsHevc, init_qp_minus26) == 18); + assert!(offset_of!(PicParamsHevc, ucNumDeltaPocsOfRefRpsIdx) == 19); + assert!(offset_of!(PicParamsHevc, wNumBitsForShortTermRPSInSlice) == 20); + assert!(offset_of!(PicParamsHevc, ReservedBits2) == 22); + assert!(offset_of!(PicParamsHevc, dwCodingParamToolFlags) == 24); + assert!(offset_of!(PicParamsHevc, dwCodingSettingPicturePropertyFlags) == 28); + assert!(offset_of!(PicParamsHevc, pps_cb_qp_offset) == 32); + assert!(offset_of!(PicParamsHevc, pps_cr_qp_offset) == 33); + assert!(offset_of!(PicParamsHevc, num_tile_columns_minus1) == 34); + assert!(offset_of!(PicParamsHevc, num_tile_rows_minus1) == 35); + assert!(offset_of!(PicParamsHevc, column_width_minus1) == 36); + assert!(offset_of!(PicParamsHevc, row_height_minus1) == 74); + assert!(offset_of!(PicParamsHevc, diff_cu_qp_delta_depth) == 116); + assert!(offset_of!(PicParamsHevc, pps_beta_offset_div2) == 117); + assert!(offset_of!(PicParamsHevc, pps_tc_offset_div2) == 118); + assert!(offset_of!(PicParamsHevc, log2_parallel_merge_level_minus2) == 119); + assert!(offset_of!(PicParamsHevc, CurrPicOrderCntVal) == 120); + assert!(offset_of!(PicParamsHevc, RefPicList) == 124); + assert!(offset_of!(PicParamsHevc, ReservedBits5) == 139); + assert!(offset_of!(PicParamsHevc, PicOrderCntValList) == 140); + assert!(offset_of!(PicParamsHevc, RefPicSetStCurrBefore) == 200); + assert!(offset_of!(PicParamsHevc, RefPicSetStCurrAfter) == 208); + assert!(offset_of!(PicParamsHevc, RefPicSetLtCurr) == 216); + assert!(offset_of!(PicParamsHevc, ReservedBits6) == 224); + assert!(offset_of!(PicParamsHevc, ReservedBits7) == 226); + assert!(offset_of!(PicParamsHevc, StatusReportFeedbackNumber) == 228); + + assert!(size_of::() == 1000); + assert!(offset_of!(QmatrixHevc, ucScalingLists0) == 0); + assert!(offset_of!(QmatrixHevc, ucScalingLists1) == 96); + assert!(offset_of!(QmatrixHevc, ucScalingLists2) == 480); + assert!(offset_of!(QmatrixHevc, ucScalingLists3) == 864); + assert!(offset_of!(QmatrixHevc, ucScalingListDCCoefSizeID2) == 992); + assert!(offset_of!(QmatrixHevc, ucScalingListDCCoefSizeID3) == 998); + + assert!(size_of::() == 10); + assert!(align_of::() == 1); + assert!(offset_of!(SliceHevcShort, BSNALunitDataLocation) == 0); + assert!(offset_of!(SliceHevcShort, SliceBytesInBuffer) == 4); + assert!(offset_of!(SliceHevcShort, wBadSliceChopping) == 8); + + // NO TAIL PADDING, per struct: the size is the last member's offset plus the + // last member's own size, nothing more. The right-hand sizes are the C + // declarations' (`UCHAR SliceGroupMap[810]`, `UINT StatusReportFeedbackNumber`, + // …), so each line is an independent statement of the total rather than a + // restatement of `size_of`. + assert!(size_of::() == offset_of!(PicParamsH264, SliceGroupMap) + 810); + assert!(size_of::() == offset_of!(QmatrixH264, bScalingLists8x8) + 2 * 64); + assert!(size_of::() == offset_of!(SliceH264Short, wBadSliceChopping) + 2); + assert!( + size_of::() == offset_of!(PicParamsHevc, StatusReportFeedbackNumber) + 4 + ); + assert!(size_of::() == offset_of!(QmatrixHevc, ucScalingListDCCoefSizeID3) + 2); + assert!(size_of::() == offset_of!(SliceHevcShort, wBadSliceChopping) + 2); +}; + +// --------------------------------------------------------------------------- +// Byte view +// --------------------------------------------------------------------------- + +mod sealed { + /// Implemented only by this module's `#[repr(C)]` plain-old-data buffers. + /// Sealed so no downstream type can opt into [`super::as_bytes`]'s unsafe + /// transmute-to-bytes on a type that owns a pointer or has padding it cares + /// about. + pub trait DxvaBuffer: Copy + 'static {} +} + +pub use sealed::DxvaBuffer; + +impl DxvaBuffer for PicParamsH264 {} +impl DxvaBuffer for QmatrixH264 {} +impl DxvaBuffer for SliceH264Short {} +impl DxvaBuffer for PicParamsHevc {} +impl DxvaBuffer for QmatrixHevc {} +impl DxvaBuffer for SliceHevcShort {} + +/// The raw bytes of a DXVA buffer, ready for `memcpy` into the mapping +/// `GetDecoderBuffer` returns. +/// +/// Reading a struct's padding bytes is what makes this unsafe in principle; here +/// it is sound and the values are meaningful, because every implementor is +/// `#[repr(C)]` POD built from a `zeroed()` base — so the tail padding a driver +/// reads is zero rather than uninitialized, which is exactly what the DXVA specs +/// call for in reserved bytes. +pub fn as_bytes(value: &T) -> &[u8] { + // SAFETY: `T: DxvaBuffer` is a sealed trait implemented only for this + // module's `#[repr(C)]` structs, none of which contains a pointer, a + // reference, or any type with a niche or a `Drop`. Their entire + // `size_of::()` byte range — payload and padding alike — is therefore + // initialized memory owned by `value`, and the returned slice borrows it for + // exactly `value`'s lifetime, so nothing can mutate or free it while the + // slice is alive. The alignment requirement is trivially met (the slice is + // `u8`), and `size_of::()` never exceeds `isize::MAX`. + unsafe { std::slice::from_raw_parts((value as *const T).cast::(), size_of::()) } +} + +/// The raw bytes of a slice-control array, laid out exactly as the +/// `D3D11_VIDEO_DECODER_BUFFER_SLICE_CONTROL` buffer wants them: `n` records +/// back to back. +pub fn slice_bytes(values: &[T]) -> &[u8] { + // SAFETY: the same POD argument as `as_bytes`, extended over a slice: the + // elements are contiguous with `size_of::()` stride by the definition of + // a Rust slice, every byte of every element is initialized (POD built from + // `zeroed()`), and the borrow ties the byte view to `values`. The length + // cannot overflow `isize::MAX`: it is the size of a live allocation. + unsafe { + std::slice::from_raw_parts(values.as_ptr().cast::(), std::mem::size_of_val(values)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One "set this member, expect this word" case of a packing table. + type PackCase = (fn(&mut T), u32); + + #[test] + fn a_pic_entry_packs_the_index_into_seven_bits_and_the_flag_into_the_eighth() { + assert_eq!(PicEntry::new(0, false).0, 0x00); + assert_eq!(PicEntry::new(5, false).0, 0x05); + assert_eq!(PicEntry::new(5, true).0, 0x85); + assert_eq!(PicEntry::new(0x7F, true).0, 0xFF); + // Round-trip: the accessors read back exactly what was packed. + let entry = PicEntry::new(17, true); + assert_eq!(entry.index(), 17); + assert!(entry.associated()); + assert_eq!(PicEntry::UNUSED.0, UNUSED_ENTRY); + } + + #[test] + fn the_h264_bitfield_word_packs_each_member_at_its_declared_bit() { + // One member at a time, checked against the bit position written in the + // C declaration — an independent derivation from the packing expression. + let base = H264BitFields::default(); + // MbsConsecutiveFlag is hard-1, so a default word is bit 11 alone. + assert_eq!(base.pack(), 1 << 11); + + let mut f = base; + f.chroma_format_idc = 1; + assert_eq!(f.pack(), (1 << 11) | (1 << 4)); + f.chroma_format_idc = 3; + assert_eq!(f.pack(), (1 << 11) | (3 << 4)); + + let mut f = base; + f.ref_pic_flag = true; + assert_eq!(f.pack(), (1 << 11) | (1 << 6)); + + let mut f = base; + f.constrained_intra_pred_flag = true; + assert_eq!(f.pack(), (1 << 11) | (1 << 7)); + + let mut f = base; + f.weighted_pred_flag = true; + assert_eq!(f.pack(), (1 << 11) | (1 << 8)); + + let mut f = base; + f.weighted_bipred_idc = 2; + assert_eq!(f.pack(), (1 << 11) | (2 << 9)); + + let mut f = base; + f.frame_mbs_only_flag = true; + assert_eq!(f.pack(), (1 << 11) | (1 << 12)); + + let mut f = base; + f.transform_8x8_mode_flag = true; + assert_eq!(f.pack(), (1 << 11) | (1 << 13)); + + let mut f = base; + f.min_luma_bipred_size_8x8 = true; + assert_eq!(f.pack(), (1 << 11) | (1 << 14)); + + let mut f = base; + f.intra_pic_flag = true; + assert_eq!(f.pack(), (1 << 11) | (1 << 15)); + } + + #[test] + fn a_typical_h264_bitfield_word_matches_a_hand_computed_value() { + // 4:2:0, reference picture, CABAC-irrelevant, weighted prediction off, + // progressive, 8x8 transform on, level 4.0, an inter picture: + // bits 4 (chroma=1), 6 (ref), 11 (consecutive), 12 (frame_mbs_only), + // 13 (transform_8x8), 14 (level >= 3.1). + let f = H264BitFields { + chroma_format_idc: 1, + ref_pic_flag: true, + frame_mbs_only_flag: true, + transform_8x8_mode_flag: true, + min_luma_bipred_size_8x8: true, + ..Default::default() + }; + assert_eq!(f.pack(), 0b0111_1000_0101_0000); + } + + #[test] + fn the_hevc_format_word_packs_each_member_at_its_declared_bit() { + let base = HevcFormatFlags::default(); + assert_eq!(base.pack(), 0); + + let mut f = base; + f.chroma_format_idc = 1; + assert_eq!(f.pack(), 1); + + let mut f = base; + f.separate_colour_plane_flag = true; + assert_eq!(f.pack(), 1 << 2); + + // Main 10: luma AND chroma at 2, at bits 3 and 6. + let mut f = base; + f.bit_depth_luma_minus8 = 2; + f.bit_depth_chroma_minus8 = 2; + assert_eq!(f.pack(), (2 << 3) | (2 << 6)); + + let mut f = base; + f.log2_max_pic_order_cnt_lsb_minus4 = 4; + assert_eq!(f.pack(), 4 << 9); + } + + #[test] + fn the_hevc_tool_word_packs_each_member_at_its_declared_bit() { + let base = HevcToolFlags::default(); + assert_eq!(base.pack(), 0); + + let checks: [PackCase; 13] = [ + (|f| f.scaling_list_enabled_flag = true, 1 << 0), + (|f| f.amp_enabled_flag = true, 1 << 1), + (|f| f.sample_adaptive_offset_enabled_flag = true, 1 << 2), + (|f| f.pcm_enabled_flag = true, 1 << 3), + (|f| f.pcm_loop_filter_disabled_flag = true, 1 << 16), + (|f| f.long_term_ref_pics_present_flag = true, 1 << 17), + (|f| f.sps_temporal_mvp_enabled_flag = true, 1 << 18), + (|f| f.strong_intra_smoothing_enabled_flag = true, 1 << 19), + (|f| f.dependent_slice_segments_enabled_flag = true, 1 << 20), + (|f| f.output_flag_present_flag = true, 1 << 21), + (|f| f.sign_data_hiding_enabled_flag = true, 1 << 25), + (|f| f.cabac_init_present_flag = true, 1 << 26), + (|f| f.num_extra_slice_header_bits = 5, 5 << 22), + ]; + for (set, expected) in checks { + let mut f = base; + set(&mut f); + assert_eq!(f.pack(), expected); + } + + // The multi-bit PCM members, at their declared widths. + let mut f = base; + f.pcm_sample_bit_depth_luma_minus1 = 7; + f.pcm_sample_bit_depth_chroma_minus1 = 7; + f.log2_min_pcm_luma_coding_block_size_minus3 = 1; + f.log2_diff_max_min_pcm_luma_coding_block_size = 2; + assert_eq!(f.pack(), (7 << 4) | (7 << 8) | (1 << 12) | (2 << 14)); + } + + #[test] + fn the_hevc_picture_word_packs_each_member_at_its_declared_bit() { + let base = HevcPictureFlags::default(); + assert_eq!(base.pack(), 0); + + let checks: [PackCase; 19] = [ + (|f| f.constrained_intra_pred_flag = true, 1 << 0), + (|f| f.transform_skip_enabled_flag = true, 1 << 1), + (|f| f.cu_qp_delta_enabled_flag = true, 1 << 2), + ( + |f| f.pps_slice_chroma_qp_offsets_present_flag = true, + 1 << 3, + ), + (|f| f.weighted_pred_flag = true, 1 << 4), + (|f| f.weighted_bipred_flag = true, 1 << 5), + (|f| f.transquant_bypass_enabled_flag = true, 1 << 6), + (|f| f.tiles_enabled_flag = true, 1 << 7), + (|f| f.entropy_coding_sync_enabled_flag = true, 1 << 8), + (|f| f.uniform_spacing_flag = true, 1 << 9), + (|f| f.loop_filter_across_tiles_enabled_flag = true, 1 << 10), + ( + |f| f.pps_loop_filter_across_slices_enabled_flag = true, + 1 << 11, + ), + ( + |f| f.deblocking_filter_override_enabled_flag = true, + 1 << 12, + ), + (|f| f.pps_deblocking_filter_disabled_flag = true, 1 << 13), + (|f| f.lists_modification_present_flag = true, 1 << 14), + ( + |f| f.slice_segment_header_extension_present_flag = true, + 1 << 15, + ), + (|f| f.irap_pic_flag = true, 1 << 16), + (|f| f.idr_pic_flag = true, 1 << 17), + (|f| f.intra_pic_flag = true, 1 << 18), + ]; + for (set, expected) in checks { + let mut f = base; + set(&mut f); + assert_eq!(f.pack(), expected); + } + } + + #[test] + fn every_dxva_buffer_has_the_size_its_spec_declares() { + // The const block above already fails the build on a mismatch; this test + // is what makes the numbers show up in a test run — and it is where a + // reader who distrusts a `const _` finds the same claim executable. + assert_eq!(size_of::(), 1040); + assert_eq!(size_of::(), 224); + assert_eq!(size_of::(), 232); + assert_eq!(size_of::(), 1000); + assert_eq!(size_of::(), 1); + // TEN, not twelve. Measured against libavcodec on hardware: the H.264 + // slice-control buffer is 20 bytes for a two-slice picture and the HEVC one + // 10 bytes for a one-slice-segment picture (module docs). A `#[repr(C)]` + // `{u32, u32, u16}` is 12, and every record after the first would then be + // displaced by two bytes per preceding record. + assert_eq!(size_of::(), 10); + assert_eq!(size_of::(), 10); + assert_eq!(align_of::(), 1); + assert_eq!(align_of::(), 1); + } + + #[test] + fn as_bytes_sees_the_struct_at_its_declared_offsets() { + // A byte view is the actual submission format, so read a few fields back + // out of it at their declared offsets — this catches an endianness or + // packing surprise the size assertions alone would miss. + let mut pp = PicParamsH264::zeroed(); + pp.wFrameWidthInMbsMinus1 = 0x0102; + pp.CurrPic = PicEntry::new(3, false); + pp.StatusReportFeedbackNumber = 0x0A0B_0C0D; + pp.frame_num = 0x1234; + let bytes = as_bytes(&pp); + assert_eq!(bytes.len(), 1040); + assert_eq!(&bytes[0..2], &0x0102u16.to_le_bytes()); + assert_eq!(bytes[4], 3); + assert_eq!(&bytes[12..16], &0x0A0B_0C0Du32.to_le_bytes()); + assert_eq!(&bytes[214..216], &0x1234u16.to_le_bytes()); + // Everything the writer did not touch is a real zero, which is what the + // reserved fields of both specs require. + assert!(bytes[230..1040].iter().all(|&b| b == 0)); + } + + #[test] + fn slice_bytes_lays_records_out_back_to_back_with_no_gap() { + let records = [ + SliceH264Short { + BSNALunitDataLocation: 0, + SliceBytesInBuffer: 100, + wBadSliceChopping: 0, + }, + SliceH264Short { + BSNALunitDataLocation: 100, + SliceBytesInBuffer: 250, + wBadSliceChopping: 0, + }, + ]; + let bytes = slice_bytes(&records); + // TEN bytes per record, so the second record starts at byte 10 — this is the + // test that fails if the packing is ever relaxed back to natural alignment, + // and it fails on the SECOND record, which is exactly where the driver + // would have started misreading. + assert_eq!(bytes.len(), 20); + assert_eq!(&bytes[0..4], &0u32.to_le_bytes()); + assert_eq!(&bytes[4..8], &100u32.to_le_bytes()); + assert_eq!(&bytes[8..10], &0u16.to_le_bytes()); + assert_eq!(&bytes[10..14], &100u32.to_le_bytes()); + assert_eq!(&bytes[14..18], &250u32.to_le_bytes()); + assert_eq!(&bytes[18..20], &0u16.to_le_bytes()); + } +} diff --git a/crates/pf-dxvadec/src/dxva_av1.rs b/crates/pf-dxvadec/src/dxva_av1.rs new file mode 100644 index 00000000..d8ff8732 --- /dev/null +++ b/crates/pf-dxvadec/src/dxva_av1.rs @@ -0,0 +1,837 @@ +//! The DXVA **AV1** buffer layouts, hand-declared — M7's Windows half. +//! +//! Same contract and the same hazards as [`crate::dxva`]: windows-rs generates +//! nothing from `dxva.h`, nothing here is type-checked against Windows, and a field +//! at the wrong offset is a driver reading a reference index where a quantiser +//! should be. +//! +//! # This one was MEASURED against Microsoft's own header +//! +//! The H.264 and HEVC layouts were checked against libavcodec's DXVA byte capture — +//! a mirror, and the best available at the time. AV1 does better: `DXVA_PicParams_AV1` +//! ships in the **Windows SDK's own `dxva.h`** (`10.0.26100.0` and `10.0.28000.0` on +//! the .173 box), which is the declaration the DRIVER was compiled against. So every +//! size, every offset and every bit position below came out of +//! `layout-probe-av1.c` compiled with MSVC against that header, and each is pinned as +//! a compile-time assertion. Re-run the probe (its own header carries the one-liner) +//! if the SDK ever moves. +//! +//! Measured: `DXVA_PicParams_AV1` is **912 bytes, alignment 1** — `dxva.h` packs +//! every one of these to a byte boundary, which is why the structs below carry +//! `#[repr(C, packed)]` and not a plain `#[repr(C)]`. +//! +//! # Two things AV1 puts somewhere unexpected +//! +//! **Global motion is per REFERENCE, inside the picture entry.** `DXVA_PicEntry_AV1` +//! is 36 bytes and carries `wmmat[6]` plus the warp type for that reference — where +//! Vulkan hangs one `StdVideoAV1GlobalMotion` block off the picture info. Same data, +//! a different owner, and a conversion that assumed the Vulkan shape would leave +//! every warped reference at identity. +//! +//! **CDEF strengths are packed two-to-a-byte.** `y_strengths[i]` and +//! `uv_strengths[i]` are single bytes holding `primary` in the low six bits and +//! `secondary` in the top two — not the separate arrays the AV1 syntax (and Vulkan's +//! Std block) use. + +/// `DXVA_PicEntry_AV1` — 36 bytes, and it carries global motion (module docs). +/// +/// ```c +/// typedef struct _DXVA_PicEntry_AV1 { +/// UINT width; +/// UINT height; +/// INT wmmat[6]; // global motion parameters +/// union { struct { UCHAR wminvalid:1; UCHAR wmtype:2; UCHAR Reserved:5; }; +/// UCHAR GlobalMotionFlags; }; +/// UCHAR Index; +/// UINT16 Reserved16Bits; +/// } DXVA_PicEntry_AV1; +/// ``` +#[repr(C, packed)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PicEntryAv1 { + /// The REFERENCE's own `UpscaledWidth` — not the current frame's. AV1 lets + /// every frame pick its own size, and this pair is what lets the driver scale + /// motion out of a differently-sized reference (libavcodec: + /// `pp->frame_refs[i].width = ref_frame->width`). + pub width: u32, + /// The reference's own `FrameHeight`, on the same terms as [`Self::width`]. + pub height: u32, + pub wmmat: [i32; 6], + pub global_motion_flags: u8, + /// ⚠⚠ The AV1 reference **SLOT** — `ref_frame_idx[i]`, 0..8 — or + /// [`UNUSED_INDEX`] where this reference is not present. **Not a surface + /// index.** + /// + /// This is a subscript INTO [`PicParamsAv1::ref_frame_map_texture_index`], + /// which is the array that names surfaces; the driver dereferences one through + /// the other. libavcodec writes `pp->frame_refs[i].Index = ref_frame ? ref_idx + /// : 0xFF` with `ref_idx = frame_header->ref_frame_idx[i]`, and Chromium's + /// `d3d11_av1_accelerator.cc` writes the same thing. + /// + /// Putting a surface index here is not a refusal: on a stream where reference + /// `i` happens to live in the slot whose number equals its surface it decodes + /// correctly, and everywhere else it predicts from whichever picture the + /// reference store holds at the surface's number. + pub index: u8, + pub reserved16: u16, +} + +/// What `DXVA_PicEntry_AV1::Index` (and `RefFrameMapTextureIndex`) carry for a +/// reference that is not present. `0xFF` is DXVA's universal "no surface". +pub const UNUSED_INDEX: u8 = 0xFF; + +impl PicEntryAv1 { + pub const fn zeroed() -> PicEntryAv1 { + PicEntryAv1 { + width: 0, + height: 0, + wmmat: [0; 6], + global_motion_flags: 0, + index: UNUSED_INDEX, + reserved16: 0, + } + } +} + +/// `GlobalMotionFlags`'s members, packed by [`GlobalMotionFlags::pack`]. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct GlobalMotionFlags { + pub wminvalid: bool, + /// `wmtype`: 0 identity, 1 translation, 2 rotzoom, 3 affine. Two bits. + pub wmtype: u8, +} + +impl GlobalMotionFlags { + pub const fn pack(self) -> u8 { + (self.wminvalid as u8) | ((self.wmtype & 0x3) << 1) + } +} + +/// The tile block inside [`PicParamsAv1`]. Declared as its own type so the +/// 64-entry arrays are named once. +#[repr(C, packed)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TilesAv1 { + pub cols: u8, + pub rows: u8, + pub context_update_id: u16, + pub widths: [u16; 64], + pub heights: [u16; 64], +} + +impl TilesAv1 { + pub const fn zeroed() -> TilesAv1 { + TilesAv1 { + cols: 0, + rows: 0, + context_update_id: 0, + widths: [0; 64], + heights: [0; 64], + } + } +} + +/// The loop-filter block. +#[repr(C, packed)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct LoopFilterAv1 { + pub filter_level: [u8; 2], + pub filter_level_u: u8, + pub filter_level_v: u8, + pub sharpness_level: u8, + pub control_flags: u8, + pub ref_deltas: [i8; 8], + pub mode_deltas: [i8; 2], + pub delta_lf_res: u8, + pub frame_restoration_type: [u8; 3], + pub log2_restoration_unit_size: [u16; 3], + pub reserved16: u16, +} + +impl LoopFilterAv1 { + pub const fn zeroed() -> LoopFilterAv1 { + LoopFilterAv1 { + filter_level: [0; 2], + filter_level_u: 0, + filter_level_v: 0, + sharpness_level: 0, + control_flags: 0, + ref_deltas: [0; 8], + mode_deltas: [0; 2], + delta_lf_res: 0, + frame_restoration_type: [0; 3], + log2_restoration_unit_size: [0; 3], + reserved16: 0, + } + } +} + +/// `loop_filter.ControlFlags`' members. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct LoopFilterFlagsAv1 { + pub mode_ref_delta_enabled: bool, + pub mode_ref_delta_update: bool, + pub delta_lf_multi: bool, + pub delta_lf_present: bool, +} + +impl LoopFilterFlagsAv1 { + pub const fn pack(self) -> u8 { + (self.mode_ref_delta_enabled as u8) + | ((self.mode_ref_delta_update as u8) << 1) + | ((self.delta_lf_multi as u8) << 2) + | ((self.delta_lf_present as u8) << 3) + } +} + +/// The quantisation block. +#[repr(C, packed)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct QuantizationAv1 { + pub control_flags: u8, + pub base_qindex: u8, + pub y_dc_delta_q: i8, + pub u_dc_delta_q: i8, + pub v_dc_delta_q: i8, + pub u_ac_delta_q: i8, + pub v_ac_delta_q: i8, + pub qm_y: u8, + pub qm_u: u8, + pub qm_v: u8, + pub reserved16: u16, +} + +impl QuantizationAv1 { + pub const fn zeroed() -> QuantizationAv1 { + QuantizationAv1 { + control_flags: 0, + base_qindex: 0, + y_dc_delta_q: 0, + u_dc_delta_q: 0, + v_dc_delta_q: 0, + u_ac_delta_q: 0, + v_ac_delta_q: 0, + qm_y: 0, + qm_u: 0, + qm_v: 0, + reserved16: 0, + } + } +} + +/// `quantization.ControlFlags`' members. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct QuantizationFlagsAv1 { + pub delta_q_present: bool, + /// Two bits. + pub delta_q_res: u8, +} + +impl QuantizationFlagsAv1 { + pub const fn pack(self) -> u8 { + (self.delta_q_present as u8) | ((self.delta_q_res & 0x3) << 1) + } +} + +/// The CDEF block. ⚠ Its strengths are packed two fields to a byte — see +/// [`CdefStrength`] and the module docs. +#[repr(C, packed)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CdefAv1 { + pub control_flags: u8, + pub y_strengths: [u8; 8], + pub uv_strengths: [u8; 8], +} + +impl CdefAv1 { + pub const fn zeroed() -> CdefAv1 { + CdefAv1 { + control_flags: 0, + y_strengths: [0; 8], + uv_strengths: [0; 8], + } + } +} + +/// `cdef.ControlFlags`' members: `damping` in bits 0-1, `bits` in 2-3. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct CdefFlagsAv1 { + /// `cdef_damping_minus_3`, two bits. + pub damping: u8, + /// `cdef_bits`, two bits. + pub bits: u8, +} + +impl CdefFlagsAv1 { + pub const fn pack(self) -> u8 { + (self.damping & 0x3) | ((self.bits & 0x3) << 2) + } +} + +/// One packed CDEF strength byte: `primary` in the low SIX bits, `secondary` in +/// the top two. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct CdefStrength { + pub primary: u8, + pub secondary: u8, +} + +impl CdefStrength { + pub const fn pack(self) -> u8 { + (self.primary & 0x3F) | ((self.secondary & 0x3) << 6) + } +} + +/// The segmentation block. +#[repr(C, packed)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SegmentationAv1 { + pub control_flags: u8, + pub reserved24: [u8; 3], + /// One packed mask per segment — see [`SegmentFeatureMask`]. + pub feature_mask: [u8; 8], + pub feature_data: [[i16; 8]; 8], +} + +impl SegmentationAv1 { + pub const fn zeroed() -> SegmentationAv1 { + SegmentationAv1 { + control_flags: 0, + reserved24: [0; 3], + feature_mask: [0; 8], + feature_data: [[0; 8]; 8], + } + } +} + +/// `segmentation.ControlFlags`' members. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct SegmentationFlagsAv1 { + pub enabled: bool, + pub update_map: bool, + pub update_data: bool, + pub temporal_update: bool, +} + +impl SegmentationFlagsAv1 { + pub const fn pack(self) -> u8 { + (self.enabled as u8) + | ((self.update_map as u8) << 1) + | ((self.update_data as u8) << 2) + | ((self.temporal_update as u8) << 3) + } +} + +/// One segment's feature mask. The bit ORDER is the AV1 `SEG_LVL_*` order, which +/// is also the order the parser's `feature_enabled[segment]` is indexed by — so a +/// conversion can shift by feature index directly. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct SegmentFeatureMask { + pub alt_q: bool, + pub alt_lf_y_v: bool, + pub alt_lf_y_h: bool, + pub alt_lf_u: bool, + pub alt_lf_v: bool, + pub ref_frame: bool, + pub skip: bool, + pub globalmv: bool, +} + +impl SegmentFeatureMask { + pub const fn pack(self) -> u8 { + (self.alt_q as u8) + | ((self.alt_lf_y_v as u8) << 1) + | ((self.alt_lf_y_h as u8) << 2) + | ((self.alt_lf_u as u8) << 3) + | ((self.alt_lf_v as u8) << 4) + | ((self.ref_frame as u8) << 5) + | ((self.skip as u8) << 6) + | ((self.globalmv as u8) << 7) + } +} + +/// The film-grain block. ⚠ Its scaling points are `[value, scaling]` PAIRS, where +/// AV1's syntax and Vulkan's Std block keep two parallel arrays. +#[repr(C, packed)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FilmGrainAv1 { + pub control_flags: u16, + pub grain_seed: u16, + pub scaling_points_y: [[u8; 2]; 14], + pub num_y_points: u8, + pub scaling_points_cb: [[u8; 2]; 10], + pub num_cb_points: u8, + pub scaling_points_cr: [[u8; 2]; 10], + pub num_cr_points: u8, + pub ar_coeffs_y: [u8; 24], + pub ar_coeffs_cb: [u8; 25], + pub ar_coeffs_cr: [u8; 25], + pub cb_mult: u8, + pub cb_luma_mult: u8, + pub cr_mult: u8, + pub cr_luma_mult: u8, + pub reserved8: u8, + pub cb_offset: i16, + pub cr_offset: i16, +} + +impl FilmGrainAv1 { + pub const fn zeroed() -> FilmGrainAv1 { + FilmGrainAv1 { + control_flags: 0, + grain_seed: 0, + scaling_points_y: [[0; 2]; 14], + num_y_points: 0, + scaling_points_cb: [[0; 2]; 10], + num_cb_points: 0, + scaling_points_cr: [[0; 2]; 10], + num_cr_points: 0, + ar_coeffs_y: [0; 24], + ar_coeffs_cb: [0; 25], + ar_coeffs_cr: [0; 25], + cb_mult: 0, + cb_luma_mult: 0, + cr_mult: 0, + cr_luma_mult: 0, + reserved8: 0, + cb_offset: 0, + cr_offset: 0, + } + } +} + +/// `film_grain.ControlFlags`' members — a SIXTEEN-bit word, unlike every other +/// control word here. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct FilmGrainFlagsAv1 { + pub apply_grain: bool, + /// Two bits. + pub scaling_shift_minus8: u8, + pub chroma_scaling_from_luma: bool, + /// Two bits. + pub ar_coeff_lag: u8, + /// Two bits. + pub ar_coeff_shift_minus6: u8, + /// Two bits. + pub grain_scale_shift: u8, + pub overlap_flag: bool, + pub clip_to_restricted_range: bool, + pub matrix_coeff_is_identity: bool, +} + +impl FilmGrainFlagsAv1 { + pub const fn pack(self) -> u16 { + (self.apply_grain as u16) + | (((self.scaling_shift_minus8 & 0x3) as u16) << 1) + | ((self.chroma_scaling_from_luma as u16) << 3) + | (((self.ar_coeff_lag & 0x3) as u16) << 4) + | (((self.ar_coeff_shift_minus6 & 0x3) as u16) << 6) + | (((self.grain_scale_shift & 0x3) as u16) << 8) + | ((self.overlap_flag as u16) << 10) + | ((self.clip_to_restricted_range as u16) << 11) + | ((self.matrix_coeff_is_identity as u16) << 12) + } +} + +/// `DXVA_PicParams_AV1` — 912 bytes, packed (module docs). +#[repr(C, packed)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PicParamsAv1 { + pub width: u32, + pub height: u32, + pub max_width: u32, + pub max_height: u32, + pub curr_pic_texture_index: u8, + pub superres_denom: u8, + pub bitdepth: u8, + pub seq_profile: u8, + pub tiles: TilesAv1, + /// `CodingParamToolFlags` — see [`CodingFlagsAv1`]. + pub coding: u32, + /// `FormatAndPictureInfoFlags` — see [`FormatFlagsAv1`]. + pub format: u8, + pub primary_ref_frame: u8, + pub order_hint: u8, + pub order_hint_bits: u8, + /// The seven reference NAMES (`LAST`..`ALTREF`), each with its own global + /// motion (module docs). + pub frame_refs: [PicEntryAv1; 7], + /// The eight reference SLOTS, as surface indices — DXVA's statement about the + /// whole reference store, the counterpart of `RefFrameList` on the other + /// codecs. [`UNUSED_INDEX`] for an empty slot. + pub ref_frame_map_texture_index: [u8; 8], + pub loop_filter: LoopFilterAv1, + pub quantization: QuantizationAv1, + pub cdef: CdefAv1, + pub interp_filter: u8, + pub segmentation: SegmentationAv1, + pub film_grain: FilmGrainAv1, + pub reserved32: u32, + pub status_report_feedback_number: u32, +} + +impl PicParamsAv1 { + pub const fn zeroed() -> PicParamsAv1 { + PicParamsAv1 { + width: 0, + height: 0, + max_width: 0, + max_height: 0, + curr_pic_texture_index: 0, + superres_denom: 0, + bitdepth: 0, + seq_profile: 0, + tiles: TilesAv1::zeroed(), + coding: 0, + format: 0, + primary_ref_frame: 0, + order_hint: 0, + order_hint_bits: 0, + frame_refs: [PicEntryAv1::zeroed(); 7], + ref_frame_map_texture_index: [UNUSED_INDEX; 8], + loop_filter: LoopFilterAv1::zeroed(), + quantization: QuantizationAv1::zeroed(), + cdef: CdefAv1::zeroed(), + interp_filter: 0, + segmentation: SegmentationAv1::zeroed(), + film_grain: FilmGrainAv1::zeroed(), + reserved32: 0, + status_report_feedback_number: 0, + } + } +} + +/// `CodingParamToolFlags`' members, in declaration order. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct CodingFlagsAv1 { + pub use_128x128_superblock: bool, + pub intra_edge_filter: bool, + pub interintra_compound: bool, + pub masked_compound: bool, + pub warped_motion: bool, + pub dual_filter: bool, + pub jnt_comp: bool, + pub screen_content_tools: bool, + pub integer_mv: bool, + pub cdef: bool, + pub restoration: bool, + pub film_grain: bool, + pub intrabc: bool, + pub high_precision_mv: bool, + pub switchable_motion_mode: bool, + pub filter_intra: bool, + pub disable_frame_end_update_cdf: bool, + pub disable_cdf_update: bool, + pub reference_mode: bool, + pub skip_mode: bool, + pub reduced_tx_set: bool, + pub superres: bool, + /// Two bits. + pub tx_mode: u8, + pub use_ref_frame_mvs: bool, + pub enable_ref_frame_mvs: bool, + pub reference_frame_update: bool, +} + +impl CodingFlagsAv1 { + pub const fn pack(self) -> u32 { + (self.use_128x128_superblock as u32) + | ((self.intra_edge_filter as u32) << 1) + | ((self.interintra_compound as u32) << 2) + | ((self.masked_compound as u32) << 3) + | ((self.warped_motion as u32) << 4) + | ((self.dual_filter as u32) << 5) + | ((self.jnt_comp as u32) << 6) + | ((self.screen_content_tools as u32) << 7) + | ((self.integer_mv as u32) << 8) + | ((self.cdef as u32) << 9) + | ((self.restoration as u32) << 10) + | ((self.film_grain as u32) << 11) + | ((self.intrabc as u32) << 12) + | ((self.high_precision_mv as u32) << 13) + | ((self.switchable_motion_mode as u32) << 14) + | ((self.filter_intra as u32) << 15) + | ((self.disable_frame_end_update_cdf as u32) << 16) + | ((self.disable_cdf_update as u32) << 17) + | ((self.reference_mode as u32) << 18) + | ((self.skip_mode as u32) << 19) + | ((self.reduced_tx_set as u32) << 20) + | ((self.superres as u32) << 21) + | (((self.tx_mode & 0x3) as u32) << 22) + | ((self.use_ref_frame_mvs as u32) << 24) + | ((self.enable_ref_frame_mvs as u32) << 25) + | ((self.reference_frame_update as u32) << 26) + } +} + +/// `FormatAndPictureInfoFlags`' members. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct FormatFlagsAv1 { + /// Two bits: 0 KEY, 1 INTER, 2 INTRA_ONLY, 3 SWITCH. + pub frame_type: u8, + pub show_frame: bool, + pub showable_frame: bool, + pub subsampling_x: bool, + pub subsampling_y: bool, + pub mono_chrome: bool, +} + +impl FormatFlagsAv1 { + pub const fn pack(self) -> u8 { + (self.frame_type & 0x3) + | ((self.show_frame as u8) << 2) + | ((self.showable_frame as u8) << 3) + | ((self.subsampling_x as u8) << 4) + | ((self.subsampling_y as u8) << 5) + | ((self.mono_chrome as u8) << 6) + } +} + +/// `DXVA_Tile_AV1` — one tile's location in the bitstream buffer. 16 bytes. +/// +/// ONE RECORD PER TILE, not per tile GROUP. `row` and `column` are the tile's +/// position in the frame's tile grid, which only a per-tile record can carry, and +/// libavcodec's `dxva2_av1.c` sizes its array `frame_header->tile_cols * +/// frame_header->tile_rows` and fills it `for (tile_num = h->tg_start; tile_num <= +/// h->tg_end; tile_num++)`. A frame whose four tiles arrive in one tile group is +/// four of these, not one. +/// +/// [`Self::data_offset`] and [`Self::data_size`] address that tile's raw payload +/// inside the bitstream buffer: the bytes AFTER its `tile_size_minus_1` field, and +/// not one byte more. See [`mod@crate::pack_av1`] for what the buffer holds around +/// them. +#[repr(C, packed)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct TileAv1 { + pub data_offset: u32, + pub data_size: u32, + pub row: u16, + pub column: u16, + pub reserved16: u16, + pub anchor_frame: u8, + pub reserved8: u8, +} + +// The byte-view permission ([`crate::dxva::as_bytes`] / [`crate::dxva::slice_bytes`]), +// for the two structures a submission actually copies into a driver mapping. The +// sealed trait's argument is even simpler here than for the H.264/HEVC buffers: +// `#[repr(C, packed)]` leaves NO padding at all, so "every byte is initialized" is +// a property of the layout rather than of how carefully `zeroed()` was written. +// +// The nested blocks (`TilesAv1`, `LoopFilterAv1`, …) deliberately do NOT implement +// it: they are never submitted on their own, only as members of +// [`PicParamsAv1`]. +impl crate::dxva::DxvaBuffer for PicParamsAv1 {} +impl crate::dxva::DxvaBuffer for TileAv1 {} + +// Every number below was printed by `layout-probe-av1.c`, compiled with MSVC +// against the Windows SDK's own `dxva.h` (10.0.26100.0) on .173. Not transcribed +// from a specification, and not copied from libavcodec. +const _: () = { + use std::mem::offset_of; + use std::mem::size_of; + + assert!(size_of::() == 36); + assert!(size_of::() == 16); + assert!(size_of::() == 912); + + assert!(offset_of!(PicParamsAv1, width) == 0); + assert!(offset_of!(PicParamsAv1, height) == 4); + assert!(offset_of!(PicParamsAv1, max_width) == 8); + assert!(offset_of!(PicParamsAv1, max_height) == 12); + assert!(offset_of!(PicParamsAv1, curr_pic_texture_index) == 16); + assert!(offset_of!(PicParamsAv1, superres_denom) == 17); + assert!(offset_of!(PicParamsAv1, bitdepth) == 18); + assert!(offset_of!(PicParamsAv1, seq_profile) == 19); + assert!(offset_of!(PicParamsAv1, tiles) == 20); + assert!(offset_of!(PicParamsAv1, coding) == 280); + assert!(offset_of!(PicParamsAv1, format) == 284); + assert!(offset_of!(PicParamsAv1, primary_ref_frame) == 285); + assert!(offset_of!(PicParamsAv1, order_hint) == 286); + assert!(offset_of!(PicParamsAv1, order_hint_bits) == 287); + assert!(offset_of!(PicParamsAv1, frame_refs) == 288); + assert!(offset_of!(PicParamsAv1, ref_frame_map_texture_index) == 540); + assert!(offset_of!(PicParamsAv1, loop_filter) == 548); + assert!(offset_of!(PicParamsAv1, quantization) == 576); + assert!(offset_of!(PicParamsAv1, cdef) == 588); + assert!(offset_of!(PicParamsAv1, interp_filter) == 605); + assert!(offset_of!(PicParamsAv1, segmentation) == 606); + assert!(offset_of!(PicParamsAv1, film_grain) == 746); + assert!(offset_of!(PicParamsAv1, reserved32) == 904); + assert!(offset_of!(PicParamsAv1, status_report_feedback_number) == 908); + + // Nested offsets, measured through the outer struct so a wrong INTERNAL + // layout cannot hide behind a right outer one. + assert!(offset_of!(TilesAv1, widths) == 4); + assert!(offset_of!(TilesAv1, heights) == 132); + assert!(offset_of!(LoopFilterAv1, filter_level_u) == 2); + assert!(offset_of!(LoopFilterAv1, sharpness_level) == 4); + assert!(offset_of!(LoopFilterAv1, control_flags) == 5); + assert!(offset_of!(LoopFilterAv1, ref_deltas) == 6); + assert!(offset_of!(LoopFilterAv1, mode_deltas) == 14); + assert!(offset_of!(LoopFilterAv1, delta_lf_res) == 16); + assert!(offset_of!(LoopFilterAv1, frame_restoration_type) == 17); + assert!(offset_of!(LoopFilterAv1, log2_restoration_unit_size) == 20); + assert!(size_of::() == 28); + assert!(offset_of!(QuantizationAv1, base_qindex) == 1); + assert!(offset_of!(QuantizationAv1, qm_y) == 7); + assert!(size_of::() == 12); + assert!(offset_of!(CdefAv1, y_strengths) == 1); + assert!(offset_of!(CdefAv1, uv_strengths) == 9); + assert!(size_of::() == 17); + assert!(offset_of!(SegmentationAv1, feature_mask) == 4); + assert!(offset_of!(SegmentationAv1, feature_data) == 12); + assert!(size_of::() == 140); + assert!(offset_of!(FilmGrainAv1, grain_seed) == 2); + assert!(offset_of!(FilmGrainAv1, scaling_points_y) == 4); + assert!(offset_of!(FilmGrainAv1, num_y_points) == 32); + assert!(offset_of!(FilmGrainAv1, scaling_points_cb) == 33); + assert!(offset_of!(FilmGrainAv1, num_cb_points) == 53); + assert!(offset_of!(FilmGrainAv1, scaling_points_cr) == 54); + assert!(offset_of!(FilmGrainAv1, num_cr_points) == 74); + assert!(offset_of!(FilmGrainAv1, ar_coeffs_y) == 75); + assert!(offset_of!(FilmGrainAv1, ar_coeffs_cb) == 99); + assert!(offset_of!(FilmGrainAv1, ar_coeffs_cr) == 124); + assert!(offset_of!(FilmGrainAv1, cb_mult) == 149); + assert!(offset_of!(FilmGrainAv1, cb_offset) == 154); + assert!(offset_of!(FilmGrainAv1, cr_offset) == 156); + assert!(size_of::() == 158); + assert!(offset_of!(PicEntryAv1, wmmat) == 8); + assert!(offset_of!(PicEntryAv1, global_motion_flags) == 32); + assert!(offset_of!(PicEntryAv1, index) == 33); + assert!(offset_of!(TileAv1, data_size) == 4); + assert!(offset_of!(TileAv1, row) == 8); + assert!(offset_of!(TileAv1, column) == 10); + assert!(offset_of!(TileAv1, anchor_frame) == 14); +}; + +#[cfg(test)] +mod tests { + use super::*; + + /// Every packed word, checked against the value MSVC produced for the same + /// single-field assignment. These are the probe's own printed numbers — the + /// point being that C bit-field order is ABI-defined, so the only honest way + /// to know where `tx_mode` lands is to have asked the compiler that the + /// driver agrees with. + #[test] + fn packed_words_match_what_msvc_measured() { + assert_eq!( + CodingFlagsAv1 { + use_128x128_superblock: true, + ..Default::default() + } + .pack(), + 0x0000_0001 + ); + assert_eq!( + CodingFlagsAv1 { + tx_mode: 3, + ..Default::default() + } + .pack(), + 0x00c0_0000 + ); + assert_eq!( + CodingFlagsAv1 { + reference_frame_update: true, + ..Default::default() + } + .pack(), + 0x0400_0000 + ); + assert_eq!( + FormatFlagsAv1 { + frame_type: 3, + ..Default::default() + } + .pack(), + 0x03 + ); + assert_eq!( + FormatFlagsAv1 { + mono_chrome: true, + ..Default::default() + } + .pack(), + 0x40 + ); + assert_eq!( + LoopFilterFlagsAv1 { + delta_lf_present: true, + ..Default::default() + } + .pack(), + 0x08 + ); + assert_eq!( + QuantizationFlagsAv1 { + delta_q_res: 3, + ..Default::default() + } + .pack(), + 0x06 + ); + assert_eq!( + CdefFlagsAv1 { + bits: 3, + ..Default::default() + } + .pack(), + 0x0c + ); + assert_eq!( + CdefStrength { + secondary: 3, + ..Default::default() + } + .pack(), + 0xc0 + ); + assert_eq!( + SegmentationFlagsAv1 { + temporal_update: true, + ..Default::default() + } + .pack(), + 0x08 + ); + assert_eq!( + SegmentFeatureMask { + globalmv: true, + ..Default::default() + } + .pack(), + 0x80 + ); + assert_eq!( + FilmGrainFlagsAv1 { + ar_coeff_shift_minus6: 3, + ..Default::default() + } + .pack(), + 0x00c0 + ); + assert_eq!( + FilmGrainFlagsAv1 { + matrix_coeff_is_identity: true, + ..Default::default() + } + .pack(), + 0x1000 + ); + } + + /// A zeroed picture-parameters block must name NO references. `0` is a valid + /// surface index, so a memset-style default would quietly point every unused + /// reference at surface 0 — which decodes, and decodes wrong. + #[test] + fn a_zeroed_block_names_no_reference() { + let p = PicParamsAv1::zeroed(); + assert!(p + .ref_frame_map_texture_index + .iter() + .all(|i| *i == UNUSED_INDEX)); + assert!(p.frame_refs.iter().all(|r| r.index == UNUSED_INDEX)); + } +} diff --git a/crates/pf-dxvadec/src/lib.rs b/crates/pf-dxvadec/src/lib.rs new file mode 100644 index 00000000..88549a0e --- /dev/null +++ b/crates/pf-dxvadec/src/lib.rs @@ -0,0 +1,172 @@ +//! Native D3D11VA (DXVA) H.264/HEVC/AV1 decode for the Windows clients — M5 and +//! M7 of the native-decode program, and the DXVA counterpart of [`pf_vkdecode`]. +//! +//! This crate is the CPU-testable half: everything between pf-bitstream's per-AU +//! plan and the bytes an `ID3D11VideoContext::SubmitDecoderBuffers` call +//! delivers. It never touches D3D11, COM, or any Windows type — which is the +//! point. The D3D11VA rung is `cfg(windows)` code that neither the macOS +//! development host nor the Linux container can compile, let alone run, so +//! anything left inside that boundary is verified by a `cargo check` on a remote +//! box and nothing more. Everything that can be a pure decision or a pure +//! conversion lives here instead, where the ordinary gates run it on every leg. +//! +//! - [`dxva`]: the `dxva.h` buffer layouts, **hand-declared** — windows-rs does +//! not generate them (see the module docs for the verification and for the +//! compile-time size/offset proofs that stand in for a header). +//! - [`config`]: decoder-creation decisions — profile GUID per codec/shape, +//! `D3D11_VIDEO_DECODER_CONFIG` selection (short-format slice control, whose +//! `ConfigBitstreamRaw` value is H.264's alone), surface alignment and pool +//! sizing. +//! - [`pack`]: the bitstream buffer's contents — start-code normalisation and +//! the 128-byte tail padding rule. +//! - [`mod@pack_av1`]: the same job for AV1, which shares neither rule — no start +//! codes to normalise, and a padding that is charged to the buffer rather than +//! to the last record. +//! - [`pic`] / [`pic_h265`] / [`pic_av1`]: one [`pf_bitstream`] `AuPlan` into +//! `DXVA_PicParams_*`, `DXVA_Qmatrix_*` and the slice-control (AV1: +//! tile-control) records, with the reference lists resolved through a DPB slot +//! map. +//! - [`descriptors`]: which buffers one `SubmitDecoderBuffers` call carries — +//! four for H.264, three or four for HEVC, three for AV1 — and the four +//! `D3D11_VIDEO_DECODER_BUFFER_DESC` fields that are a decision — +//! where two of review 13's three structural defects lived, and the reason +//! they are now a CPU test rather than a Windows-only code path. +//! +//! # Why the slot map comes from pf-vkdecode +//! +//! [`SlotMap`] is re-exported from [`pf_vkdecode`] rather than reimplemented. +//! It is not a Vulkan object: it is a ledger mapping +//! [`pf_bitstream::h264::PicId`]s to hardware DPB slot indices, codec-agnostic +//! (H.264 and H.265 share it there) and, as it turns out, API-agnostic too — +//! DXVA's `DXVA_PicEntry::Index7Bits` is a decode-surface index with exactly the +//! lifetime the map already models. It lives in pf-vkdecode because M2 is where +//! it was written and where hardware has already proven it over a 92-minute +//! soak; duplicating a ledger of that provenance to avoid one crate edge would +//! buy nothing and cost a divergence. +//! +//! # Unsafe posture +//! +//! Almost none. Every DXVA structure is built field by field from a `const fn +//! zeroed()`, never `mem::zeroed`, so construction is entirely safe code. The +//! only unsafe in the crate is the byte view the submission needs +//! ([`dxva::as_bytes`] / [`dxva::slice_bytes`]), fenced behind a sealed trait +//! that only this crate's `#[repr(C)]` PODs implement, and carrying a written +//! proof — enforced: +#![deny(clippy::undocumented_unsafe_blocks)] + +pub mod config; +pub mod descriptors; +pub mod dxva; +pub mod dxva_av1; +pub mod pack; +pub mod pack_av1; +pub mod pic; +pub mod pic_av1; +pub mod pic_h265; + +/// The AV1 tile walk, borrowed from the Vulkan crate for exactly the reason +/// [`SlotMap`] is: it is spec-literal `tile_group_obu()` byte arithmetic (5.11.1) +/// with no Vulkan in it, both native rungs need the same per-tile payload ranges, +/// and a second copy would be a second chance to get the `tile_size_minus_1` +/// widths wrong. [`Av1Bitstream::groups`] is the half only this crate reads — +/// see [`mod@pack_av1`] for why the two rungs upload different layouts. +pub use pf_vkdecode::plan_bitstream; +pub use pf_vkdecode::Av1Bitstream; +pub use pf_vkdecode::Av1TileError; +/// The DPB slot ledger — see the crate docs for why it is borrowed rather than +/// redefined. Re-exported so this crate's callers name it through `pf_dxvadec`. +pub use pf_vkdecode::SlotError; +pub use pf_vkdecode::SlotMap; + +// Unlike pf-vkdecode, this crate exposes the PLANNER rather than wrapping it: DXVA +// submission is synchronous and stateless (`DecoderBeginFrame` … `DecoderEndFrame` +// with no fences, no timeline, no picture pool to decouple), so there is no +// per-decode state worth an owning decoder type here. The Windows layer drives the +// planner itself — and names every type it touches through this crate, so it needs +// no pf-bitstream dependency of its own. +/// The AV1 planner and its plan. ⚠ Its `plan_au` returns a **`Vec`** of plans: an +/// AV1 access unit is a TEMPORAL UNIT and may carry several frames, of which at +/// most one displays. +pub use pf_bitstream::av1::AuPlan as AuPlanAv1; +pub use pf_bitstream::av1::Av1Planner; +pub use pf_bitstream::av1::FrameType as FrameTypeAv1; +pub use pf_bitstream::av1::PicId as PicIdAv1; +pub use pf_bitstream::av1::PlanError as PlanErrorAv1; +pub use pf_bitstream::av1::PlanWarning as PlanWarningAv1; +pub use pf_bitstream::av1::NUM_REF_SLOTS; +/// The H.264 planner and the plan it produces. +pub use pf_bitstream::h264::AuPlan; +pub use pf_bitstream::h264::ColourDescription; +pub use pf_bitstream::h264::DisplayCrop; +pub use pf_bitstream::h264::H264Planner; +pub use pf_bitstream::h264::PlanError; +pub use pf_bitstream::h264::PlanWarning; +/// The H.265 planner and its plan — separate types with the same job, exactly as +/// pf-bitstream defines them (their warning enums genuinely differ, and a consumer +/// dispatching per codec must be able to name both). +pub use pf_bitstream::h265::AuPlan as AuPlanH265; +pub use pf_bitstream::h265::H265Planner; +pub use pf_bitstream::h265::PlanError as PlanErrorH265; +pub use pf_bitstream::h265::PlanWarning as PlanWarningH265; +/// Which warnings mean the PICTURE is damaged — pf-vkdecode's one list, reused so +/// both native rungs conceal on exactly the same predicate. +pub use pf_vkdecode::is_integrity_warning; +pub use pf_vkdecode::is_integrity_warning_av1; +pub use pf_vkdecode::is_integrity_warning_h265; + +pub use config::align_surface; +pub use config::pick_config; +pub use config::pool_size; +pub use config::profile_for; +pub use config::short_slice_config; +pub use config::surface_alignment; +pub use config::Codec; +pub use config::ConfigFacts; +pub use config::DxvaProfile; +pub use config::AV1_VLD_PROFILE0; +pub use config::AV1_VLD_PROFILE0_10BIT; +pub use config::DXGI_FORMAT_NV12; +pub use config::DXGI_FORMAT_P010; +pub use config::H264_VLD_NOFGT; +pub use config::HEVC_VLD_MAIN; +pub use config::HEVC_VLD_MAIN10; +pub use descriptors::descriptors_av1; +pub use descriptors::descriptors_h264; +pub use descriptors::descriptors_h265; +pub use descriptors::BufferDescriptor; +pub use descriptors::BUFFER_BITSTREAM; +pub use descriptors::BUFFER_INVERSE_QUANTIZATION_MATRIX; +pub use descriptors::BUFFER_PICTURE_PARAMETERS; +pub use descriptors::BUFFER_SLICE_CONTROL; +pub use dxva::as_bytes; +pub use dxva::slice_bytes; +pub use dxva::PicParamsH264; +pub use dxva::PicParamsHevc; +pub use dxva::QmatrixH264; +pub use dxva::QmatrixHevc; +pub use dxva::SliceH264Short; +pub use dxva::SliceHevcShort; +pub use dxva::BITSTREAM_ALIGN; +pub use dxva_av1::PicParamsAv1; +pub use dxva_av1::TileAv1; +pub use pack::pack; +pub use pack::packed_size; +pub use pack::PackError; +pub use pack::Packed; +pub use pack::SliceRecord; +pub use pack_av1::pack_av1; +pub use pack_av1::packed_size_av1; +pub use pack_av1::PackedAv1; +pub use pic::plan_to_dxva; +pub use pic::slice_control; +pub use pic::DecodePlanDxva; +pub use pic::DxvaRef; +pub use pic::PlanToDxvaError; +pub use pic_av1::plan_to_dxva_av1; +pub use pic_av1::DecodePlanDxvaAv1; +pub use pic_av1::PlanToDxvaAv1Error; +pub use pic_h265::plan_to_dxva_h265; +pub use pic_h265::slice_control_h265; +pub use pic_h265::DecodePlanDxvaH265; +pub use pic_h265::DxvaRefH265; +pub use pic_h265::PlanToDxvaH265Error; diff --git a/crates/pf-dxvadec/src/pack.rs b/crates/pf-dxvadec/src/pack.rs new file mode 100644 index 00000000..614e3777 --- /dev/null +++ b/crates/pf-dxvadec/src/pack.rs @@ -0,0 +1,368 @@ +//! Bitstream-buffer packing: the AU's slice NALUs laid into the mapping +//! `GetDecoderBuffer(D3D11_VIDEO_DECODER_BUFFER_BITSTREAM)` hands back, plus the +//! byte locations the slice-control records point at. +//! +//! Pure, so the rules that are easy to get subtly wrong — the start-code +//! normalisation and the 128-byte tail padding — are unit-tested on every CI leg +//! rather than inferred from a picture on someone's screen. +//! +//! Three rules, all from the DXVA specs and all matching what libavcodec's +//! `dxva2_h264.c`/`dxva2_hevc.c` submit: +//! +//! 1. **Every slice is preceded by a three-byte start code** (`00 00 01`) in the +//! buffer, and `BSNALunitDataLocation` points at the start code, not at the +//! NALU header. The AU as planned already carries start codes, but Annex-B +//! allows a four-byte one (a `zero_byte` ahead of the three-byte prefix) and +//! the plan's ranges include whichever the encoder emitted; normalising to +//! three keeps every driver looking at the byte pattern its reference +//! implementation was written against. +//! 2. **`SliceBytesInBuffer` counts the start code**, so it is `3 + NALU bytes`. +//! 3. **The buffer's data size is padded to a multiple of 128 bytes with zeros**, +//! and the padding is charged to the LAST slice's `SliceBytesInBuffer`. +//! Trailing zeros after a slice's `rbsp_trailing_bits` are legal bitstream +//! filler, so a driver that reads to the stated length reads only padding — +//! which is precisely why the padding must be counted rather than left +//! dangling past the last record. +//! +//! Non-VCL NALUs never enter the buffer: only the plan's slice ranges are packed. +//! That is the same discipline pf-vkdecode's recording layer follows (feeding +//! whole AUs, parameter sets included, hangs VCN firmware), and here it also +//! keeps `BSNALunitDataLocation` meaningful — the specs define it as the offset +//! of a *slice* NALU. + +use std::ops::Range; + +use crate::dxva::BITSTREAM_ALIGN; + +/// One packed slice, in the codec-neutral shape both slice-control records need. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SliceRecord { + /// `BSNALunitDataLocation` — byte offset of the slice's start code within + /// the bitstream buffer. + pub location: u32, + /// `SliceBytesInBuffer` — start code plus NALU bytes, plus (on the last + /// slice only) the buffer's tail padding. + pub bytes: u32, +} + +/// What came out of a pack. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Packed { + pub records: Vec, + /// Bytes written, padding included — the `DataSize` of the bitstream + /// buffer's `D3D11_VIDEO_DECODER_BUFFER_DESC`. + pub data_size: u32, +} + +/// Why an AU could not be packed. All three are stream or sizing conditions the +/// caller turns into a refused AU, never a panic. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PackError { + /// The plan held no slices. + NoSlices, + /// A slice range falls outside the AU buffer it was planned from — a caller + /// bug (a plan paired with the wrong AU), checked rather than trusted + /// because the alternative is an out-of-bounds slice index. + RangeOutsideAu { start: usize, end: usize, au: usize }, + /// A slice range does not begin with an Annex-B start code. pf-bitstream + /// plans from an Annex-B AU, so this means the AU was mutated between + /// planning and packing. + NoStartCode { start: usize }, + /// The driver's bitstream buffer cannot hold this AU. Real: a mapping is + /// typically a few MiB and a 4K IDR can approach it; the honest answer is a + /// refused AU (and the recovery request that follows) rather than a + /// truncated picture. + BufferTooSmall { needed: usize, capacity: usize }, + /// A byte offset or length exceeded `u32`, which is what the DXVA records + /// carry. + Overflow(usize), + /// AV1 ([`mod@crate::pack_av1`]): the frame carried no tile data. + NoTiles, + /// AV1: a tile payload lies inside none of the tile-group regions the same + /// walk produced. Unreachable through [`pf_vkdecode::plan_bitstream`], and + /// checked because the alternative to refusing is a tile record addressing + /// another tile's bytes. + TileOutsideGroup { start: usize, end: usize }, + /// AV1: the caller's record template and the walk's tile list are different + /// lengths, so no record can be matched to a tile with confidence. + TileCountMismatch { records: usize, tiles: usize }, +} + +impl std::fmt::Display for PackError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PackError::NoSlices => write!(f, "the plan holds no slices"), + PackError::RangeOutsideAu { start, end, au } => { + write!( + f, + "slice range {start}..{end} falls outside the {au}-byte AU" + ) + } + PackError::NoStartCode { start } => { + write!(f, "the slice at byte {start} carries no Annex-B start code") + } + PackError::BufferTooSmall { needed, capacity } => write!( + f, + "the AU needs {needed} bitstream bytes; the driver's buffer holds {capacity}" + ), + PackError::Overflow(value) => write!(f, "byte value {value} exceeds u32"), + PackError::NoTiles => write!(f, "the frame carried no tile data"), + PackError::TileOutsideGroup { start, end } => write!( + f, + "tile payload {start}..{end} lies inside no tile-group region" + ), + PackError::TileCountMismatch { records, tiles } => write!( + f, + "{records} tile records against {tiles} tiles in the bitstream" + ), + } + } +} + +impl std::error::Error for PackError {} + +/// The payload of an Annex-B NALU: the bytes after its start code. +/// +/// Accepts both prefixes the standard allows — `00 00 01` and `00 00 00 01` — +/// and nothing else. Deliberately not a general scan: the plan says this range +/// IS a NALU, so a start code anywhere but the front would mean the range is +/// wrong, and finding a later one would paper over that. +fn nalu_payload(nalu: &[u8]) -> Option<&[u8]> { + match nalu { + [0, 0, 1, rest @ ..] => Some(rest), + [0, 0, 0, 1, rest @ ..] => Some(rest), + _ => None, + } +} + +/// The exact byte count [`pack`] needs before padding: three start-code bytes +/// plus each slice's NALU payload. +/// +/// Separate from [`pack`] so a caller can size or check a mapping before it maps +/// one — and so the "how big is this AU" question has one answer rather than two +/// that can drift. +pub fn packed_size(au: &[u8], slices: &[Range]) -> Result { + if slices.is_empty() { + return Err(PackError::NoSlices); + } + let mut total = 0usize; + for range in slices { + let nalu = au.get(range.clone()).ok_or(PackError::RangeOutsideAu { + start: range.start, + end: range.end, + au: au.len(), + })?; + let payload = nalu_payload(nalu).ok_or(PackError::NoStartCode { start: range.start })?; + total = total.saturating_add(3 + payload.len()); + } + Ok(total) +} + +/// Pack `slices` of `au` into `dst`, returning the slice-control locations. +/// +/// `dst` is the driver's mapped bitstream buffer (its whole reported size, not a +/// sub-slice): the padding rule needs to know the real capacity, because a +/// buffer with no room for the tail padding gets as much as fits rather than an +/// error — the picture is complete either way, and libavcodec clamps the same +/// way. +pub fn pack(au: &[u8], slices: &[Range], dst: &mut [u8]) -> Result { + let needed = packed_size(au, slices)?; + if needed > dst.len() { + return Err(PackError::BufferTooSmall { + needed, + capacity: dst.len(), + }); + } + + let mut records = Vec::with_capacity(slices.len()); + let mut cursor = 0usize; + for range in slices { + // `packed_size` already proved both of these; re-deriving rather than + // caching keeps this loop's indexing self-evidently in bounds. + let nalu = au.get(range.clone()).ok_or(PackError::RangeOutsideAu { + start: range.start, + end: range.end, + au: au.len(), + })?; + let payload = nalu_payload(nalu).ok_or(PackError::NoStartCode { start: range.start })?; + let location = u32::try_from(cursor).map_err(|_| PackError::Overflow(cursor))?; + dst[cursor..cursor + 3].copy_from_slice(&[0, 0, 1]); + dst[cursor + 3..cursor + 3 + payload.len()].copy_from_slice(payload); + cursor += 3 + payload.len(); + let bytes = + u32::try_from(3 + payload.len()).map_err(|_| PackError::Overflow(payload.len()))?; + records.push(SliceRecord { location, bytes }); + } + + // Tail padding to the 128-byte granule. libavcodec's expression is + // `128 - (current & 127)`, which yields a FULL 128-byte block when the data + // already lands on the granule rather than yielding zero — reproduced + // verbatim, because "the buffer always ends in at least one padding byte" is + // a property some drivers have been observed to want and none can object to. + let want = BITSTREAM_ALIGN - (cursor % BITSTREAM_ALIGN); + let padding = want.min(dst.len() - cursor); + dst[cursor..cursor + padding].fill(0); + cursor += padding; + if let Some(last) = records.last_mut() { + // Charged to the last record so a driver reading `SliceBytesInBuffer` + // never walks past the data size — see the module docs. + last.bytes = last + .bytes + .saturating_add(u32::try_from(padding).unwrap_or(u32::MAX)); + } + + Ok(Packed { + records, + data_size: u32::try_from(cursor).map_err(|_| PackError::Overflow(cursor))?, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// An AU of `count` slices, each `payload` bytes, alternating between the + /// three- and four-byte start codes so both prefixes are exercised. + fn synth_au(count: usize, payload: usize) -> (Vec, Vec>) { + let mut au = Vec::new(); + let mut ranges = Vec::new(); + for i in 0..count { + let start = au.len(); + if i % 2 == 0 { + au.extend_from_slice(&[0, 0, 1]); + } else { + au.extend_from_slice(&[0, 0, 0, 1]); + } + // A recognisable, non-zero payload per slice. + au.extend(std::iter::repeat_n(0xA0 + i as u8, payload)); + ranges.push(start..au.len()); + } + (au, ranges) + } + + #[test] + fn each_slice_is_written_with_a_three_byte_start_code_whatever_the_au_carried() { + let (au, ranges) = synth_au(2, 5); + let mut dst = vec![0xCCu8; 4096]; + let packed = pack(&au, &ranges, &mut dst).unwrap(); + assert_eq!(packed.records.len(), 2); + // Slice 0: three-byte prefix in the AU, three-byte prefix in the buffer. + assert_eq!(packed.records[0].location, 0); + assert_eq!(&dst[0..3], &[0, 0, 1]); + assert_eq!(&dst[3..8], &[0xA0; 5]); + assert_eq!(packed.records[0].bytes, 8); + // Slice 1 carried a FOUR-byte prefix in the AU and still lands as three. + assert_eq!(packed.records[1].location, 8); + assert_eq!(&dst[8..11], &[0, 0, 1]); + assert_eq!(&dst[11..16], &[0xA1; 5]); + } + + #[test] + fn the_buffer_is_padded_to_a_128_byte_granule_and_the_padding_is_charged_to_the_last_slice() { + let (au, ranges) = synth_au(1, 5); + let mut dst = vec![0xCCu8; 4096]; + let packed = pack(&au, &ranges, &mut dst).unwrap(); + // 3 + 5 = 8 bytes of slice, padded to 128. + assert_eq!(packed.data_size, 128); + assert_eq!(packed.records[0].bytes, 128); + assert!(dst[8..128].iter().all(|&b| b == 0), "padding must be zeros"); + // Beyond the reported data size the mapping is untouched — the driver + // never looks there, and neither do we. + assert_eq!(dst[128], 0xCC); + } + + #[test] + fn data_already_on_the_granule_still_gets_a_full_padding_block() { + // 125-byte payload + 3-byte start code = 128 exactly. + let (au, ranges) = synth_au(1, 125); + let mut dst = vec![0u8; 4096]; + let packed = pack(&au, &ranges, &mut dst).unwrap(); + assert_eq!(packed.data_size, 256); + assert_eq!(packed.records[0].bytes, 256); + } + + #[test] + fn padding_is_clamped_to_what_the_mapping_can_hold() { + let (au, ranges) = synth_au(1, 5); + // Exactly enough for the slice and ten spare bytes. + let mut dst = vec![0u8; 18]; + let packed = pack(&au, &ranges, &mut dst).unwrap(); + assert_eq!(packed.data_size, 18); + assert_eq!(packed.records[0].bytes, 18); + } + + #[test] + fn an_au_larger_than_the_mapping_is_refused_rather_than_truncated() { + let (au, ranges) = synth_au(2, 100); + let mut dst = vec![0u8; 64]; + assert_eq!( + pack(&au, &ranges, &mut dst), + Err(PackError::BufferTooSmall { + needed: 206, + capacity: 64, + }) + ); + } + + #[test] + fn locations_are_contiguous_across_a_multi_slice_au() { + let (au, ranges) = synth_au(4, 30); + let mut dst = vec![0u8; 4096]; + let packed = pack(&au, &ranges, &mut dst).unwrap(); + let mut expected = 0u32; + for (i, record) in packed.records.iter().enumerate() { + assert_eq!(record.location, expected); + // Only the last record carries padding. + if i + 1 < packed.records.len() { + assert_eq!(record.bytes, 33); + } + expected += 33; + } + assert_eq!(packed.data_size, 4 * 33 + (128 - (4 * 33) % 128)); + } + + #[test] + fn a_slice_without_a_start_code_is_a_typed_error_not_a_mis_packed_buffer() { + let au = vec![0x41u8; 32]; + let mut dst = vec![0u8; 512]; + let range = 0usize..32; + assert_eq!( + pack(&au, std::slice::from_ref(&range), &mut dst), + Err(PackError::NoStartCode { start: 0 }) + ); + } + + #[test] + fn a_range_outside_the_au_is_caught_before_it_indexes() { + let (au, _) = synth_au(1, 5); + let mut dst = vec![0u8; 512]; + let au_len = au.len(); + let range = 0usize..au_len + 1; + assert_eq!( + pack(&au, std::slice::from_ref(&range), &mut dst), + Err(PackError::RangeOutsideAu { + start: 0, + end: au_len + 1, + au: au_len, + }) + ); + } + + #[test] + fn an_empty_plan_is_refused() { + let mut dst = vec![0u8; 512]; + assert_eq!(pack(&[], &[], &mut dst), Err(PackError::NoSlices)); + assert_eq!(packed_size(&[], &[]), Err(PackError::NoSlices)); + } + + #[test] + fn packed_size_agrees_with_what_pack_writes_before_padding() { + let (au, ranges) = synth_au(3, 17); + assert_eq!(packed_size(&au, &ranges).unwrap(), 3 * 20); + let mut dst = vec![0u8; 4096]; + let packed = pack(&au, &ranges, &mut dst).unwrap(); + let unpadded: u32 = + packed.records.iter().map(|r| r.bytes).sum::() - (packed.data_size - 3 * 20u32); + assert_eq!(unpadded, 60); + } +} diff --git a/crates/pf-dxvadec/src/pack_av1.rs b/crates/pf-dxvadec/src/pack_av1.rs new file mode 100644 index 00000000..e3dbf7c5 --- /dev/null +++ b/crates/pf-dxvadec/src/pack_av1.rs @@ -0,0 +1,387 @@ +//! The AV1 bitstream buffer's contents, and the tile-control records that address +//! it — the counterpart of [`mod@crate::pack`], which cannot be reused because AV1 has +//! no Annex-B start codes to normalise and no slices to prefix. +//! +//! # What goes in the buffer +//! +//! Every tile-group (or frame) OBU's **`tile_data` region**, concatenated in plan +//! order: from the first tile's `tile_size_minus_1` field through the end of the +//! OBU payload. Not the OBU header, not the `obu_size` field, not — for an +//! `OBU_FRAME` — the frame header, all of which the driver reads out of +//! `DXVA_PicParams_AV1` instead. The `tile_size_minus_1` fields BETWEEN tiles do +//! ride along, unread. +//! +//! That is byte for byte what libavcodec's `dxva2_av1.c` uploads. Its +//! `decode_slice` is handed `raw_tile_group->tile_data.data` — CBS AV1's name for +//! exactly this region — and either points `ctx_pic->bitstream` straight at it +//! (the single-tile-group shortcut) or `memcpy`s each one onto the end of an +//! accumulating buffer; `commit_bitstream_and_slice_buffer` then `memcpy`s the +//! result into the driver's mapping. [`pf_vkdecode::Av1Bitstream::groups`] is that +//! same region, produced by the same walk that finds the tiles. +//! +//! ⚠ The native Vulkan rung uploads something DIFFERENT — the tile payloads alone, +//! size fields stripped — and both are correct, because both APIs address tiles by +//! an explicit (offset, size) pair and neither ever reads the bytes between them. +//! The layouts differ because the METHOD differs: on Vulkan the reference +//! implementation is libavcodec's Vulkan hwaccel, and here it is libavcodec's DXVA +//! hwaccel. This backend reproduces libavcodec on the evidence that a hand-built +//! variant of a D3D11VA submission was once rejected by an Intel driver outright, +//! so where a choice exists it is not made on first principles. +//! +//! # Two rules that differ from the H.264/HEVC packer +//! +//! 1. **The padding is charged to NOBODY.** `commit_bitstream_and_slice_buffer` +//! pads the bitstream buffer to the 128-byte granule with the same expression +//! `dxva2_h264.c` uses — `FFMIN(128 - (size & 127), dxva_size - size)`, so a +//! buffer already on the granule still gets a full block — and adds it to the +//! BUFFER's `DataSize`. It does not touch a single `DXVA_Tile_AV1`. The H.264 +//! and HEVC paths do the opposite (`SliceBytesInBuffer += padding` on the last +//! record), and copying that habit here would tell the driver the last tile is +//! up to 128 bytes longer than it is — trailing zeros are legal filler after a +//! slice's `rbsp_trailing_bits`, but an AV1 tile's size is exact and its +//! entropy decoder is not looking for a stop bit. +//! 2. **One record per TILE, not per tile group.** See [`TileAv1`]. + +use pf_vkdecode::Av1Bitstream; + +use crate::dxva::BITSTREAM_ALIGN; +use crate::dxva_av1::TileAv1; +use crate::pack::PackError; + +/// What came out of an AV1 pack. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PackedAv1 { + /// The tile-control records as the driver reads them: the caller's rows, + /// columns and `anchor_frame`, with `DataOffset`/`DataSize` rewritten to + /// address the packed buffer. + pub tiles: Vec, + /// Bytes written, padding included — the `DataSize` of the bitstream buffer's + /// `D3D11_VIDEO_DECODER_BUFFER_DESC`. + pub data_size: u32, +} + +/// The exact byte count [`pack_av1`] needs before padding: every tile-group +/// region, end to end. +/// +/// Separate from [`pack_av1`] for the same reason [`crate::pack::packed_size`] is: +/// so "how big is this access unit's tile data" has one answer rather than two +/// that can drift. +pub fn packed_size_av1(bitstream: &Av1Bitstream) -> usize { + bitstream.groups.iter().fold(0usize, |total, group| { + total.saturating_add(group.end.saturating_sub(group.start)) + }) +} + +/// Pack one frame's tile data into `dst`, returning the tile-control records that +/// address it. +/// +/// `tiles` is the per-tile record template [`crate::plan_to_dxva_av1`] produced: +/// its rows, columns and `anchor_frame` are carried through untouched and its +/// access-unit-relative `DataOffset`/`DataSize` are REPLACED — wholly, both +/// fields, so no record can come out of here half-rebased. +/// +/// `dst` is the driver's mapped bitstream buffer at its whole reported size, not a +/// sub-slice: the padding rule needs the real capacity, because a buffer with no +/// room for the tail padding gets as much as fits rather than an error (libavcodec +/// clamps the same way, and the picture is complete either way). +pub fn pack_av1( + au: &[u8], + bitstream: &Av1Bitstream, + tiles: &[TileAv1], + dst: &mut [u8], +) -> Result { + if bitstream.tiles.is_empty() || bitstream.groups.is_empty() { + return Err(PackError::NoTiles); + } + if tiles.len() != bitstream.tiles.len() { + return Err(PackError::TileCountMismatch { + records: tiles.len(), + tiles: bitstream.tiles.len(), + }); + } + let needed = packed_size_av1(bitstream); + if needed > dst.len() { + return Err(PackError::BufferTooSmall { + needed, + capacity: dst.len(), + }); + } + + // The tile-group regions, copied end to end. `bases` remembers where each + // landed so a tile's offset is its position INSIDE its own group plus that + // group's base — the arithmetic `dxva2_av1.c` spells as + // `ctx_pic->bitstream_size + tile_offset`. + let mut cursor = 0usize; + let mut bases = Vec::with_capacity(bitstream.groups.len()); + for group in &bitstream.groups { + let bytes = au.get(group.clone()).ok_or(PackError::RangeOutsideAu { + start: group.start, + end: group.end, + au: au.len(), + })?; + dst[cursor..cursor + bytes.len()].copy_from_slice(bytes); + bases.push((group.clone(), cursor)); + cursor += bytes.len(); + } + + let mut records = Vec::with_capacity(tiles.len()); + for (tile, template) in bitstream.tiles.iter().zip(tiles) { + // Which group holds this tile. Resolved by CONTAINMENT rather than by + // re-deriving the per-group tile counts: the counts are how the walk split + // the tiles in the first place, and a second derivation that disagreed + // would silently rebase a tile against the wrong group's base. + let (group, base) = bases + .iter() + .find(|(group, _)| group.start <= tile.start && tile.end <= group.end) + .ok_or(PackError::TileOutsideGroup { + start: tile.start, + end: tile.end, + })?; + let offset = base + (tile.start - group.start); + let size = tile.end - tile.start; + records.push(TileAv1 { + data_offset: u32::try_from(offset).map_err(|_| PackError::Overflow(offset))?, + data_size: u32::try_from(size).map_err(|_| PackError::Overflow(size))?, + ..*template + }); + } + + // Tail padding to the 128-byte granule — libavcodec's expression verbatim, so + // data already on the granule still gets a FULL block. Charged to the buffer's + // `DataSize` and to no tile record (module docs). + let want = BITSTREAM_ALIGN - (cursor % BITSTREAM_ALIGN); + let padding = want.min(dst.len() - cursor); + dst[cursor..cursor + padding].fill(0); + cursor += padding; + + Ok(PackedAv1 { + tiles: records, + data_size: u32::try_from(cursor).map_err(|_| PackError::Overflow(cursor))?, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dxva_av1::UNUSED_INDEX; + + /// Byte ranges from `(start, end)` pairs. Spelled this way rather than as + /// `vec![a..b]` because clippy reads a one-element `Vec` of `Range` as a + /// mistyped `vec![a; b]`, which is a fair thing to suspect and not what these + /// are. + fn ranges(pairs: [(usize, usize); N]) -> Vec> { + pairs.into_iter().map(|(start, end)| start..end).collect() + } + + /// A record template with a recognisable row/column and offsets that must not + /// survive the pack. + fn template(row: u16, column: u16) -> TileAv1 { + TileAv1 { + data_offset: 0xDEAD_BEEF, + data_size: 0xDEAD_BEEF, + row, + column, + reserved16: 0, + anchor_frame: UNUSED_INDEX, + reserved8: 0, + } + } + + /// Two tile groups of one tile each, at AU offsets 10..20 and 40..55, with a + /// two-byte size field ahead of nothing (single-tile groups code none) — so + /// each group's region IS its tile. + fn two_groups() -> (Vec, Av1Bitstream) { + let mut au = vec![0u8; 64]; + for (i, byte) in au.iter_mut().enumerate() { + *byte = i as u8; + } + ( + au, + Av1Bitstream { + tiles: ranges([(10, 20), (40, 55)]), + groups: ranges([(10, 20), (40, 55)]), + }, + ) + } + + #[test] + fn the_tile_data_regions_are_concatenated_and_the_offsets_follow_them() { + let (au, bitstream) = two_groups(); + let mut dst = vec![0xCCu8; 512]; + let packed = + pack_av1(&au, &bitstream, &[template(0, 0), template(0, 1)], &mut dst).unwrap(); + assert_eq!(&dst[0..10], &au[10..20]); + assert_eq!(&dst[10..25], &au[40..55]); + assert_eq!( + (packed.tiles[0].data_offset, packed.tiles[0].data_size), + (0, 10) + ); + assert_eq!( + (packed.tiles[1].data_offset, packed.tiles[1].data_size), + (10, 15), + "the second group's tile is rebased onto the first group's length, \ + which is `ctx_pic->bitstream_size + tile_offset`" + ); + // The template's rows and columns ride across; its poison offsets do not. + assert_eq!((packed.tiles[1].row, packed.tiles[1].column), (0, 1)); + let anchor = packed.tiles[1].anchor_frame; + assert_eq!(anchor, UNUSED_INDEX); + } + + #[test] + fn a_tile_inside_a_group_keeps_its_distance_from_the_group_start() { + // One group, 100..160, holding two tiles: the first at 102..120 (two bytes + // of `tile_size_minus_1` ahead of it) and the second at 122..160. The size + // fields are COPIED and never addressed — which is the layout libavcodec + // uploads and the thing a payload-only packer would not reproduce. + let au: Vec = (0..200u32).map(|i| i as u8).collect(); + let bitstream = Av1Bitstream { + tiles: ranges([(102, 120), (122, 160)]), + groups: ranges([(100, 160)]), + }; + let mut dst = vec![0u8; 512]; + let packed = + pack_av1(&au, &bitstream, &[template(0, 0), template(0, 1)], &mut dst).unwrap(); + assert_eq!( + &dst[0..60], + &au[100..160], + "the WHOLE region, size fields and all" + ); + assert_eq!( + (packed.tiles[0].data_offset, packed.tiles[0].data_size), + (2, 18) + ); + assert_eq!( + (packed.tiles[1].data_offset, packed.tiles[1].data_size), + (22, 38) + ); + } + + #[test] + fn the_padding_is_charged_to_the_buffer_and_to_no_tile_record() { + // THE asymmetry with `pack`. 25 bytes of tile data pad to 128, and both + // tiles' `DataSize` must still be their own exact byte counts — an AV1 + // tile's size is exact, and 103 bytes of trailing zeros handed to its + // entropy decoder is not filler, it is corruption. + let (au, bitstream) = two_groups(); + let mut dst = vec![0xCCu8; 512]; + let packed = + pack_av1(&au, &bitstream, &[template(0, 0), template(0, 1)], &mut dst).unwrap(); + assert_eq!(packed.data_size, 128); + assert_eq!(packed.tiles.iter().map(|t| t.data_size).sum::(), 25); + assert!( + dst[25..128].iter().all(|&b| b == 0), + "padding must be zeros" + ); + assert_eq!( + dst[128], 0xCC, + "past the data size the mapping is untouched" + ); + } + + #[test] + fn data_already_on_the_granule_still_gets_a_full_padding_block() { + // libavcodec's `128 - (size & 127)` never yields zero, so a 128-byte + // buffer reports 256. Reproduced verbatim rather than "fixed". + let au: Vec = (0..256u32).map(|i| i as u8).collect(); + let bitstream = Av1Bitstream { + tiles: ranges([(0, 128)]), + groups: ranges([(0, 128)]), + }; + let mut dst = vec![0u8; 512]; + let packed = pack_av1(&au, &bitstream, &[template(0, 0)], &mut dst).unwrap(); + assert_eq!(packed.data_size, 256); + // `TileAv1` is `#[repr(packed)]`: read the field out before comparing it. + let size = packed.tiles[0].data_size; + assert_eq!(size, 128); + } + + #[test] + fn padding_is_clamped_to_what_the_mapping_can_hold() { + let (au, bitstream) = two_groups(); + let mut dst = vec![0u8; 30]; + let packed = + pack_av1(&au, &bitstream, &[template(0, 0), template(0, 1)], &mut dst).unwrap(); + assert_eq!(packed.data_size, 30); + } + + #[test] + fn an_au_larger_than_the_mapping_is_refused_rather_than_truncated() { + let (au, bitstream) = two_groups(); + let mut dst = vec![0u8; 16]; + assert_eq!( + pack_av1(&au, &bitstream, &[template(0, 0), template(0, 1)], &mut dst), + Err(PackError::BufferTooSmall { + needed: 25, + capacity: 16, + }) + ); + assert_eq!(packed_size_av1(&bitstream), 25); + } + + #[test] + fn a_region_outside_the_au_is_caught_before_it_indexes() { + let au = vec![0u8; 32]; + let bitstream = Av1Bitstream { + tiles: ranges([(10, 40)]), + groups: ranges([(10, 40)]), + }; + let mut dst = vec![0u8; 512]; + assert_eq!( + pack_av1(&au, &bitstream, &[template(0, 0)], &mut dst), + Err(PackError::RangeOutsideAu { + start: 10, + end: 40, + au: 32, + }) + ); + } + + #[test] + fn a_tile_that_belongs_to_no_group_is_refused_rather_than_rebased_against_group_zero() { + // The two halves of an `Av1Bitstream` disagreeing. Nothing in the walk can + // produce this, which is exactly why it is checked rather than assumed: + // the alternative to a typed refusal is a tile record pointing at another + // tile's bytes, and a picture that decodes. + let au: Vec = (0..200u32).map(|i| i as u8).collect(); + let bitstream = Av1Bitstream { + tiles: ranges([(10, 20), (150, 160)]), + groups: ranges([(10, 20)]), + }; + let mut dst = vec![0u8; 512]; + assert_eq!( + pack_av1(&au, &bitstream, &[template(0, 0), template(0, 1)], &mut dst), + Err(PackError::TileOutsideGroup { + start: 150, + end: 160, + }) + ); + } + + #[test] + fn a_record_count_that_disagrees_with_the_tile_count_is_refused() { + let (au, bitstream) = two_groups(); + let mut dst = vec![0u8; 512]; + assert_eq!( + pack_av1(&au, &bitstream, &[template(0, 0)], &mut dst), + Err(PackError::TileCountMismatch { + records: 1, + tiles: 2, + }) + ); + } + + #[test] + fn an_empty_plan_is_refused() { + let mut dst = vec![0u8; 512]; + let empty = Av1Bitstream { + tiles: Vec::new(), + groups: Vec::new(), + }; + assert_eq!( + pack_av1(&[], &empty, &[], &mut dst), + Err(PackError::NoTiles) + ); + assert_eq!(packed_size_av1(&empty), 0); + } +} diff --git a/crates/pf-dxvadec/src/pic.rs b/crates/pf-dxvadec/src/pic.rs new file mode 100644 index 00000000..6546c0cd --- /dev/null +++ b/crates/pf-dxvadec/src/pic.rs @@ -0,0 +1,1127 @@ +//! Per-AU H.264 conversion: one [`AuPlan`] into the `DXVA_PicParams_H264`, +//! `DXVA_Qmatrix_H264` and slice-control records an +//! `ID3D11VideoContext::SubmitDecoderBuffers` call is built from — +//! [`pf_vkdecode::pic`]'s job, one hardware API over. +//! +//! # Surfaces ARE slots +//! +//! Vulkan's picture pool is deliberately decoupled from its DPB slots (a +//! re-activated slot binds a fresh image). DXVA has no such indirection: a +//! `DXVA_PicEntry_H264` carries the **uncompressed surface index**, so the DPB +//! slot index and the decode texture array's `ArraySlice` are the same number by +//! construction. That is why this module can drive the very same +//! [`SlotMap`](pf_vkdecode::SlotMap) the Vulkan rung uses without adapting it: the +//! ledger's contract — a picture keeps its slot for exactly as long as +//! pf-bitstream's DPB holds the picture — is precisely DXVA's surface lifetime +//! too. +//! +//! # `RefFrameList` is the MARKED DPB, not the AU's reference set +//! +//! The DXVA specification asks for every picture currently marked "used for +//! reference" to appear in `RefFrameList`, and `UsedForReferenceFlags` is a +//! statement about the DPB rather than about this access unit. libavcodec's DXVA +//! path fills it by walking its whole DPB — `short_ref` then `long_ref` — never +//! the derived lists. +//! +//! An earlier revision of this module put the union of the AU's own derived +//! reference lists there, on the strength of the native Vulkan rung binding the +//! same set bit-exact. That argument does not transfer, because the two APIs +//! define the field differently: Vulkan's `pReferenceSlots` is spec-defined as the +//! slots THIS decode operation uses, so a subset is right there. Where the +//! difference bites is not list derivation — the omitted pictures are the tail of +//! the sorted initial list, so 8.2.4.2/8.2.4.3 reproduce the same final list +//! either way, which is exactly why a smoke test cannot see it — but the +//! long-term/RFI class: a long-term reference held across pictures that name none +//! of them would vanish from `RefFrameList` for those AUs and reappear later, and +//! a driver keeping internal per-reference state is entitled to read the absence +//! as "no longer a reference", discard it, and decode the recovery against +//! whatever the surface then holds. +//! +//! So the array is built from [`pf_bitstream::h264::AuPlan::dpb_refs`], the marked +//! DPB pf-bitstream captures at begin-picture time, with the AU's own references +//! FIRST and the rest of the marked DPB appended. The order is this module's +//! choice, not the spec's — DXVA imposes none (a driver resolves an entry by its +//! `FrameNumList`/`FieldOrderCntList` pair) and libavcodec emits a different one, +//! so a byte-diff against libavcodec must compare this array as a SET. What the +//! ordering buys: a DPB deeper than the sixteen-entry array can only ever lose a +//! picture no slice of this AU names. +//! +//! The snapshot is also the AUTHORITY for each entry's marking, POC pair and +//! `FrameNumList` key, in preference to the slice lists' copy of them. That +//! matters under concealment: pf-bitstream substitutes a lost reference in place +//! and relabels the substitute short-term with its own `frame_num`, so a picture +//! the DPB holds long-term can appear short-term in a list. Feeding a driver an +//! `AssociatedFlag` of 0 with a `LongTermFrameIdx` in the `FrameNum` slot (or the +//! reverse) is how `LongTermPicNum` matching resolves to the wrong surface. +//! +//! # Progressive envelope +//! +//! pf-bitstream rejects interlaced streams before a plan exists, so +//! `field_pic_flag`, `MbaffFrameFlag`, `CurrPic.AssociatedFlag` and the +//! bottom-field halves of `UsedForReferenceFlags` are written for a frame and +//! nothing else. The top/bottom PicOrderCnt PAIRS are still real pairs — a +//! progressive frame's bottom count differs from its top whenever the PPS carries +//! `bottom_field_pic_order_in_frame_present_flag` — and ride through from +//! pf-bitstream verbatim. + +use std::ops::Range; + +use pf_bitstream::h264::AuPlan; +use pf_bitstream::h264::PicId; +use pf_bitstream::h264::RefPic; +use pf_vkdecode::SlotError; +use pf_vkdecode::SlotMap; +use tracing::trace; + +use crate::dxva::H264BitFields; +use crate::dxva::PicEntry; +use crate::dxva::PicParamsH264; +use crate::dxva::QmatrixH264; +use crate::dxva::SliceH264Short; + +/// `RefFrameList`'s length — the H.264 DXVA reference-frame ceiling, and the +/// spec's own maximum reference count. +const REF_FRAME_LIST_LEN: usize = 16; + +/// One entry of `RefFrameList`: a picture the DPB holds marked for reference, +/// resolved to its surface index. Kept alongside the packed picture parameters for +/// the backend's logging and for tests that assert the mapping rather than the +/// bytes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DxvaRef { + /// The DPB slot, which is the decode texture array's `ArraySlice`. + pub slot: u8, + pub id: PicId, + pub is_long_term: bool, + /// The stored picture's 8.2.1 field order counts — `FieldOrderCntList[i]`. + pub top_field_order_cnt: i32, + pub bottom_field_order_cnt: i32, + /// `FrameNumList[i]`: `frame_num` for a short-term reference, + /// `LongTermFrameIdx` for a long-term one. + pub frame_num_or_lt_idx: u16, +} + +impl DxvaRef { + fn new(slot: u8, rp: &RefPic) -> DxvaRef { + DxvaRef { + slot, + id: rp.id, + is_long_term: rp.is_long_term, + top_field_order_cnt: rp.top_field_order_cnt, + bottom_field_order_cnt: rp.bottom_field_order_cnt, + frame_num_or_lt_idx: rp.frame_num_or_lt_idx, + } + } +} + +/// Everything CPU-derivable of one AU's DXVA submission. The Windows layer adds +/// the live objects: the decoder, the mapped buffers and the output view. +#[derive(Debug, Clone, PartialEq)] +pub struct DecodePlanDxva { + pub pic_params: PicParamsH264, + pub qmatrix: QmatrixH264, + /// Byte ranges of the AU's slice NALUs, start code included, in plan order — + /// exactly what [`crate::pack::pack`] takes. + pub slice_ranges: Vec>, + /// The surface the decoded picture is written into + /// (`CreateVideoDecoderOutputView` over this array slice, and + /// `DecoderBeginFrame`'s target). + pub setup_slot: u8, + /// The planner id of the decoded picture (`AuPlan.dpb.stored`). + pub setup_id: PicId, + /// Whether later AUs may reference the decoded picture. When `false` the + /// surface exists for the decode itself plus any remaining DPB residency, + /// and may already have been released by this very AU's `removed`. + pub setup_is_reference: bool, + /// The marked DPB, resolved to surfaces — the AU's own references first, then + /// every other marked picture (module docs). Laid out in exactly this order in + /// `pic_params.RefFrameList`. + pub refs: Vec, + /// `MbWidth * MbHeight` of the coded picture: what libavcodec writes into the + /// `NumMBsInBuffer` of the BITSTREAM and SLICE_CONTROL buffer descriptors on + /// the H.264 path (`commit_bitstream_and_slice_buffer`, both slice formats). + /// + /// The picture parameters already carry the same two numbers, so this is a + /// convenience rather than new information — but the Windows layer sees only + /// the packed bytes, and a descriptor field libavcodec fills is not a field to + /// leave at zero on the strength of its being redundant. A driver that + /// validates the descriptor rejects at the first `SubmitDecoderBuffers`, which + /// is precisely the failure this backend's "reproduce libavcodec exactly" + /// method exists to avoid. + pub mb_count: u32, +} + +/// Conversion failures. Stream DAMAGE never lands here — pf-bitstream degrades it +/// to [`pf_bitstream::h264::PlanWarning`]s upstream; these are caller bugs, or +/// features outside what this backend submits. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanToDxvaError { + /// The plan holds no slices; there is nothing to submit. + NoSlices, + /// The plan's `DpbUpdate.stored` is `None`. `plan_au` always stores; only + /// `flush()` produces such updates, and those go to + /// [`SlotMap::apply`](pf_vkdecode::SlotMap::apply) directly. + NoStoredId, + /// A reference list entry's id holds no slot: an earlier plan of this stream + /// never went through this [`SlotMap`]. + UnresolvedReference(PicId), + Slot(SlotError), + /// The map was built for a different DPB depth than this plan's + /// `max_dpb_frames` — an SPS renegotiation resized the DPB. The session must + /// rebuild the decoder, its surface pool and its [`SlotMap`]; converting + /// against the stale map would hand out surface indices the pool does not + /// have. + CapacityMismatch { + required: usize, + capacity: usize, + }, + /// The AU references more distinct pictures than `RefFrameList` holds. + /// Unreachable from a spec-conformant stream (16 is the H.264 ceiling too), + /// and an error rather than a truncation because a dropped reference decodes + /// to a wrong picture instead of a missing one. + TooManyReferences(usize), + /// Flexible macroblock ordering. `SliceGroupMap` would have to carry a + /// derived map this backend does not build; punktfunk hosts never emit FMO, + /// and libavcodec's own DXVA path does not implement it either. Refusing + /// hands the session to the FFmpeg rung with a clean stream. + SliceGroups { + count: u32, + }, + /// `separate_colour_plane_flag` (4:4:4 with independently coded planes). + /// pf-bitstream's envelope gate refuses it upstream; checked again here + /// because the picture-parameters layout for it is a different shape + /// entirely. + SeparateColourPlanes, + /// A picture dimension in macroblocks exceeds the `USHORT` the picture + /// parameters carry — 65536 macroblocks a side, i.e. a megapixel-scale + /// impossibility off a real SPS. + DimensionOverflow { + width_mbs: u32, + height_mbs: u32, + }, +} + +impl std::fmt::Display for PlanToDxvaError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlanToDxvaError::NoSlices => write!(f, "the plan holds no slices"), + PlanToDxvaError::NoStoredId => write!( + f, + "the plan stores no picture (flush updates go to SlotMap::apply)" + ), + PlanToDxvaError::UnresolvedReference(id) => { + write!(f, "referenced picture {id} holds no DPB slot in this map") + } + PlanToDxvaError::Slot(err) => write!(f, "slot assignment failed: {err}"), + PlanToDxvaError::CapacityMismatch { required, capacity } => write!( + f, + "the plan needs {required} slots but the map holds {capacity} — \ + an SPS renegotiation resized the DPB; rebuild decoder and map" + ), + PlanToDxvaError::TooManyReferences(count) => { + write!(f, "{count} references exceed DXVA's RefFrameList of 16") + } + PlanToDxvaError::SliceGroups { count } => write!( + f, + "the PPS codes {count} slice groups (FMO), which this backend does not submit" + ), + PlanToDxvaError::SeparateColourPlanes => { + write!(f, "separate_colour_plane_flag is outside this backend") + } + PlanToDxvaError::DimensionOverflow { + width_mbs, + height_mbs, + } => write!( + f, + "a {width_mbs}x{height_mbs}-macroblock picture exceeds the DXVA picture parameters" + ), + } + } +} + +impl std::error::Error for PlanToDxvaError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + PlanToDxvaError::Slot(err) => Some(err), + _ => None, + } + } +} + +impl From for PlanToDxvaError { + fn from(err: SlotError) -> Self { + PlanToDxvaError::Slot(err) + } +} + +/// Convert one planned AU, driving `slots` through the AU's slot lifecycle. +/// +/// `status_id` becomes `StatusReportFeedbackNumber` — a per-picture tag the +/// driver echoes in its status reports. libavcodec makes it a monotonic counter +/// starting at 1; the caller does the same, because 0 is the value a driver reads +/// out of a buffer nobody wrote. +/// +/// Atomicity contract, identical to [`pf_vkdecode::plan_to_vk`]'s: every fallible +/// step runs before any mutation of `slots`, so an error leaves the map exactly +/// as it was. In order: +/// 1. envelope and capacity are validated (read-only); +/// 2. references resolve against the PRE-removal state (read-only) — this AU's +/// own end-of-picture marking can evict a picture its slices legitimately +/// reference; +/// 3. `removed` is applied, then the setup slot is assigned last. +pub fn plan_to_dxva( + plan: &AuPlan, + slots: &mut SlotMap, + status_id: u32, +) -> Result { + let first_slice = plan.slices.first().ok_or(PlanToDxvaError::NoSlices)?; + let setup_id = plan.dpb.stored.ok_or(PlanToDxvaError::NoStoredId)?; + let sps = &plan.sps; + let pps = &plan.pps; + let pic = &plan.picture; + + if pps.num_slice_groups_minus1 != 0 { + return Err(PlanToDxvaError::SliceGroups { + count: pps.num_slice_groups_minus1 + 1, + }); + } + if sps.separate_colour_plane_flag { + return Err(PlanToDxvaError::SeparateColourPlanes); + } + + // The map must match THIS plan's DPB depth; a mismatch means an SPS + // renegotiation resized the DPB and the decoder must be rebuilt. + let required = pic.max_dpb_frames + 1; + if slots.capacity() != required { + return Err(PlanToDxvaError::CapacityMismatch { + required, + capacity: slots.capacity(), + }); + } + + // Picture dimensions in macroblocks. Height is expressed in FRAME + // macroblocks (the DXVA spec is explicit that it is not a field count), so + // the map-units count doubles for a non-frame-only SPS — unreachable inside + // pf-bitstream's progressive envelope, written out anyway so the expression + // says what the spec says rather than what our envelope happens to allow. + let width_mbs = u32::from(sps.pic_width_in_mbs_minus1) + 1; + let height_mbs = (u32::from(sps.pic_height_in_map_units_minus1) + 1) + * (2 - u32::from(sps.frame_mbs_only_flag)); + let (Ok(width_minus1), Ok(height_minus1)) = ( + u16::try_from(width_mbs.saturating_sub(1)), + u16::try_from(height_mbs.saturating_sub(1)), + ) else { + return Err(PlanToDxvaError::DimensionOverflow { + width_mbs, + height_mbs, + }); + }; + + // `RefFrameList`: the marked DPB (module docs), the AU's own references first + // so a DPB deeper than the array can only lose a picture no slice names. + // Per-slice list ORDER (the `ref_idx` mapping) is not expressed here at all — + // it lives in the slice headers the hardware parses. + let mut refs: Vec = Vec::new(); + for slice in &plan.slices { + for rp in slice.ref_list0.iter().chain(&slice.ref_list1) { + if refs.iter().any(|existing| existing.id == rp.id) { + continue; + } + // The snapshot is the authority for the marking and the pair-key: a + // list entry may be a concealment substitute relabelled short-term + // (module docs). A list naming a picture the DPB does not hold marked + // is a planner-contract violation rather than stream damage; it cannot + // happen off 8.2.4 (every initial list is built FROM the marked DPB), + // and the entry's own copy is the honest fallback if it ever does. + let marked = plan.dpb_refs.iter().find(|d| d.id == rp.id); + if marked.is_none() { + trace!( + id = rp.id, + "a slice list names a picture the marked DPB does not hold" + ); + } + let slot = slots + .slot_of(rp.id) + .ok_or(PlanToDxvaError::UnresolvedReference(rp.id))?; + refs.push(DxvaRef::new(slot, marked.unwrap_or(rp))); + } + } + // The AU's own set is what the array MUST hold; 16 is the H.264 ceiling too, + // so exceeding it means the plan is malformed. An error rather than a + // truncation, because a dropped reference decodes to a wrong picture instead + // of a missing one. + if refs.len() > REF_FRAME_LIST_LEN { + return Err(PlanToDxvaError::TooManyReferences(refs.len())); + } + // Then the rest of the marked DPB, in the planner's DPB order. Overflow past + // the array is dropped rather than refused: these are pictures this AU does + // not reference, so the decode is unaffected and refusing would cost the + // whole picture. A slot the map never saw is skipped for the same reason — + // the AU's own references have already been resolved strictly above. + for rp in &plan.dpb_refs { + if refs.len() == REF_FRAME_LIST_LEN { + trace!( + marked = plan.dpb_refs.len(), + "the marked DPB exceeds RefFrameList; the tail is not expressible" + ); + break; + } + if refs.iter().any(|existing| existing.id == rp.id) { + continue; + } + match slots.slot_of(rp.id) { + Some(slot) => refs.push(DxvaRef::new(slot, rp)), + None => trace!(id = rp.id, "a marked DPB picture holds no slot in this map"), + } + } + + let mut pp = PicParamsH264::zeroed(); + pp.wFrameWidthInMbsMinus1 = width_minus1; + pp.wFrameHeightInMbsMinus1 = height_minus1; + pp.num_ref_frames = sps.max_num_ref_frames; + pp.bit_depth_luma_minus8 = pic.bit_depth_luma_minus8; + pp.bit_depth_chroma_minus8 = pic.bit_depth_chroma_minus8; + // Reserved, and not actually free: libavcodec writes 3 here for every + // standard profile (0 only for the legacy Intel ClearVideo GUID and for the + // old ATI zigzag workaround, neither of which this backend uses), and the + // Microsoft reference decoder does the same. Left at 3 rather than 0 so we + // are byte-identical to the path every Windows player exercises. + pp.Reserved16Bits = 3; + pp.StatusReportFeedbackNumber = status_id; + pp.CurrFieldOrderCnt = [pic.top_field_order_cnt, pic.bottom_field_order_cnt]; + pp.pic_init_qs_minus26 = pps.pic_init_qs_minus26; + pp.chroma_qp_index_offset = pps.chroma_qp_index_offset; + pp.second_chroma_qp_index_offset = pps.second_chroma_qp_index_offset; + // "The fields after ContinuationFlag are present." Always, here: this + // backend never submits the truncated form. + pp.ContinuationFlag = 1; + pp.pic_init_qp_minus26 = pps.pic_init_qp_minus26; + // The PPS defaults, NOT a slice's override: the picture parameters describe + // the parameter set, and each slice header carries its own + // `num_ref_idx_active_override_flag` for the hardware to apply. + pp.num_ref_idx_l0_active_minus1 = pps.num_ref_idx_l0_default_active_minus1; + pp.num_ref_idx_l1_active_minus1 = pps.num_ref_idx_l1_default_active_minus1; + pp.frame_num = pic.frame_num; + pp.log2_max_frame_num_minus4 = sps.log2_max_frame_num_minus4; + pp.pic_order_cnt_type = sps.pic_order_cnt_type; + // Each POC type reads exactly one of these; the other stays 0 rather than + // carrying a value the SPS never coded for this type. + if sps.pic_order_cnt_type == 0 { + pp.log2_max_pic_order_cnt_lsb_minus4 = sps.log2_max_pic_order_cnt_lsb_minus4; + } else if sps.pic_order_cnt_type == 1 { + pp.delta_pic_order_always_zero_flag = u8::from(sps.delta_pic_order_always_zero_flag); + } + pp.direct_8x8_inference_flag = u8::from(sps.direct_8x8_inference_flag); + pp.entropy_coding_mode_flag = u8::from(pps.entropy_coding_mode_flag); + pp.pic_order_present_flag = u8::from(pps.bottom_field_pic_order_in_frame_present_flag); + // num_slice_groups_minus1 / slice_group_map_type / slice_group_change_rate_minus1 + // / SliceGroupMap all stay 0: FMO was refused above. + pp.deblocking_filter_control_present_flag = + u8::from(pps.deblocking_filter_control_present_flag); + pp.redundant_pic_cnt_present_flag = u8::from(pps.redundant_pic_cnt_present_flag); + + let is_intra = plan + .slices + .iter() + .all(|slice| slice.header.slice_type.is_i() || slice.header.slice_type.is_si()); + pp.wBitFields = H264BitFields { + chroma_format_idc: pic.chroma_format_idc, + ref_pic_flag: pic.nal_ref_idc != 0, + constrained_intra_pred_flag: pps.constrained_intra_pred_flag, + weighted_pred_flag: pps.weighted_pred_flag, + weighted_bipred_idc: pps.weighted_bipred_idc, + frame_mbs_only_flag: sps.frame_mbs_only_flag, + transform_8x8_mode_flag: pps.transform_8x8_mode_flag, + // The DXVA spec defines MinLumaBipredSize8x8Flag as level_idc >= 31, + // which is the level at which 8x8 is the smallest bi-predicted luma + // block; libavcodec writes the identical comparison. + min_luma_bipred_size_8x8: pic.level_idc as u8 >= 31, + intra_pic_flag: is_intra, + } + .pack(); + + // The reference arrays: entry i describes RefFrameList[i]. Unused entries are + // 0xFF with zeroed counts, which is the padding both the spec and every + // shipping decoder use. + pp.RefFrameList = [PicEntry::UNUSED; REF_FRAME_LIST_LEN]; + for (i, r) in refs.iter().enumerate() { + pp.RefFrameList[i] = PicEntry::new(r.slot, r.is_long_term); + // Both field counts of a progressive frame; a real pair whenever the PPS + // carried bottom_field_pic_order_in_frame_present_flag. TOP first — the + // one ordering in this file no vector of ours can catch, because a + // progressive stream without that flag has the two counts equal. + pp.FieldOrderCntList[i] = [r.top_field_order_cnt, r.bottom_field_order_cnt]; + // frame_num for short-term references, LongTermFrameIdx for long-term + // ones — the pair-key DXVA identifies references by, exactly as + // pf-bitstream's DPB snapshot hands it over. + pp.FrameNumList[i] = r.frame_num_or_lt_idx; + // Two bits per entry: top field at 2i, bottom at 2i+1. A progressive + // frame reference is marked for both. `i < 16` bounds the shift. + pp.UsedForReferenceFlags |= 0b11 << (2 * i); + } + // NonExistingFrameFlags stays 0: pf-bitstream substitutes for a lost + // reference and warns; it never hands over a "non-existing frame" placeholder + // for the hardware to invent a picture from. + + // The quantization matrices, in the coded (zig-zag) order both the parser + // stores and DXVA wants — see `QmatrixH264`'s docs. The PPS's lists are + // authoritative: the parser has already applied Table 7-2's fallback rules, + // so a PPS that codes no matrix already carries the SPS's (or the flat + // default). + let mut qm = QmatrixH264::zeroed(); + qm.bScalingLists4x4 = pps.scaling_lists_4x4; + // Only Intra-Y and Inter-Y travel: DXVA has two 8x8 slots because 8x8 chroma + // lists exist only in 4:4:4, which is refused above. The vendored parser + // stores the 4:2:0 pair at indices 0 and 1 (its loop runs `for i in + // 0..num_8x8` with num_8x8 = 2) — NOT at 0 and 3 the way libavcodec's own + // scaling_matrix8 is indexed. + qm.bScalingLists8x8[0] = pps.scaling_lists_8x8[0]; + qm.bScalingLists8x8[1] = pps.scaling_lists_8x8[1]; + + let slice_ranges: Vec> = plan.slices.iter().map(|s| s.data.clone()).collect(); + + // Mutations LAST, after every fallible step above (fn docs). Removals first — + // they were real regardless of this AU's fate — then the setup assignment. + // + // The AU's own picture can itself appear in `removed`: a non-reference + // picture with no free frame buffer bypasses the DPB and is stored-and- + // evicted within one plan. Its surface must still exist for the decode + // itself, so it is assigned here and released right after. + let setup_evicted = plan.dpb.removed.contains(&setup_id); + for &id in &plan.dpb.removed { + if id == setup_id { + continue; + } + if !slots.release(id) { + // Tolerated but never silent: reachable only when the caller skipped + // feeding an AU's plan through this map. + trace!(id, "DpbUpdate removed an id this SlotMap never assigned"); + } + } + let setup_slot = slots.assign(setup_id)?; + if setup_evicted { + slots.release(setup_id); + } + // Written after the assignment for the obvious reason that the surface index + // is not known until then; AssociatedFlag is the bottom-field flag, 0 by the + // progressive envelope. + pp.CurrPic = PicEntry::new(setup_slot, false); + + // `first_slice` is read for nothing but this debug aid today — the DXVA + // picture parameters name no PPS id (unlike Vulkan's Std picture info, + // which does), because the parameter set travels IN the picture parameters + // rather than being referenced by id. + debug_assert_eq!( + first_slice.header.pic_parameter_set_id, pps.pic_parameter_set_id, + "the plan's activated PPS must be the first slice's" + ); + + Ok(DecodePlanDxva { + pic_params: pp, + qmatrix: qm, + slice_ranges, + setup_slot, + setup_id, + setup_is_reference: pic.is_reference, + refs, + mb_count: width_mbs * height_mbs, + }) +} + +/// The slice-control records for a packed AU. +/// +/// Split from [`plan_to_dxva`] because the byte locations only exist after the +/// bitstream buffer is mapped and packed — the conversion is per-plan, this is +/// per-submission. +pub fn slice_control(records: &[crate::pack::SliceRecord]) -> Vec { + records + .iter() + .map(|r| SliceH264Short { + BSNALunitDataLocation: r.location, + SliceBytesInBuffer: r.bytes, + // 0 — a whole slice in one buffer; this backend never chops. + wBadSliceChopping: 0, + }) + .collect() +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + use std::rc::Rc; + + use cros_codecs::codec::h264::nalu_writer::NaluWriter; + use cros_codecs::codec::h264::parser::Nalu; + use cros_codecs::codec::h264::parser::NaluType; + use cros_codecs::codec::h264::parser::Pps; + use cros_codecs::codec::h264::parser::PpsBuilder; + use cros_codecs::codec::h264::parser::Profile; + use cros_codecs::codec::h264::parser::Sps; + use cros_codecs::codec::h264::parser::SpsBuilder; + use cros_codecs::codec::h264::synthesizer::Synthesizer; + use pf_bitstream::h264::H264Planner; + use pf_bitstream::h264::Level; + + use super::*; + + /// The same vendored vector pf-bitstream and pf-vkdecode plan (goldens: 250 + /// AUs, 500 slices), included from the same path rather than copied. + const TEST_25FPS: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264" + ); + + /// Test-only AU splitter, the same shape pf-vkdecode's `pic` tests use + /// (which in turn mirrors pf-bitstream's `#[cfg(test)]`-private helper): a + /// new AU starts at a non-slice NALU following a slice, or at a slice whose + /// `first_mb_in_slice` is 0 following a slice. + fn split_into_aus(stream: &[u8]) -> Vec<&[u8]> { + use cros_codecs::codec::h264::parser::NaluType; + let mut aus = Vec::new(); + let mut cursor = Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + + while let Ok(nalu) = Nalu::next(&mut cursor) { + let nalu_offset = cursor.position() as usize; + let start = nalu_offset - nalu.offset; + let is_slice = matches!(nalu.header.type_, NaluType::Slice | NaluType::SliceIdr); + let first_mb_zero = + is_slice && stream.get(nalu_offset + 1).is_some_and(|b| b & 0x80 != 0); + + if au_has_slice && (!is_slice || first_mb_zero) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus + } + + /// Plan the vendored stream and convert every AU, returning the plans paired + /// with their conversions. + fn convert_stream() -> Vec<(AuPlan, DecodePlanDxva)> { + let mut planner = H264Planner::new(); + let mut slots: Option = None; + let mut out = Vec::new(); + for (i, au) in split_into_aus(TEST_25FPS).into_iter().enumerate() { + let Ok(plan) = planner.plan_au(au) else { + continue; + }; + let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames)); + if map.capacity() != plan.picture.max_dpb_frames + 1 { + *map = SlotMap::new(plan.picture.max_dpb_frames); + } + let dxva = plan_to_dxva(&plan, map, i as u32 + 1).expect("conversion"); + out.push((plan, dxva)); + } + out + } + + /// A 64x64 Main-profile SPS/PPS pair, for the one shape no vendored vector + /// carries: long-term reference marking. Authored with the vendored builders + /// and synthesizer, exactly as pf-bitstream's own MMCO tests do — slice headers + /// written by hand below because upstream has no slice-header synthesizer. + fn authored_sps_pps() -> (Rc, Rc) { + let sps = SpsBuilder::new() + .seq_parameter_set_id(0) + .profile_idc(Profile::Main) + .level_idc(Level::L4) + .frame_mbs_only_flag(true) + .direct_8x8_inference_flag(true) + .max_num_ref_frames(4) + .log2_max_frame_num_minus4(0) + .pic_order_cnt_type(0) + .log2_max_pic_order_cnt_lsb_minus4(0) + .resolution(64, 64) + .build(); + let pps = PpsBuilder::new(Rc::clone(&sps)) + .pic_parameter_set_id(0) + .pic_init_qp(26) + .build(); + (sps, pps) + } + + /// One IDR slice NALU. The planner reads headers only, so no slice data + /// follows the rbsp stop bit. + fn write_idr_slice() -> Vec { + let mut buf = Vec::new(); + { + let mut w = NaluWriter::new(&mut buf, true); + w.write_header(3, NaluType::SliceIdr as u8).unwrap(); + w.write_ue(0u32).unwrap(); // first_mb_in_slice + w.write_ue(2u32).unwrap(); // slice_type: I + w.write_ue(0u32).unwrap(); // pic_parameter_set_id + w.write_f(4, 0u32).unwrap(); // frame_num, u(4) + w.write_ue(0u32).unwrap(); // idr_pic_id + w.write_f(4, 0u32).unwrap(); // pic_order_cnt_lsb, u(4) + w.write_f(1, 0u32).unwrap(); // no_output_of_prior_pics_flag + w.write_f(1, 0u32).unwrap(); // long_term_reference_flag + w.write_se(0i32).unwrap(); // slice_qp_delta + w.write_f(1, 1u32).unwrap(); // rbsp stop bit + while !w.aligned() { + w.write_f(1, 0u32).unwrap(); + } + } + buf + } + + /// One P slice NALU. `mmco_ops` = `None` for sliding-window marking, `Some` + /// for adaptive marking with `(operation, single-argument)` pairs — the writer + /// appends the terminating op 0. + fn write_p_slice( + frame_num: u32, + poc_lsb: u32, + ref_idc: u8, + num_ref_idx_l0_active: u32, + mmco_ops: Option<&[(u32, u32)]>, + ) -> Vec { + let mut buf = Vec::new(); + { + let mut w = NaluWriter::new(&mut buf, true); + w.write_header(ref_idc, NaluType::Slice as u8).unwrap(); + w.write_ue(0u32).unwrap(); // first_mb_in_slice + w.write_ue(0u32).unwrap(); // slice_type: P + w.write_ue(0u32).unwrap(); // pic_parameter_set_id + w.write_f(4, frame_num).unwrap(); // frame_num, u(4) + w.write_f(4, poc_lsb).unwrap(); // pic_order_cnt_lsb, u(4) + w.write_f(1, 1u32).unwrap(); // num_ref_idx_active_override_flag + w.write_ue(num_ref_idx_l0_active - 1).unwrap(); + w.write_f(1, 0u32).unwrap(); // ref_pic_list_modification_flag_l0 + if ref_idc != 0 { + match mmco_ops { + None => w.write_f(1, 0u32).map(|_| ()).unwrap(), + Some(ops) => { + w.write_f(1, 1u32).unwrap(); // adaptive_ref_pic_marking_mode_flag + for (op, arg) in ops { + w.write_ue(*op).unwrap(); + w.write_ue(*arg).unwrap(); + } + w.write_ue(0u32).unwrap(); // end of the MMCO list + } + } + } + w.write_se(0i32).unwrap(); // slice_qp_delta + w.write_f(1, 1u32).unwrap(); // rbsp stop bit + while !w.aligned() { + w.write_f(1, 0u32).unwrap(); + } + } + buf + } + + /// The unique pictures the AU's own slice lists name, in first-appearance + /// order — what `RefFrameList` used to hold, and what it must now merely start + /// with. + fn au_reference_ids(plan: &AuPlan) -> Vec { + let mut ids = Vec::new(); + for slice in &plan.slices { + for rp in slice.ref_list0.iter().chain(&slice.ref_list1) { + if !ids.contains(&rp.id) { + ids.push(rp.id); + } + } + } + ids + } + + #[test] + fn the_reference_list_is_the_marked_dpb_led_by_the_pictures_this_au_names() { + let converted = convert_stream(); + let mut wider_than_the_au = 0usize; + for (plan, dxva) in &converted { + let au = au_reference_ids(plan); + // The AU's own references lead, in their own order: truncation at the + // array's sixteen can then only ever drop a picture no slice names. + assert_eq!( + dxva.refs + .iter() + .take(au.len()) + .map(|r| r.id) + .collect::>(), + au + ); + // And the whole marked DPB is there — every picture the planner reports + // marked, none missing, none invented. + let mut listed: Vec = dxva.refs.iter().map(|r| r.id).collect(); + let mut marked: Vec = plan.dpb_refs.iter().map(|r| r.id).collect(); + listed.sort_unstable(); + marked.sort_unstable(); + assert_eq!(listed, marked); + if dxva.refs.len() > au.len() { + wider_than_the_au += 1; + } + } + // This vector is not a degenerate case for the change: the DPB holds a + // reference the AU's own lists do not reach on nearly half its pictures. + assert!( + wider_than_the_au >= 100, + "only {wider_than_the_au} of {} AUs exercised the difference", + converted.len() + ); + } + + #[test] + fn a_long_term_reference_no_slice_names_still_reaches_the_reference_list() { + // The RFI shape, end to end: an anchor pinned long-term that the current + // picture's (truncated) reference list never names. Built here rather than + // taken from a vector because no vendored vector carries an MMCO 6. + let (sps, pps) = authored_sps_pps(); + let mut au0 = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au0, true).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(3, &pps, &mut au0, true).unwrap(); + au0.extend(write_idr_slice()); + // AU1 pins itself long-term (MMCO 4 admits index 0, MMCO 6 assigns it). + let au1 = write_p_slice(1, 2, 1, 1, Some(&[(4, 1), (6, 0)])); + // AU2 activates ONE reference, which 8.2.4.2.1 makes the short-term IDR. + let au2 = write_p_slice(2, 4, 1, 1, None); + + let mut planner = H264Planner::new(); + let plans: Vec = [au0.as_slice(), au1.as_slice(), au2.as_slice()] + .into_iter() + .map(|au| planner.plan_au(au).expect("plan")) + .collect(); + let mut slots = SlotMap::new(plans[0].picture.max_dpb_frames); + let converted: Vec = plans + .iter() + .enumerate() + .map(|(i, plan)| plan_to_dxva(plan, &mut slots, i as u32 + 1).expect("convert")) + .collect(); + + let idr_id = plans[0].dpb.stored.unwrap(); + let lt_id = plans[1].dpb.stored.unwrap(); + assert_eq!(au_reference_ids(&plans[2]), vec![idr_id]); + + // Both pictures are in RefFrameList: the one AU2 names, and the long-term + // anchor it does not. A driver told the anchor is gone may discard it. + let dxva = &converted[2]; + assert_eq!( + dxva.refs + .iter() + .map(|r| (r.id, r.is_long_term, r.frame_num_or_lt_idx)) + .collect::>(), + vec![(idr_id, false, 0), (lt_id, true, 0)] + ); + // …with the marking and pair-key the DPB holds, not the slice list's. + assert!( + dxva.pic_params.RefFrameList[1].associated(), + "AssociatedFlag" + ); + assert_eq!(dxva.pic_params.FrameNumList[1], 0, "LongTermFrameIdx"); + assert_eq!(dxva.pic_params.UsedForReferenceFlags & 0b1111, 0b1111); + assert_eq!(dxva.pic_params.RefFrameList[2], PicEntry::UNUSED); + } + + #[test] + fn an_unequal_field_order_count_pair_rides_through_top_first() { + // Every vector this crate has is progressive with + // bottom_field_pic_order_in_frame_present_flag clear, so TopFieldOrderCnt + // == BottomFieldOrderCnt everywhere and a swapped pair is invisible across + // all 250 AUs. The pair is real whenever a PPS does carry that flag, so + // this drives one through the conversion with the two counts distinct. + let mut planner = H264Planner::new(); + let aus = split_into_aus(TEST_25FPS); + let first = planner.plan_au(aus[0]).expect("plan 0"); + let mut second = planner.plan_au(aus[1]).expect("plan 1"); + let ref_id = first.dpb.stored.unwrap(); + + for rp in second + .dpb_refs + .iter_mut() + .filter(|rp| rp.id == ref_id) + .chain( + second.slices[0] + .ref_list0 + .iter_mut() + .filter(|rp| rp.id == ref_id), + ) + { + rp.top_field_order_cnt = 4; + rp.bottom_field_order_cnt = 6; + } + + let mut slots = SlotMap::new(first.picture.max_dpb_frames); + plan_to_dxva(&first, &mut slots, 1).expect("convert 0"); + let dxva = plan_to_dxva(&second, &mut slots, 2).expect("convert 1"); + assert_eq!(dxva.refs[0].id, ref_id); + assert_eq!( + dxva.pic_params.FieldOrderCntList[0], + [4, 6], + "TopFieldOrderCnt occupies index 0" + ); + } + + #[test] + fn the_macroblock_count_is_the_coded_picture_in_macroblocks() { + // libavcodec writes this into `NumMBsInBuffer` on the H.264 bitstream and + // slice-control descriptors (`commit_bitstream_and_slice_buffer`: + // `h->mb_width * h->mb_height`). The vendored vector is 320x240 — 20x15 + // macroblocks. + for (plan, dxva) in convert_stream() { + let width = u32::from(plan.sps.pic_width_in_mbs_minus1) + 1; + let height = (u32::from(plan.sps.pic_height_in_map_units_minus1) + 1) + * (2 - u32::from(plan.sps.frame_mbs_only_flag)); + assert_eq!(dxva.mb_count, width * height); + assert_eq!(dxva.mb_count, 20 * 15); + // The same two numbers the picture parameters carry, minus one each. + assert_eq!( + u32::from(dxva.pic_params.wFrameWidthInMbsMinus1 + 1) + * u32::from(dxva.pic_params.wFrameHeightInMbsMinus1 + 1), + dxva.mb_count + ); + } + } + + #[test] + fn the_whole_vendored_stream_converts_without_a_single_refusal() { + let converted = convert_stream(); + // pf-bitstream's own golden for this vector is 250 planned AUs; the + // conversion must not lose any of them. + assert_eq!(converted.len(), 250); + } + + #[test] + fn the_setup_surface_is_the_current_picture_entry_and_is_never_also_a_reference_entry() { + for (_, dxva) in convert_stream() { + assert_eq!(dxva.pic_params.CurrPic.index(), dxva.setup_slot); + assert!( + !dxva.pic_params.CurrPic.associated(), + "progressive: CurrPic's AssociatedFlag is the bottom-field flag" + ); + // The picture being decoded must not appear in its own reference + // list — that would be a surface read and written in one operation. + for r in &dxva.refs { + assert_ne!(r.slot, dxva.setup_slot, "a reference aliases the target"); + } + } + } + + #[test] + fn reference_entries_carry_their_pictures_frame_num_poc_pair_and_used_flags() { + for (plan, dxva) in convert_stream() { + for (i, r) in dxva.refs.iter().enumerate() { + // The planner's DPB snapshot is the authority for every one of + // these — not the slice lists' copy. + let rp = plan + .dpb_refs + .iter() + .find(|d| d.id == r.id) + .expect("every entry is a marked DPB picture"); + assert_eq!(dxva.pic_params.RefFrameList[i].index(), r.slot); + assert_eq!(dxva.pic_params.RefFrameList[i].associated(), r.is_long_term); + assert_eq!(r.is_long_term, rp.is_long_term); + assert_eq!(dxva.pic_params.FrameNumList[i], rp.frame_num_or_lt_idx); + assert_eq!( + dxva.pic_params.FieldOrderCntList[i], + [rp.top_field_order_cnt, rp.bottom_field_order_cnt] + ); + // A progressive reference is used for BOTH fields. + assert_eq!( + dxva.pic_params.UsedForReferenceFlags >> (2 * i) & 0b11, + 0b11 + ); + } + // Everything past the marked DPB is the 0xFF sentinel with a cleared + // use flag — never a stale surface index. + for i in dxva.refs.len()..REF_FRAME_LIST_LEN { + assert_eq!(dxva.pic_params.RefFrameList[i], PicEntry::UNUSED); + assert_eq!(dxva.pic_params.FrameNumList[i], 0); + assert_eq!(dxva.pic_params.FieldOrderCntList[i], [0, 0]); + assert_eq!(dxva.pic_params.UsedForReferenceFlags >> (2 * i) & 0b11, 0); + } + // pf-bitstream never emits a gap placeholder as an id. + assert_eq!(dxva.pic_params.NonExistingFrameFlags, 0); + } + } + + #[test] + fn the_idr_is_intra_and_the_inter_pictures_are_not() { + let converted = convert_stream(); + let (_, first) = &converted[0]; + assert_ne!(first.pic_params.wBitFields & (1 << 15), 0, "IDR is intra"); + assert!(first.refs.is_empty(), "an IDR references nothing"); + // The vector is IPPP…, so the second picture is inter and references the + // first. + let (_, second) = &converted[1]; + assert_eq!(second.pic_params.wBitFields & (1 << 15), 0); + assert_eq!(second.refs.len(), 1); + } + + #[test] + fn the_picture_parameters_carry_the_active_sps_and_pps_verbatim() { + let converted = convert_stream(); + let (plan, dxva) = &converted[0]; + let pp = &dxva.pic_params; + assert_eq!( + pp.wFrameWidthInMbsMinus1, plan.sps.pic_width_in_mbs_minus1, + "320-wide stream: 20 macroblocks" + ); + assert_eq!( + pp.wFrameHeightInMbsMinus1, + plan.sps.pic_height_in_map_units_minus1 + ); + assert_eq!(pp.num_ref_frames, plan.sps.max_num_ref_frames); + assert_eq!(pp.frame_num, plan.picture.frame_num); + assert_eq!( + pp.log2_max_frame_num_minus4, + plan.sps.log2_max_frame_num_minus4 + ); + assert_eq!(pp.pic_order_cnt_type, plan.sps.pic_order_cnt_type); + assert_eq!(pp.pic_init_qp_minus26, plan.pps.pic_init_qp_minus26); + assert_eq!(pp.pic_init_qs_minus26, plan.pps.pic_init_qs_minus26); + assert_eq!(pp.chroma_qp_index_offset, plan.pps.chroma_qp_index_offset); + assert_eq!( + pp.second_chroma_qp_index_offset, + plan.pps.second_chroma_qp_index_offset + ); + assert_eq!( + pp.entropy_coding_mode_flag, + u8::from(plan.pps.entropy_coding_mode_flag) + ); + assert_eq!( + pp.num_ref_idx_l0_active_minus1, + plan.pps.num_ref_idx_l0_default_active_minus1 + ); + // The invariants a driver reads before anything else. + assert_eq!(pp.ContinuationFlag, 1); + assert_eq!(pp.Reserved16Bits, 3); + assert_eq!(pp.StatusReportFeedbackNumber, 1); + // FMO is refused, so its whole descriptor block stays zero. + assert_eq!(pp.num_slice_groups_minus1, 0); + assert_eq!(pp.slice_group_map_type, 0); + assert_eq!(pp.slice_group_change_rate_minus1, 0); + assert!(pp.SliceGroupMap.iter().all(|&b| b == 0)); + } + + #[test] + fn the_quantization_matrices_are_the_parsers_lists_in_coded_order() { + let converted = convert_stream(); + let (plan, dxva) = &converted[0]; + assert_eq!(dxva.qmatrix.bScalingLists4x4, plan.pps.scaling_lists_4x4); + assert_eq!( + dxva.qmatrix.bScalingLists8x8[0], + plan.pps.scaling_lists_8x8[0] + ); + assert_eq!( + dxva.qmatrix.bScalingLists8x8[1], + plan.pps.scaling_lists_8x8[1] + ); + } + + #[test] + fn slice_ranges_ride_through_in_plan_order() { + for (plan, dxva) in convert_stream() { + assert_eq!(dxva.slice_ranges.len(), plan.slices.len()); + for (range, slice) in dxva.slice_ranges.iter().zip(&plan.slices) { + assert_eq!(*range, slice.data); + } + } + } + + #[test] + fn poc_type_specific_fields_are_written_only_for_the_type_that_codes_them() { + // The vendored stream is POC type 0, so the type-1 field must be clear + // even though the SPS has a (default) value for it. + let converted = convert_stream(); + let (plan, dxva) = &converted[0]; + assert_eq!(plan.sps.pic_order_cnt_type, 0); + assert_eq!( + dxva.pic_params.log2_max_pic_order_cnt_lsb_minus4, + plan.sps.log2_max_pic_order_cnt_lsb_minus4 + ); + assert_eq!(dxva.pic_params.delta_pic_order_always_zero_flag, 0); + } + + #[test] + fn a_capacity_mismatch_is_refused_and_leaves_the_map_untouched() { + let mut planner = H264Planner::new(); + let au = split_into_aus(TEST_25FPS).into_iter().next().unwrap(); + let plan = planner.plan_au(au).expect("plan"); + // A map sized for a different DPB depth: an SPS renegotiation. + let mut slots = SlotMap::new(plan.picture.max_dpb_frames + 1); + let before = slots.active(); + assert_eq!( + plan_to_dxva(&plan, &mut slots, 1), + Err(PlanToDxvaError::CapacityMismatch { + required: plan.picture.max_dpb_frames + 1, + capacity: plan.picture.max_dpb_frames + 2, + }) + ); + assert_eq!(slots.active(), before, "a refusal must not mutate the map"); + } + + #[test] + fn a_reference_the_map_never_saw_is_refused_and_leaves_the_map_untouched() { + // Plan two AUs but feed only the second through the map: its reference + // resolves to nothing. + let aus = split_into_aus(TEST_25FPS); + let mut planner = H264Planner::new(); + let first = planner.plan_au(aus[0]).expect("plan 0"); + let second = planner.plan_au(aus[1]).expect("plan 1"); + let mut slots = SlotMap::new(second.picture.max_dpb_frames); + assert!(!second.slices[0].ref_list0.is_empty()); + let missing = second.slices[0].ref_list0[0].id; + assert_eq!(first.dpb.stored, Some(missing)); + assert_eq!( + plan_to_dxva(&second, &mut slots, 1), + Err(PlanToDxvaError::UnresolvedReference(missing)) + ); + assert_eq!(slots.active(), 0); + } + + #[test] + fn slice_control_records_carry_the_packers_locations_verbatim() { + let records = [ + crate::pack::SliceRecord { + location: 0, + bytes: 40, + }, + crate::pack::SliceRecord { + location: 40, + bytes: 216, + }, + ]; + let control = slice_control(&records); + assert_eq!(control.len(), 2); + // Read by VALUE, in braces: `DXVA_Slice_H264_Short` is `#[repr(C, packed)]` + // (ten bytes, see `dxva.rs`'s alignment section), so a reference to one of + // its `u32` members would be unaligned and is a compile error — `assert_eq!` + // takes references to its operands. + assert_eq!({ control[0].BSNALunitDataLocation }, 0); + assert_eq!({ control[0].SliceBytesInBuffer }, 40); + assert_eq!({ control[1].BSNALunitDataLocation }, 40); + assert_eq!({ control[1].SliceBytesInBuffer }, 216); + assert!(control.iter().all(|c| { c.wBadSliceChopping } == 0)); + // …and the records reach the driver ten bytes apart, which is the fact the + // whole submission depends on: the second record's location is at byte 10, + // not 12. + let bytes = crate::dxva::slice_bytes(&control); + assert_eq!(bytes.len(), 20); + assert_eq!(&bytes[10..14], &40u32.to_le_bytes()); + } + + #[test] + fn a_slot_is_reused_only_after_its_picture_leaves_the_dpb() { + // The whole-stream churn check: no two live pictures may share a surface + // index, which for DXVA is the difference between a decode and a + // corrupted reference. + let mut planner = H264Planner::new(); + let mut slots: Option = None; + let mut live: Vec<(PicId, u8)> = Vec::new(); + for (i, au) in split_into_aus(TEST_25FPS).into_iter().enumerate() { + let Ok(plan) = planner.plan_au(au) else { + continue; + }; + let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames)); + let removed = plan.dpb.removed.clone(); + let dxva = plan_to_dxva(&plan, map, i as u32 + 1).expect("conversion"); + live.retain(|&(id, _)| !removed.contains(&id)); + assert!( + live.iter().all(|&(_, slot)| slot != dxva.setup_slot), + "AU {i} decodes into a surface a live picture still holds" + ); + live.push((dxva.setup_id, dxva.setup_slot)); + } + } +} diff --git a/crates/pf-dxvadec/src/pic_av1.rs b/crates/pf-dxvadec/src/pic_av1.rs new file mode 100644 index 00000000..d9614f48 --- /dev/null +++ b/crates/pf-dxvadec/src/pic_av1.rs @@ -0,0 +1,1537 @@ +//! One AV1 [`AuPlan`] into the DXVA structures — M7's Windows conversion. +//! +//! The layouts it fills are measured against the Windows SDK's own `dxva.h` +//! ([`crate::dxva_av1`]); this module is where the AV1 frame header's meaning is +//! mapped onto them, and where the three places DXVA disagrees with the other +//! backends are handled. +//! +//! # The reference numbering — a FOURTH convention +//! +//! This program has now written down four spellings of "which pictures does this +//! frame use": +//! +//! * Vulkan H.265: DPB **slot** indices in `RefPicSetStCurr*`; +//! * DXVA H.265: **positions into `RefPicList[]`** in identically named arrays; +//! * VAAPI H.265: membership **flags** ORed onto each DPB entry; +//! * **DXVA AV1: two arrays that mean different things at once.** +//! `frame_refs[7]` is indexed by reference NAME (`LAST`..`ALTREF`) and each entry +//! carries a **reference SLOT** (`ref_frame_idx[name]`), that reference's own +//! coded size, and that reference's own global motion; `RefFrameMapTextureIndex[8]` +//! is indexed by that same slot and holds the **surface** — it states the whole +//! reference store, the way `RefFrameList` does for the other two codecs. The +//! driver dereferences one through the other, so the slot is the only thing +//! `Index` may hold. Vulkan spells the first array's contents identically +//! (`referenceNameSlotIndices` — slot indices by name); DXVA differs from it only +//! in hanging the size and the warp off the same entry. Writing the surface into +//! `Index` is not a refusal, it is a frame predicted from whatever picture sits +//! in the slot numbered like that surface. +//! +//! # Global motion lives per reference +//! +//! Vulkan hangs one `StdVideoAV1GlobalMotion` block off the picture info, with an +//! eight-entry array inside it. DXVA puts each reference's warp parameters in that +//! reference's own `DXVA_PicEntry_AV1`. Both are indexed by reference NAME — +//! `global_motion_params()` loops `ref = LAST_FRAME..ALTREF_FRAME` — so the +//! Vulkan block is a straight copy and DXVA's per-entry read is +//! `gm_params[LAST_FRAME + name]`. Reading it by DPB SLOT instead is the exact +//! transposition that silently gives every warped reference somebody else's warp; +//! it agrees with the truth only while reference `i` happens to sit in slot `i+1`. + +use pf_bitstream::av1::coded_cdef_sec_strength; +use pf_bitstream::av1::AuPlan; +use pf_bitstream::av1::FrameType; +use pf_bitstream::av1::PicId; +use pf_bitstream::av1::NUM_REF_SLOTS; +use pf_bitstream::av1::REFS_PER_FRAME; + +use crate::dxva_av1::CdefAv1; +use crate::dxva_av1::CdefFlagsAv1; +use crate::dxva_av1::CdefStrength; +use crate::dxva_av1::CodingFlagsAv1; +use crate::dxva_av1::FilmGrainAv1; +use crate::dxva_av1::FilmGrainFlagsAv1; +use crate::dxva_av1::FormatFlagsAv1; +use crate::dxva_av1::GlobalMotionFlags; +use crate::dxva_av1::LoopFilterAv1; +use crate::dxva_av1::LoopFilterFlagsAv1; +use crate::dxva_av1::PicEntryAv1; +use crate::dxva_av1::PicParamsAv1; +use crate::dxva_av1::QuantizationAv1; +use crate::dxva_av1::QuantizationFlagsAv1; +use crate::dxva_av1::SegmentFeatureMask; +use crate::dxva_av1::SegmentationAv1; +use crate::dxva_av1::SegmentationFlagsAv1; +use crate::dxva_av1::TileAv1; +use crate::dxva_av1::TilesAv1; +use crate::dxva_av1::UNUSED_INDEX; +use crate::plan_bitstream; +use crate::Av1Bitstream; +use crate::Av1TileError; +use crate::SlotError; +use crate::SlotMap; + +/// `DXVA_PicParams_AV1::tiles` holds at most 64 column and 64 row sizes. +pub const MAX_TILE_DIM: usize = 64; + +/// As many `DXVA_Tile_AV1` records as one submission carries. +/// +/// libavcodec's `MAX_TILES`, and its refusal is the whole comment: *"too many +/// tiles, exceeding all defined levels in the AV1 spec"* — `dxva2_av1_decode_slice` +/// answers `AVERROR(ENOSYS)` past it, and its `ctx_pic->tiles` is a fixed +/// 256-entry array. The 64x64 grid [`MAX_TILE_DIM`] admits 4096, which no AV1 +/// level defines and no driver has been asked for. +pub const MAX_TILES: usize = 256; + +/// `log2_restoration_unit_size` on a frame that restores nothing. +/// +/// Not a meaningful size — every plane's `frame_restoration_type` is NONE and a +/// driver reading the field at all has nothing to apply it to. It is 8 because +/// that is what libavcodec's `dxva2_av1.c` writes, and 8 is the top of the range +/// `dxva.h` documents (6..8); the parser's own array is still zero here, whose +/// `trailing_zeros` would be 16. +const LOG2_RESTORATION_UNIT_SIZE_UNUSED: u16 = 8; + +/// `qm_y`/`qm_u`/`qm_v` on a frame that uses no quantiser matrix. +/// +/// `DXVA_PicParams_AV1::quantization` carries no `using_qmatrix` flag, so the three +/// indices have to say it themselves; `0xFF` is what libavcodec's `dxva2_av1.c` +/// writes and what `dxva.h` documents as the unused value. **Not** 0 — 0 selects a +/// real matrix. +const QM_UNUSED: u8 = 0xFF; + +/// `LAST_FRAME` (AV1 spec): the first reference NAME, and the offset between a +/// position in `ref_frame_idx` and the index the spec's per-reference arrays +/// (global motion, order hints, sign bias) use. `INTRA_FRAME` is 0. +const LAST_FRAME: usize = 1; + +/// Everything one AV1 `SubmitDecoderBuffers` call needs. +#[derive(Debug, Clone)] +pub struct DecodePlanDxvaAv1 { + pub pic_params: PicParamsAv1, + /// One record per **tile** — not per tile GROUP — in decode order across the + /// frame's tile groups, exactly as libavcodec's `dxva2_av1.c` fills + /// `ctx_pic->tiles[tile_num]` for `tile_num` in `tg_start..=tg_end`. + /// + /// `row`, `column` and `anchor_frame` are final. `DataOffset`/`DataSize` are + /// ACCESS-UNIT-relative here and are replaced outright by + /// [`mod@crate::pack_av1`], exactly as the H.264 and H.265 slice-control + /// records are rebased by [`mod@crate::pack`]. + pub tiles: Vec, + /// Where the tiles and the tile-group regions are in the access unit — what + /// the packer copies and what it rebases against. + pub bitstream: Av1Bitstream, + pub setup_slot: u8, + pub setup_id: PicId, +} + +/// Why a plan cannot be expressed as DXVA AV1 buffers. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanToDxvaAv1Error { + /// A `show_existing_frame` plan decodes nothing and has no submission. + NoDecode, + NoTiles, + /// The access unit's tile OBUs could not be walked into per-tile payloads. + Tiles(Av1TileError), + /// The frame header's tile GRID and the tiles the access unit actually carried + /// disagree — a dropped tile group, most likely, which nothing else reports. + /// Submitting anyway declares `cols * rows` tiles over a shorter buffer. + TileCountMismatch { + /// Tile-control records built from the access unit's tile-group spans. + records: usize, + /// Tiles the bitstream walk found. + walked: usize, + /// `tile_cols * tile_rows` — what the picture parameters announce. + grid: usize, + }, + /// A reference the slot map does not hold. + UnresolvedReference(PicId), + /// More tile columns or rows than the picture parameters can express. + TooManyTiles { + cols: u32, + rows: u32, + }, + /// A field wider than its DXVA type. + FieldOverflow { + field: &'static str, + value: u32, + }, + Slot(SlotError), +} + +impl From for PlanToDxvaAv1Error { + fn from(e: SlotError) -> Self { + PlanToDxvaAv1Error::Slot(e) + } +} + +impl std::fmt::Display for PlanToDxvaAv1Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlanToDxvaAv1Error::NoDecode => { + write!(f, "a show_existing_frame plan has no decode submission") + } + PlanToDxvaAv1Error::NoTiles => write!(f, "the frame carried no tile group"), + PlanToDxvaAv1Error::Tiles(e) => write!(f, "tile walk: {e}"), + PlanToDxvaAv1Error::TileCountMismatch { + records, + walked, + grid, + } => write!( + f, + "the frame header's tile grid is {grid} tiles; the access unit carried \ + {walked} and produced {records} records" + ), + PlanToDxvaAv1Error::UnresolvedReference(id) => { + write!(f, "reference picture {id} holds no DPB slot") + } + PlanToDxvaAv1Error::TooManyTiles { cols, rows } => write!( + f, + "{cols}x{rows} tiles exceed the {MAX_TILE_DIM}-entry picture parameters" + ), + PlanToDxvaAv1Error::FieldOverflow { field, value } => { + write!(f, "{field} = {value} does not fit its DXVA field") + } + PlanToDxvaAv1Error::Slot(e) => write!(f, "DPB slot map: {e:?}"), + } + } +} + +impl std::error::Error for PlanToDxvaAv1Error {} + +fn narrow(field: &'static str, value: u32) -> Result { + u8::try_from(value).map_err(|_| PlanToDxvaAv1Error::FieldOverflow { field, value }) +} + +fn narrow16(field: &'static str, value: u32) -> Result { + u16::try_from(value).map_err(|_| PlanToDxvaAv1Error::FieldOverflow { field, value }) +} + +/// Convert one planned AV1 frame. +/// +/// `au` is the access unit `plan` was planned from: the tile-control records need +/// the per-TILE byte ranges, and finding those means walking each tile group's +/// header and its `tile_size_minus_1` fields — which is a walk over the bitstream, +/// not over the plan. (The H.264 and H.265 conversions need no such thing: a slice +/// NALU's range IS what the driver reads.) +/// +/// ⚠ There is no `status_id` parameter, unlike the H.264 and H.265 conversions: +/// `StatusReportFeedbackNumber` is left **zero** for AV1 (see where it is filled +/// below), so a caller passing one would be handing over a number that goes +/// nowhere. Nothing mutates `slots` until every fallible step has passed. +pub fn plan_to_dxva_av1( + au: &[u8], + plan: &AuPlan, + slots: &mut SlotMap, +) -> Result { + let setup_id = plan.dpb.stored.ok_or(PlanToDxvaAv1Error::NoDecode)?; + if plan.tiles.is_empty() { + return Err(PlanToDxvaAv1Error::NoTiles); + } + let h = &*plan.header; + let seq = &*plan.sequence; + + // --- resolve, before any mutation ------------------------------------ + // The reference store, by SLOT. This is `RefFrameMapTextureIndex`, and it is a + // statement about the whole store — the same thing `RefFrameList` is for the + // other two codecs, and the reason an LTR no slice names still has to appear. + let mut ref_frame_map = [UNUSED_INDEX; NUM_REF_SLOTS]; + for r in &plan.dpb_refs { + let slot = slots + .slot_of(r.id) + .ok_or(PlanToDxvaAv1Error::UnresolvedReference(r.id))?; + ref_frame_map[usize::from(r.slot)] = slot; + } + + // The seven reference NAMES. Each carries a reference SLOT, that reference's + // own coded size, and that reference's own global motion (module docs). + // + // `plan.refs` is indexed BY NAME and a lost reference leaves a hole, so the + // name comes off the iterator and holes are skipped — they keep DXVA's + // `UNUSED_INDEX`. A compacted list (which is what this loop used to receive) + // renamed every reference after the first loss. + let mut frame_refs = [PicEntryAv1::zeroed(); REFS_PER_FRAME]; + let inter = !matches!( + h.frame_type, + FrameType::KeyFrame | FrameType::IntraOnlyFrame + ); + if inter { + for (name, r) in plan.refs.iter().enumerate() { + let Some(r) = r else { continue }; + // The reference must still be in the store — this rung's ledger has to + // hold a surface for it, or `ref_frame_map` above named nothing at + // `r.slot` and the driver would follow `Index` to an empty entry. + slots + .slot_of(r.id) + .ok_or(PlanToDxvaAv1Error::UnresolvedReference(r.id))?; + // ⚠ Global motion is indexed by reference NAME, never by DPB slot. + // AV1's `global_motion_params()` loops `ref = LAST_FRAME..ALTREF_FRAME` + // and the vendored parser stores it that way; libavcodec's + // `dxva2_av1.c` reads `gm_params[AV1_REF_FRAME_LAST + i]` for + // `frame_refs[i]`. Reading by slot instead happens to agree only while + // reference `i` sits in slot `i + 1`, and silently hands every warped + // reference somebody else's warp the moment it does not. + let gm_name = LAST_FRAME + name; + let gm = &h.global_motion_params; + frame_refs[name] = PicEntryAv1 { + // ⚠ The REFERENCE's own size, never this frame's. libavcodec: + // `pp->frame_refs[i].width = ref_frame->width` off the reference's + // `AVFrame`. AV1 lets every frame pick its own size up to the + // sequence maximum, and these two fields are how the driver knows + // to SCALE motion out of a differently-sized reference (7.11.3.3 + // `xStep`/`yStep` are computed from `RefUpscaledWidth[refIdx]`). + // Sending the current frame's size makes every scaled prediction + // read as unscaled, and agrees with the truth only while nothing + // resizes. + width: r.state.upscaled_width, + height: r.state.frame_height, + wmmat: gm.gm_params[gm_name], + global_motion_flags: GlobalMotionFlags { + // `warp_valid` is the parser's `setup_shear` verdict — a warp + // whose shear parameters are out of range is unusable, and + // DXVA's flag is the inverse. + wminvalid: !gm.warp_valid[gm_name], + wmtype: gm.gm_type[gm_name] as u8, + } + .pack(), + // ⚠⚠ The AV1 reference SLOT — `ref_frame_idx[name]`, 0..8 — and NOT + // the surface index. `Index` is a subscript INTO + // `RefFrameMapTextureIndex`, which the loop above already filled by + // slot, so the driver resolves the surface itself. libavcodec: + // `pp->frame_refs[i].Index = ref_frame ? ref_idx : 0xFF` with + // `ref_idx = frame_header->ref_frame_idx[i]`; Chromium's + // `d3d11_av1_accelerator.cc` writes the same thing. `RefPic::slot` + // IS that index (`Av1Planner` reads the store at + // `ref_frame_idx[name]` and the entry carries the slot it sits in). + index: r.slot, + reserved16: 0, + }; + } + } + + // --- tiles ------------------------------------------------------------ + let t = &h.tile_info; + if t.tile_cols as usize > MAX_TILE_DIM || t.tile_rows as usize > MAX_TILE_DIM { + return Err(PlanToDxvaAv1Error::TooManyTiles { + cols: t.tile_cols, + rows: t.tile_rows, + }); + } + let mut tiles = TilesAv1::zeroed(); + tiles.cols = narrow("tiles.cols", t.tile_cols)?; + tiles.rows = narrow("tiles.rows", t.tile_rows)?; + tiles.context_update_id = t.context_update_tile_id as u16; + // `widths`/`heights` are each tile's size in SUPERBLOCKS — a count, where the + // parser (and the AV1 syntax) records `*_in_sbs_minus_1`. ⚠ The `+ 1` is the + // whole of it: libavcodec's `dxva2_av1.c` writes + // `pp->tiles.widths[i] = frame_header->width_in_sbs_minus_1[i] + 1`, and + // Chromium's `d3d11_av1_accelerator.cc` independently writes a count too. + // Sending the coded minus-one value understates EVERY tile by one superblock, + // on every frame — the vendored vector is five superblocks wide in one tile + // and would have told the driver four. + for i in 0..t.tile_cols as usize { + tiles.widths[i] = narrow16("tiles.widths", t.width_in_sbs_minus_1[i].saturating_add(1))?; + } + for i in 0..t.tile_rows as usize { + tiles.heights[i] = narrow16( + "tiles.heights", + t.height_in_sbs_minus_1[i].saturating_add(1), + )?; + } + + // The tile records. ONE PER TILE — `dxva2_av1.c` sizes its array + // `tile_cols * tile_rows` and fills it `for (tile_num = h->tg_start; tile_num + // <= h->tg_end; tile_num++)`, so a frame whose four tiles arrive in a single + // tile group is four records with four different `row`/`column` pairs. One + // record per tile GROUP pointing at the whole OBU is not a coarser version of + // this: it hands the driver the OBU header and the tile-group header as + // entropy-coded tile data. + // + // The BYTES come from the walk (`plan_bitstream`, shared with the Vulkan rung) + // and the tile NUMBERING comes from the plan's own tile-group spans, which is + // how libav numbers them. + // + // ⚠ The cross-check that matters is against the tile GRID, not between those + // two: both are computed from the same `tg_start`/`tg_end` pair, so comparing + // them is comparing an expression with itself. `tile_cols * tile_rows` is an + // independent statement — it comes from the frame header, it is what + // `pic_params.tiles.cols`/`rows` announce to the driver, and it is exactly + // libavcodec's own guard (`ctx_pic->tile_count = frame_header->tile_cols * + // frame_header->tile_rows; if (ctx_pic->tile_count > MAX_TILES) return + // AVERROR(ENOSYS)`). + // + // The failure it catches is a DROPPED TILE GROUP: an access unit that lost one + // in transit carries no `TruncatedAu` warning (the OBU walk simply never sees + // it), so nothing else in this rung notices — and the submission then declares + // a grid the tile-control buffer has too few records for, which is a driver + // reading past `DataSize`. + let cols = t.tile_cols.max(1); + let rows = t.tile_rows.max(1); + let grid = (cols as usize).saturating_mul(rows as usize); + if grid > MAX_TILES { + return Err(PlanToDxvaAv1Error::Tiles(Av1TileError::TooManyTiles { + tiles: grid, + })); + } + let bitstream = plan_bitstream(au, &plan.tiles, h).map_err(PlanToDxvaAv1Error::Tiles)?; + let mut tile_records = Vec::with_capacity(bitstream.tiles.len()); + for tg in &plan.tiles { + // A group whose end precedes its start is malformed; the walk refuses it + // too, so this saturates rather than growing a second refusal path. + let count = tg.tg_end.saturating_sub(tg.tg_start).saturating_add(1); + for step in 0..count { + let tile_num = tg.tg_start.saturating_add(step); + tile_records.push(TileAv1 { + // Filled from the walk below, in ACCESS-UNIT coordinates; + // `pack_av1` then replaces both fields with buffer-relative ones + // (field docs). + data_offset: 0, + data_size: 0, + row: (tile_num / cols) as u16, + column: (tile_num % cols) as u16, + reserved16: 0, + // libavcodec writes `0xFF` on every tile: `anchor_frame` selects a + // reference for large-scale tile decoding, which no punktfunk + // stream and no conformance vector here uses. + anchor_frame: UNUSED_INDEX, + reserved8: 0, + }); + } + } + if tile_records.len() != grid || bitstream.tiles.len() != grid { + return Err(PlanToDxvaAv1Error::TileCountMismatch { + records: tile_records.len(), + walked: bitstream.tiles.len(), + grid, + }); + } + for (record, tile) in tile_records.iter_mut().zip(&bitstream.tiles) { + record.data_offset = + u32::try_from(tile.start).map_err(|_| PlanToDxvaAv1Error::FieldOverflow { + field: "tile.DataOffset", + value: u32::MAX, + })?; + record.data_size = u32::try_from(tile.end - tile.start).map_err(|_| { + PlanToDxvaAv1Error::FieldOverflow { + field: "tile.DataSize", + value: u32::MAX, + } + })?; + } + + // --- the blocks ------------------------------------------------------- + let lf = &h.loop_filter_params; + let mut loop_filter = LoopFilterAv1::zeroed(); + loop_filter.filter_level = [lf.loop_filter_level[0], lf.loop_filter_level[1]]; + loop_filter.filter_level_u = lf.loop_filter_level[2]; + loop_filter.filter_level_v = lf.loop_filter_level[3]; + loop_filter.sharpness_level = lf.loop_filter_sharpness; + loop_filter.control_flags = LoopFilterFlagsAv1 { + mode_ref_delta_enabled: lf.loop_filter_delta_enabled, + mode_ref_delta_update: lf.loop_filter_delta_update, + delta_lf_multi: lf.delta_lf_multi, + delta_lf_present: lf.delta_lf_present, + } + .pack(); + loop_filter.ref_deltas = lf.loop_filter_ref_deltas; + loop_filter.mode_deltas = lf.loop_filter_mode_deltas; + loop_filter.delta_lf_res = lf.delta_lf_res; + // Loop restoration. + // + // ⚠ DXVA wants the LOG2 of the unit size where the parser records the size + // itself — and the parser records NOTHING when restoration is off. AV1 5.9.20 + // only computes `LoopRestorationSize` inside `if ( UsesLr )`, so on a frame with + // every plane's restoration type NONE the vendored parser's array is still + // `[0, 0, 0]`, and `0u16.trailing_zeros()` is **16** — a restoration unit of + // 65536 samples, in a field `dxva.h` documents as 6, 7 or 8. That is 271 of the + // vendored vector's 274 frames. + // + // libavcodec's `dxva2_av1.c` sends `uses_lr ? 6 + lr_unit_shift : 8` for luma + // and `uses_lr ? 6 + lr_unit_shift - lr_uv_shift : 8` for the two chroma planes, + // and libavcodec is the implementation every driver was validated against, so + // the OFF value is 8 rather than 0 or 16. With restoration on, the parser's own + // `loop_restoration_size[i]` already carries the per-plane `>> lr_uv_shift`, so + // its `trailing_zeros` IS `6 + lr_unit_shift - lr_uv_shift` — the two agree + // wherever the field is read at all. + let lr = &h.loop_restoration_params; + for i in 0..3 { + loop_filter.frame_restoration_type[i] = lr.frame_restoration_type[i] as u8; + loop_filter.log2_restoration_unit_size[i] = if lr.uses_lr { + lr.loop_restoration_size[i].trailing_zeros() as u16 + } else { + LOG2_RESTORATION_UNIT_SIZE_UNUSED + }; + } + + let q = &h.quantization_params; + let mut quantization = QuantizationAv1::zeroed(); + quantization.control_flags = QuantizationFlagsAv1 { + delta_q_present: q.delta_q_present, + delta_q_res: narrow("delta_q_res", q.delta_q_res)?, + } + .pack(); + quantization.base_qindex = narrow("base_qindex", q.base_q_idx)?; + quantization.y_dc_delta_q = q.delta_q_y_dc as i8; + quantization.u_dc_delta_q = q.delta_q_u_dc as i8; + quantization.v_dc_delta_q = q.delta_q_v_dc as i8; + quantization.u_ac_delta_q = q.delta_q_u_ac as i8; + quantization.v_ac_delta_q = q.delta_q_v_ac as i8; + // ⚠ The quantiser-matrix indices need a SENTINEL when the frame uses no matrix. + // `DXVA_PicParams_AV1::quantization` has no `using_qmatrix` bit — 0xFF is the + // only way to say "none" — and the vendored parser only assigns `qm_y`/`qm_u`/ + // `qm_v` inside `if using_qmatrix`, so a frame without one carries **0**, which + // is a perfectly valid matrix index. Left alone the driver dequantizes against + // matrix 0 on every such frame, which is every frame of both vendored vectors. + // libavcodec: `pp->quantization.qm_y = frame_header->using_qmatrix ? + // frame_header->qm_y : 0xFF` (Chromium the same). + let (qm_y, qm_u, qm_v) = if q.using_qmatrix { + ( + narrow("qm_y", q.qm_y)?, + narrow("qm_u", q.qm_u)?, + narrow("qm_v", q.qm_v)?, + ) + } else { + (QM_UNUSED, QM_UNUSED, QM_UNUSED) + }; + quantization.qm_y = qm_y; + quantization.qm_u = qm_u; + quantization.qm_v = qm_v; + + let c = &h.cdef_params; + let mut cdef = CdefAv1::zeroed(); + cdef.control_flags = CdefFlagsAv1 { + damping: narrow("cdef_damping", c.cdef_damping.saturating_sub(3))?, + bits: narrow("cdef_bits", c.cdef_bits)?, + } + .pack(); + // Two fields to a byte (module docs) — not the parallel arrays AV1's syntax + // and Vulkan's Std block use. + // + // ⚠ `secondary` gets TWO bits here, and the parser's value does not fit them: + // AV1 5.9.19 rewrites the syntax element in place (a coded 3 becomes 4) and + // cros-codecs follows the spec, while `DXVA_PicParams_AV1` — like VA-API, + // NVDEC and Vulkan — wants the coded two-bit read, which is what libavcodec's + // `dxva2_av1.c` sends. Passing the parser's 4 through `pack`'s `& 0x3` would + // turn the STRONGEST secondary filter into NO filter, silently, on every frame + // that codes one. `coded_cdef_sec_strength` is the inverse; its docs carry the + // evidence. + for i in 0..8 { + cdef.y_strengths[i] = CdefStrength { + primary: c.cdef_y_pri_strength[i] as u8, + secondary: coded_cdef_sec_strength(c.cdef_y_sec_strength[i]), + } + .pack(); + cdef.uv_strengths[i] = CdefStrength { + primary: c.cdef_uv_pri_strength[i] as u8, + secondary: coded_cdef_sec_strength(c.cdef_uv_sec_strength[i]), + } + .pack(); + } + + let s = &h.segmentation_params; + let mut segmentation = SegmentationAv1::zeroed(); + segmentation.control_flags = SegmentationFlagsAv1 { + enabled: s.segmentation_enabled, + update_map: s.segmentation_update_map, + update_data: s.segmentation_update_data, + temporal_update: s.segmentation_temporal_update, + } + .pack(); + for seg in 0..8 { + let e = &s.feature_enabled[seg]; + segmentation.feature_mask[seg] = SegmentFeatureMask { + alt_q: e[0], + alt_lf_y_v: e[1], + alt_lf_y_h: e[2], + alt_lf_u: e[3], + alt_lf_v: e[4], + ref_frame: e[5], + skip: e[6], + globalmv: e[7], + } + .pack(); + segmentation.feature_data[seg] = s.feature_data[seg]; + } + + // Film grain: only where the sequence enables it AND the frame applies it — + // the same gate the Vulkan conversion uses, and for the same reason. + let fg_on = seq.film_grain_params_present && h.film_grain_params.apply_grain; + let mut film_grain = FilmGrainAv1::zeroed(); + if fg_on { + let fg = &h.film_grain_params; + film_grain.control_flags = FilmGrainFlagsAv1 { + apply_grain: true, + scaling_shift_minus8: fg.grain_scaling_minus_8, + chroma_scaling_from_luma: fg.chroma_scaling_from_luma, + ar_coeff_lag: narrow("ar_coeff_lag", fg.ar_coeff_lag)?, + ar_coeff_shift_minus6: fg.ar_coeff_shift_minus_6, + grain_scale_shift: fg.grain_scale_shift, + overlap_flag: fg.overlap_flag, + clip_to_restricted_range: fg.clip_to_restricted_range, + matrix_coeff_is_identity: seq.color_config.matrix_coefficients as u32 == 0, + } + .pack(); + film_grain.grain_seed = fg.grain_seed; + // ⚠ DXVA wants [value, scaling] PAIRS where the parser (and Vulkan) keep + // two parallel arrays. The counts are bounded by the DXVA capacity, and an + // over-count is refused rather than truncated: fewer scaling points than + // the stream declared is different grain, not less grain. + let pts = |name: &'static str, n: u8, cap: usize| -> Result { + if usize::from(n) > cap { + return Err(PlanToDxvaAv1Error::FieldOverflow { + field: name, + value: u32::from(n), + }); + } + Ok(usize::from(n)) + }; + let ny = pts( + "num_y_points", + fg.num_y_points, + film_grain.scaling_points_y.len(), + )?; + let ncb = pts( + "num_cb_points", + fg.num_cb_points, + film_grain.scaling_points_cb.len(), + )?; + let ncr = pts( + "num_cr_points", + fg.num_cr_points, + film_grain.scaling_points_cr.len(), + )?; + for i in 0..ny { + film_grain.scaling_points_y[i] = [fg.point_y_value[i], fg.point_y_scaling[i]]; + } + for i in 0..ncb { + film_grain.scaling_points_cb[i] = [fg.point_cb_value[i], fg.point_cb_scaling[i]]; + } + for i in 0..ncr { + film_grain.scaling_points_cr[i] = [fg.point_cr_value[i], fg.point_cr_scaling[i]]; + } + film_grain.num_y_points = fg.num_y_points; + film_grain.num_cb_points = fg.num_cb_points; + film_grain.num_cr_points = fg.num_cr_points; + film_grain + .ar_coeffs_y + .copy_from_slice(&fg.ar_coeffs_y_plus_128[..24]); + film_grain + .ar_coeffs_cb + .copy_from_slice(&fg.ar_coeffs_cb_plus_128[..25]); + film_grain + .ar_coeffs_cr + .copy_from_slice(&fg.ar_coeffs_cr_plus_128[..25]); + film_grain.cb_mult = fg.cb_mult; + film_grain.cb_luma_mult = fg.cb_luma_mult; + film_grain.cr_mult = fg.cr_mult; + film_grain.cr_luma_mult = fg.cr_luma_mult; + film_grain.cb_offset = fg.cb_offset as i16; + film_grain.cr_offset = fg.cr_offset as i16; + } + + // --- mutations, after every fallible step ----------------------------- + for &id in &plan.dpb.removed { + if id == setup_id { + continue; + } + let _ = slots.release(id); + } + let setup_slot = match slots.slot_of(setup_id) { + Some(existing) => existing, + None => slots.assign(setup_id)?, + }; + + let color = &seq.color_config; + let mut pic_params = PicParamsAv1::zeroed(); + pic_params.width = h.upscaled_width; + pic_params.height = h.frame_height; + pic_params.max_width = u32::from(seq.max_frame_width_minus_1) + 1; + pic_params.max_height = u32::from(seq.max_frame_height_minus_1) + 1; + pic_params.curr_pic_texture_index = setup_slot; + // The superres denominator as DXVA wants it: the real one, not the coded one, + // and SUPERRES_NUM when superres is off. + pic_params.superres_denom = if h.use_superres { + narrow("superres_denom", h.superres_denom)? + } else { + SUPERRES_NUM + }; + pic_params.bitdepth = if color.high_bitdepth { + if color.twelve_bit { + 12 + } else { + 10 + } + } else { + 8 + }; + pic_params.seq_profile = seq.seq_profile as u8; + pic_params.tiles = tiles; + pic_params.coding = CodingFlagsAv1 { + use_128x128_superblock: seq.use_128x128_superblock, + intra_edge_filter: seq.enable_intra_edge_filter, + interintra_compound: seq.enable_interintra_compound, + masked_compound: seq.enable_masked_compound, + warped_motion: h.allow_warped_motion, + dual_filter: seq.enable_dual_filter, + jnt_comp: seq.enable_jnt_comp, + screen_content_tools: h.allow_screen_content_tools != 0, + integer_mv: h.force_integer_mv != 0, + cdef: seq.enable_cdef, + restoration: seq.enable_restoration, + film_grain: seq.film_grain_params_present, + intrabc: h.allow_intrabc, + high_precision_mv: h.allow_high_precision_mv, + switchable_motion_mode: h.is_motion_mode_switchable, + filter_intra: seq.enable_filter_intra, + disable_frame_end_update_cdf: h.disable_frame_end_update_cdf, + disable_cdf_update: h.disable_cdf_update, + reference_mode: h.reference_select, + skip_mode: h.skip_mode_present, + reduced_tx_set: h.reduced_tx_set, + superres: h.use_superres, + tx_mode: h.tx_mode as u8, + use_ref_frame_mvs: h.use_ref_frame_mvs, + enable_ref_frame_mvs: seq.enable_ref_frame_mvs, + // ⚠ A literal 1, and NOT `refresh_frame_flags != 0`. libavcodec writes + // `pp->coding.reference_frame_update = 1` unconditionally; Chromium writes + // `!(show_existing_frame && frame_type == KEY_FRAME)`, which is also 1 + // everywhere this function runs (a `show_existing_frame` unit decodes + // nothing and is refused above with `NoDecode`). So both references agree on + // the value for every frame that reaches here, and a frame refreshing no + // slot — legal AV1, and what `refresh_frame_flags != 0` would have sent 0 + // for — is not the exception either. + reference_frame_update: true, + } + .pack(); + pic_params.format = FormatFlagsAv1 { + frame_type: h.frame_type as u8, + show_frame: h.show_frame, + showable_frame: h.showable_frame, + subsampling_x: color.subsampling_x, + subsampling_y: color.subsampling_y, + mono_chrome: color.mono_chrome, + } + .pack(); + pic_params.primary_ref_frame = narrow("primary_ref_frame", h.primary_ref_frame)?; + pic_params.order_hint = narrow("order_hint", h.order_hint)?; + pic_params.order_hint_bits = if seq.enable_order_hint { + // The parser types this one signed; a negative value would be a parse bug, + // and turning it into a huge unsigned one here would hide that. + narrow( + "order_hint_bits", + u32::try_from(seq.order_hint_bits_minus_1).map_err(|_| { + PlanToDxvaAv1Error::FieldOverflow { + field: "order_hint_bits_minus_1", + value: 0, + } + })? + 1, + )? + } else { + 0 + }; + pic_params.frame_refs = frame_refs; + pic_params.ref_frame_map_texture_index = ref_frame_map; + pic_params.loop_filter = loop_filter; + pic_params.quantization = quantization; + pic_params.cdef = cdef; + pic_params.interp_filter = h.interpolation_filter as u8; + pic_params.segmentation = segmentation; + pic_params.film_grain = film_grain; + // ⚠ `StatusReportFeedbackNumber` stays ZERO — the `zeroed()` value, written + // nowhere. This is AV1-SPECIFIC: libavcodec DOES tag its H.264 and HEVC + // submissions, and `dxva2_av1.c` alone has the line commented out with the + // reason — + // + // // XXX: Setting the StatusReportFeedbackNumber breaks decoding on some + // // drivers (tested on NVIDIA 457.09) + // // Status Reporting is not used by FFmpeg, hence not providing a number + // // does not cause any issues + // //pp->StatusReportFeedbackNumber = 1 + DXVA_CONTEXT_REPORT_ID(avctx, ctx)++; + // + // Chromium's `d3d11_av1_accelerator.cc` reaches the same place from the other + // direction: "should not be equal to 0 ... but it crashes :|". Two independent + // implementations both ship the zero, so this rung ships it too — and does not + // even accept a number to drop (fn docs). + + Ok(DecodePlanDxvaAv1 { + pic_params, + tiles: tile_records, + bitstream, + setup_slot, + setup_id, + }) +} + +/// `SUPERRES_NUM` (AV1 spec): the denominator that means "no upscaling". +const SUPERRES_NUM: u8 = 8; + +#[cfg(test)] +mod tests { + use super::*; + use crate::descriptors::descriptors_av1; + use crate::descriptors::BUFFER_BITSTREAM; + use crate::descriptors::BUFFER_PICTURE_PARAMETERS; + use crate::descriptors::BUFFER_SLICE_CONTROL; + use crate::dxva::BITSTREAM_ALIGN; + use crate::pack_av1::pack_av1; + use crate::pack_av1::packed_size_av1; + use cros_codecs::bitstream_utils::IvfIterator; + use pf_bitstream::av1::Av1Planner; + + const AV1_25FPS: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1" + ); + + /// The whole vector, converted **and packed** — the closest a CPU gate gets to + /// the hardware leg, and the test that would have caught the defect this + /// module shipped with. + /// + /// The load-bearing assertion is the last one: the bytes each + /// `DXVA_Tile_AV1` addresses inside the packed buffer must equal that tile's + /// payload in the access unit. A record pointing at the whole tile-group OBU + /// satisfies every OTHER check here — it is in range, it is inside the buffer, + /// its size is consistent — and hands the driver the OBU header, the frame + /// header and the tile-group header as entropy-coded tile data. There is no + /// way to see that from the picture parameters, and no way to see it from a + /// smoke test either: it decodes, and it decodes to noise. + /// + /// ⚠ That assertion is nonetheless WEAKER than it looks, which is why the + /// tile-group ARITHMETIC is checked separately below. `pack_av1` computes a + /// record's offset as `base + (tile.start - group.start)` from the very ranges + /// this compares against, so the two sides descend from one expression: a walk + /// that mistook where a tile begins satisfies it exactly. The independent + /// statement is `tile_group_obu()`'s own accounting — every tile's payload plus + /// one `TileSizeBytes` field per tile EXCEPT THE LAST fills the group's region + /// with nothing over and nothing short — and it is a fact about the bitstream + /// rather than about the packer. + #[test] + fn the_whole_vendored_vector_packs_into_a_three_buffer_submission() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let mut dst = vec![0u8; 1 << 20]; + let mut frames = 0u32; + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("the clean vector plans") { + if plan.dpb.stored.is_none() { + continue; + } + let dx = plan_to_dxva_av1(packet, &plan, &mut slots).expect("converts"); + frames += 1; + // Poison the mapping so a record can only be "right" by pointing + // at bytes this pack actually wrote. + dst.fill(0xCC); + let packed = pack_av1(packet, &dx.bitstream, &dx.tiles, &mut dst).expect("packs"); + + assert_eq!( + packed.data_size as usize % BITSTREAM_ALIGN, + 0, + "frame {frames}: the bitstream buffer is padded to the granule" + ); + assert_eq!(packed.tiles.len(), dx.bitstream.tiles.len()); + + for (record, tile) in packed.tiles.iter().zip(&dx.bitstream.tiles) { + // `#[repr(packed)]` — copy the fields out before using them. + let (offset, size) = (record.data_offset as usize, record.data_size as usize); + assert!( + offset + size <= packed.data_size as usize, + "frame {frames}: a tile record runs past the buffer's DataSize" + ); + assert_eq!( + &dst[offset..offset + size], + &packet[tile.clone()], + "frame {frames}: the bytes a tile record addresses must BE that \ + tile's payload" + ); + // …and specifically NOT the tile group's OBU header, which is + // where the payload does not start. + assert!( + plan.tiles + .iter() + .all(|tg| tile.start != tg.data.start || tile.end != tg.data.end), + "frame {frames}: a tile record covers a whole tile-group OBU" + ); + } + + // `tile_group_obu()`'s accounting, per GROUP — the check the byte + // comparison above cannot make (fn docs). `TileSizeBytes` is only + // coded when the frame has more than one tile, so a single-tile + // group carries no size field at all and the sum is the group. + let size_bytes = + if plan.header.tile_info.tile_cols * plan.header.tile_info.tile_rows > 1 { + plan.header.tile_info.tile_size_bytes as usize + } else { + 0 + }; + for group in &dx.bitstream.groups { + let in_group: Vec<_> = dx + .bitstream + .tiles + .iter() + .filter(|t| group.start <= t.start && t.end <= group.end) + .collect(); + assert!(!in_group.is_empty(), "frame {frames}: an empty tile group"); + let payloads: usize = in_group.iter().map(|t| t.end - t.start).sum(); + assert_eq!( + payloads + (in_group.len() - 1) * size_bytes, + group.end - group.start, + "frame {frames}: the group's {} tiles plus its {} size fields \ + must account for the region EXACTLY — a short sum is a tile \ + boundary read in the wrong place, which every offset after it \ + inherits", + in_group.len(), + in_group.len() - 1 + ); + } + + let descs = descriptors_av1(&packed); + assert_eq!( + descs.iter().map(|d| d.buffer_type).collect::>(), + vec![ + BUFFER_PICTURE_PARAMETERS, + BUFFER_BITSTREAM, + BUFFER_SLICE_CONTROL, + ], + "frame {frames}: AV1 submits three buffers and never a matrix" + ); + // Only the first of these is independent of `descriptors_av1`'s own + // arithmetic — the other two would compare `packed.data_size` and + // `16 * tiles.len()` with the expressions they were built from. So + // they are asserted against the BYTES instead: what the packer wrote, + // and the record size measured out of the Windows SDK's `dxva.h`. + assert_eq!(descs[0].data_size, 912, "DXVA_PicParams_AV1, measured"); + assert_eq!( + descs[1].data_size as usize % BITSTREAM_ALIGN, + 0, + "frame {frames}: the bitstream descriptor states the PADDED size" + ); + assert!( + descs[1].data_size as usize >= packed_size_av1(&dx.bitstream), + "frame {frames}: the bitstream descriptor is at least the tile data" + ); + assert_eq!( + descs[2].data_size as usize, + size_of::() * dx.tiles.len(), + "frame {frames}: sixteen bytes per TILE" + ); + assert!(descs.iter().all(|d| d.num_mbs_in_buffer == 0)); + } + } + assert_eq!(frames, 274); + } + + /// Convert every frame of the vendored vector and check what a driver reads. + /// + /// The anti-vacuity assertions matter as much as the checks: a run that never + /// saw an inter frame, or never saw the reference store hold a picture the + /// frame does not name, would pass every check below while exercising none of + /// the code that makes them interesting. + #[test] + fn the_whole_vendored_vector_converts() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let (mut frames, mut inter, mut store_beyond_refs) = (0u32, 0u32, 0u32); + let mut gm_by_slot_would_differ = 0u32; + let mut index_by_surface_would_differ = 0u32; + let mut ref_size_would_differ = 0u32; + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("the clean vector plans") { + if plan.dpb.stored.is_none() { + continue; + } + // Would writing the SURFACE into `Index` have been visible at all? + // Only where the two numbers differ — so this is counted BEFORE the + // conversion, which is when the ledger holds what the conversion + // reads (it releases displaced pictures on its way out). + for r in plan.refs.iter().flatten() { + let surface = slots.slot_of(r.id).expect("a named reference is held"); + if surface != r.slot { + index_by_surface_would_differ += 1; + } + } + let dx = + plan_to_dxva_av1(packet, &plan, &mut slots).expect("the clean vector converts"); + frames += 1; + + // Tile records must describe TILE PAYLOAD ranges inside the access + // unit — the bytes after each tile's `tile_size_minus_1` field, + // never the whole tile-group OBU. A record covering the OBU would + // hand the driver the OBU header and the frame header as + // entropy-coded tile data. + assert_eq!(dx.tiles.len(), dx.bitstream.tiles.len()); + for (rec, range) in dx.tiles.iter().zip(&dx.bitstream.tiles) { + assert_eq!(rec.data_offset as usize, range.start); + assert_eq!(rec.data_size as usize, range.end - range.start); + assert!(range.end <= packet.len()); + // Inside its own tile-group region, which is what the packer + // rebases against. + assert!(dx + .bitstream + .groups + .iter() + .any(|g| g.start <= range.start && range.end <= g.end)); + } + for tg in &plan.tiles { + // Every tile record lies strictly INSIDE its OBU, never at its + // first byte: the OBU header alone is one or two bytes. + assert!(dx + .tiles + .iter() + .all(|rec| rec.data_offset as usize != tg.data.start)); + } + + // The store: every named slot resolves to a real surface, and any + // slot with no picture stays UNUSED. `0` is a valid surface, so a + // slot left at 0 by accident would point at a live picture. + let named = dx + .pic_params + .ref_frame_map_texture_index + .iter() + .filter(|i| **i != UNUSED_INDEX) + .count(); + assert_eq!(named, plan.dpb_refs.len()); + let referenced = plan.refs.iter().flatten().count(); + if named > referenced { + store_beyond_refs += 1; + } + + if referenced > 0 { + inter += 1; + // Every reference NAME must carry the SLOT the frame header + // named, that slot must hold a surface, that reference's own + // coded size must travel with it, and its global motion must be + // the entry the AV1 syntax codes for THAT name. + for (name, r) in plan.refs.iter().enumerate() { + let e = dx.pic_params.frame_refs[name]; + let Some(named_ref) = r else { + assert_eq!( + e.index, UNUSED_INDEX, + "an unnamed reference must stay unused, not read as \ + slot 0" + ); + continue; + }; + assert_eq!( + e.index, named_ref.slot, + "reference name {name} must carry ref_frame_idx[{name}] — \ + the SLOT — because `Index` subscripts \ + RefFrameMapTextureIndex; a surface index there predicts \ + from whatever sits in the slot of that number" + ); + assert_ne!( + dx.pic_params.ref_frame_map_texture_index[usize::from(e.index)], + UNUSED_INDEX, + "reference name {name} points at an empty slot" + ); + // The REFERENCE's own size, not this frame's — a distinction + // this vector cannot show (nothing resizes), so it is + // asserted against the planner's per-reference state rather + // than against a difference. + let (w, h) = (e.width, e.height); + assert_eq!( + (w, h), + (named_ref.state.upscaled_width, named_ref.state.frame_height), + "reference name {name} must carry its OWN coded size" + ); + if named_ref.state.upscaled_width != plan.header.upscaled_width + || named_ref.state.frame_height != plan.header.frame_height + { + ref_size_would_differ += 1; + } + let gm = &plan.header.global_motion_params; + // `PicEntryAv1` is `#[repr(packed)]`, so its fields are + // copied out before being compared — a reference to one + // may be unaligned. + let (wmmat, flags) = (e.wmmat, e.global_motion_flags); + assert_eq!( + wmmat, + gm.gm_params[LAST_FRAME + name], + "reference name {name} must carry gm_params[LAST_FRAME \ + + {name}], not the entry at its DPB slot" + ); + assert_eq!( + flags, + GlobalMotionFlags { + wminvalid: !gm.warp_valid[LAST_FRAME + name], + wmtype: gm.gm_type[LAST_FRAME + name] as u8, + } + .pack() + ); + // Would reading by DPB SLOT have given the same answer? + let slot = usize::from(named_ref.slot); + if gm.gm_params[LAST_FRAME + name] != gm.gm_params[slot] + || gm.gm_type[LAST_FRAME + name] != gm.gm_type[slot] + { + gm_by_slot_would_differ += 1; + } + } + } + assert_eq!(dx.pic_params.curr_pic_texture_index, dx.setup_slot); + + // Both native rungs take AV1's RENDER size as a display crop and + // clamp it to the decoded picture, because 5.9.6 puts no upper + // bound on `render_width_minus_1` — it is a hint, not a window. + // This vector never exercises the clamp, and saying so here is the + // point: the Vulkan rung's 250/250 bit-identical parity result + // cannot have moved when the clamp was added. + assert!( + plan.picture.render_width <= plan.picture.upscaled_width + && plan.picture.render_height <= plan.picture.frame_height, + "frame {frames}: this vector's render region fits inside the \ + decoded picture, so the display-size clamp is inert on it" + ); + } + } + + assert_eq!(frames, 274); + eprintln!("gm reads where name and slot disagree: {gm_by_slot_would_differ}"); + eprintln!( + "reference entries where the surface is not the slot: \ + {index_by_surface_would_differ}" + ); + assert!( + index_by_surface_would_differ > 0, + "no reference of this vector ever sat in a slot whose number differs from \ + its surface index, so `Index` cannot be told from a surface index here — \ + which is exactly how the surface read shipped" + ); + assert_eq!( + ref_size_would_differ, 0, + "this vector never resizes, so `frame_refs[].width` cannot be told from \ + the current frame's width by VALUE; it is pinned against \ + `RefPic::state` instead, and this counter says so rather than leaving \ + the reader to wonder" + ); + assert!( + gm_by_slot_would_differ > 0, + "reading global motion by DPB SLOT never disagreed with reading it by \ + reference NAME on this vector, so the assertions above cannot tell the \ + two apart — which is how the slot read shipped in the first place" + ); + assert!(inter > 0, "a 274-frame vector must have inter frames"); + assert!( + store_beyond_refs > 0, + "the reference store never held a picture the frame did not name — so \ + this run never exercised the difference between RefFrameMapTextureIndex \ + (the whole store) and frame_refs (what this frame uses), which is the \ + distinction the Ally X class of bug lives in" + ); + } + + /// The CHROMA deblocking levels reach `filter_level_u` / `filter_level_v`, and + /// `log2_restoration_unit_size` is never the parser's silence. + /// + /// Two halves of the same block, both measured on hardware rather than argued. + /// + /// The levels first. ⚠ The AV1 Vulkan rung's frame-0 parity leg came back `luma + /// IDENTICAL, chroma 319/38400 bytes differ` and that signature was reproduced + /// EXACTLY — count, `|delta|` histogram and the first six differing bytes with + /// their values — by decoding the vector's frame 0 with `loop_filter_level[2]` + /// and `[3]` forced to zero in the bitstream. **That divergence turned out NOT + /// to be a levels bug** (it was a freed sequence header making the driver treat + /// the frame as monochrome — `pf_vkdecode::session_av1`), so do not cite it as + /// evidence that a rung got the pair wrong. What it does establish, and what + /// keeps this test, is the SIGNATURE: frame 0 codes `[1, 7, 8, 12]`, two luma + /// levels and two chroma ones, and dropping only the chroma pair is invisible + /// to luma and to every other plane statistic. A rung that lost the pair would + /// fail Windows parity in a way nothing else here would notice, and this rung's + /// `[2]` and `[3]` reads are four characters from `[0]` and `[1]`. + /// + /// Then the restoration unit size, which is a units defect the vendored parser + /// invites: `LoopRestorationSize` is only computed inside `if ( UsesLr )` + /// (5.9.20), so the array is `[0, 0, 0]` on a frame that restores nothing and + /// `trailing_zeros` turns that into **16** — 271 of these 274 frames, in a field + /// `dxva.h` documents as 6..8. libavcodec sends 8. + #[test] + fn the_chroma_loop_filter_levels_and_the_restoration_unit_size_reach_the_driver() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let (mut frames, mut with_chroma_lf, mut with_lr) = (0u32, 0u32, 0u32); + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("the clean vector plans") { + if plan.dpb.stored.is_none() { + continue; + } + let dx = plan_to_dxva_av1(packet, &plan, &mut slots).expect("converts"); + frames += 1; + let lf = &plan.header.loop_filter_params; + // `#[repr(packed)]` — copy the block out before reading its fields. + let sent = dx.pic_params.loop_filter; + assert_eq!( + ( + sent.filter_level[0], + sent.filter_level[1], + sent.filter_level_u, + sent.filter_level_v + ), + ( + lf.loop_filter_level[0], + lf.loop_filter_level[1], + lf.loop_filter_level[2], + lf.loop_filter_level[3] + ), + "frame {frames}: DXVA splits AV1's four levels into a luma PAIR \ + plus two named chroma fields — U is index 2 and V is index 3" + ); + if lf.loop_filter_level[2] != 0 || lf.loop_filter_level[3] != 0 { + with_chroma_lf += 1; + } + if frames == 1 { + assert_eq!( + (sent.filter_level, sent.filter_level_u, sent.filter_level_v), + ([1, 7], 8, 12), + "frame 0's levels, and the pair whose loss the Vulkan rung's \ + frame-0 divergence was reproduced from" + ); + } + + let lr = &plan.header.loop_restoration_params; + let sizes = sent.log2_restoration_unit_size; + if lr.uses_lr { + with_lr += 1; + assert_eq!(lr.loop_restoration_size, [128, 128, 128]); + assert_eq!(sizes, [7, 7, 7], "6 + lr_unit_shift, per plane"); + } else { + assert_eq!( + sizes, [LOG2_RESTORATION_UNIT_SIZE_UNUSED; 3], + "frame {frames}: restores nothing, so the size is \ + libavcodec's 8 — never the parser's zero read as 16" + ); + } + assert!( + sizes.iter().all(|s| (6..=8).contains(s)), + "frame {frames}: log2_restoration_unit_size is 6, 7 or 8" + ); + } + } + + assert_eq!(frames, 274); + assert_eq!( + with_chroma_lf, 123, + "123 of 274 frames of this vector deblock chroma; at zero the levels \ + above are all zero anyway and this test could not tell a dropped pair \ + from a carried one" + ); + assert_eq!( + with_lr, 3, + "three frames use loop restoration, so both branches of the size are \ + exercised" + ); + } + + /// The packed CDEF strength bytes carry the CODED secondary strength. + /// + /// `CdefStrength::pack` gives `secondary` TWO bits, and the vendored parser + /// holds the AV1 spec's post-fixup value — 4 where the stream coded 3 (5.9.19 + /// rewrites the syntax element in place). `& 0x3` then turns the STRONGEST + /// secondary filter into no filter at all, silently, on 68 of this vector's 274 + /// frames including the first. libavcodec's `dxva2_av1.c` assigns CBS's + /// unmodified two-bit read into the same bitfield, which is the convention every + /// driver was validated against. + /// + /// Asserted against the packed BYTE rather than the intermediate struct, + /// because the truncation is what `pack` does and a test that stopped at the + /// struct would not have seen it. + #[test] + fn cdef_secondary_strengths_survive_the_two_bit_pack() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let (mut frames, mut corrected_frames) = (0u32, 0u32); + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("the clean vector plans") { + if plan.dpb.stored.is_none() { + continue; + } + let dx = plan_to_dxva_av1(packet, &plan, &mut slots).expect("converts"); + frames += 1; + let raw = &plan.header.cdef_params; + // `#[repr(packed)]` — copy the arrays out before indexing them. + let cdef = dx.pic_params.cdef; + let (y, uv) = (cdef.y_strengths, cdef.uv_strengths); + let mut corrected = false; + for i in 0..8 { + let want_y = coded_cdef_sec_strength(raw.cdef_y_sec_strength[i]); + let want_uv = coded_cdef_sec_strength(raw.cdef_uv_sec_strength[i]); + assert_eq!( + (y[i] >> 6, uv[i] >> 6), + (want_y, want_uv), + "frame {frames}: the secondary strength must survive the \ + two-bit field — the parser's 4 packs to 0" + ); + assert_eq!( + (y[i] & 0x3f, uv[i] & 0x3f), + ( + raw.cdef_y_pri_strength[i] as u8, + raw.cdef_uv_pri_strength[i] as u8 + ), + "the PRIMARY strengths are not fixed up by the spec and must \ + reach the driver untouched" + ); + if raw.cdef_y_sec_strength[i] == 4 || raw.cdef_uv_sec_strength[i] == 4 { + corrected = true; + } + } + if corrected { + corrected_frames += 1; + } + if frames == 1 { + assert_eq!( + (y[3] >> 6, uv[0] >> 6), + (3, 3), + "frame 0 codes the strongest secondary strength twice, and \ + the uncorrected conversion packed both as 0" + ); + } + } + } + + assert_eq!(frames, 274); + assert_eq!( + corrected_frames, 68, + "68 of 274 frames of this vector need the correction; at zero this test \ + compares an untouched conversion against itself" + ); + } + + /// A key frame names no reference, and must say so with the unused sentinel + /// rather than with slot 0. + #[test] + fn a_key_frame_names_no_reference() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let plans = planner.plan_au(first).expect("the first unit plans"); + let plan = plans.first().expect("a frame"); + assert!(plan.picture.is_key, "the vector opens on a key frame"); + let dx = plan_to_dxva_av1(first, plan, &mut slots).expect("converts"); + assert!(dx + .pic_params + .frame_refs + .iter() + .all(|e| e.index == UNUSED_INDEX)); + } + + /// The tile sizes are COUNTS of superblocks, not the coded minus-one values. + /// + /// A units defect the parser's field names invite, and the reason it needs its + /// own test is that nothing else can see it: every offset, every size and every + /// descriptor stays right, the picture decodes, and the driver has simply been + /// told each tile is one superblock narrower and shorter than it is. + /// + /// The number is checked against the FRAME rather than against the field it came + /// from: this vector is one tile, so the tile's width in superblocks is the whole + /// frame's, `ceil(320 / 64) = 5` columns by `ceil(240 / 64) = 4` rows at 64x64 + /// superblocks. A conversion that shipped the minus-one value would say 4 by 3. + #[test] + fn the_tile_sizes_are_superblock_counts_not_the_coded_minus_one() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let mut frames = 0u32; + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("the clean vector plans") { + if plan.dpb.stored.is_none() { + continue; + } + let dx = plan_to_dxva_av1(packet, &plan, &mut slots).expect("converts"); + frames += 1; + let t = &plan.header.tile_info; + // `#[repr(packed)]` — copy the block out before reading its arrays. + let tiles = dx.pic_params.tiles; + assert_eq!((tiles.cols, tiles.rows), (1, 1), "this vector is one tile"); + let sb = if plan.sequence.use_128x128_superblock { + 128 + } else { + 64 + }; + assert_eq!( + (tiles.widths[0], tiles.heights[0]), + ( + plan.header.frame_width.div_ceil(sb) as u16, + plan.header.frame_height.div_ceil(sb) as u16 + ), + "frame {frames}: the single tile spans the whole frame in \ + superblocks — libav sends `width_in_sbs_minus_1[i] + 1`" + ); + assert_eq!( + (tiles.widths[0], tiles.heights[0]), + ( + t.width_in_sbs_minus_1[0] as u16 + 1, + t.height_in_sbs_minus_1[0] as u16 + 1 + ), + "frame {frames}: and that is the coded value plus one" + ); + // Past the frame's tile grid the arrays stay zero — a driver reading + // `cols` entries never sees them, and a phantom `1` would be a tile + // where the frame has none. (`#[repr(packed)]`: the arrays are + // copied out whole before being iterated.) + let (widths, heights) = (tiles.widths, tiles.heights); + assert!(widths[1..].iter().all(|w| *w == 0)); + assert!(heights[1..].iter().all(|h| *h == 0)); + } + } + assert_eq!(frames, 274); + } + + /// Three fields whose correct value is a SENTINEL or a constant, on every frame + /// of the vector — none of which any other assertion here would notice. + /// + /// * `StatusReportFeedbackNumber` **zero**: libavcodec has the assignment + /// commented out for AV1 alone ("breaks decoding on some drivers (tested on + /// NVIDIA 457.09)") and Chromium ships the zero too ("should not be equal to + /// 0 ... but it crashes :|"). This rung does not even accept a number. + /// * `qm_y`/`qm_u`/`qm_v` **0xFF** where the frame uses no quantiser matrix. + /// The struct has no `using_qmatrix` bit, and the parser leaves the indices at + /// 0 — a VALID matrix — so the sentinel is the only thing standing between + /// every frame of this vector and a dequantisation against matrix 0. + /// * `reference_frame_update` **1**, which libavcodec writes as a literal. + #[test] + fn the_three_fields_whose_right_answer_is_a_constant() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let (mut frames, mut without_qmatrix, mut without_refresh) = (0u32, 0u32, 0u32); + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("the clean vector plans") { + if plan.dpb.stored.is_none() { + continue; + } + let dx = plan_to_dxva_av1(packet, &plan, &mut slots).expect("converts"); + frames += 1; + let pp = &dx.pic_params; + let status = pp.status_report_feedback_number; + assert_eq!( + status, 0, + "frame {frames}: AV1 submits a zero StatusReportFeedbackNumber" + ); + + let q = pp.quantization; + let (qm_y, qm_u, qm_v) = (q.qm_y, q.qm_u, q.qm_v); + if plan.header.quantization_params.using_qmatrix { + assert_eq!( + (qm_y, qm_u, qm_v), + ( + plan.header.quantization_params.qm_y as u8, + plan.header.quantization_params.qm_u as u8, + plan.header.quantization_params.qm_v as u8 + ) + ); + } else { + without_qmatrix += 1; + assert_eq!( + (qm_y, qm_u, qm_v), + (QM_UNUSED, QM_UNUSED, QM_UNUSED), + "frame {frames}: with no quantiser matrix the indices are the \ + 0xFF sentinel — 0 is matrix zero, which the driver would \ + dequantize against" + ); + } + + // `reference_frame_update` is bit 22 of the coding flags — read back + // through `pack` rather than spelled as a magic mask. + let coding = pp.coding; + let on = CodingFlagsAv1 { + reference_frame_update: true, + ..Default::default() + } + .pack(); + assert_eq!(coding & on, on, "frame {frames}: libav writes a literal 1"); + if plan.header.refresh_frame_flags == 0 { + without_refresh += 1; + } + } + } + assert_eq!(frames, 274); + assert_eq!( + without_qmatrix, 274, + "no frame of this vector uses a quantiser matrix, so the sentinel is what \ + the driver reads on every one of them — at zero this test proves nothing" + ); + // Not an anti-vacuity assertion but a note about what this vector CANNOT + // show: `reference_frame_update` only differs from `refresh_frame_flags != 0` + // on a frame that refreshes nothing, and this vector has none. + assert_eq!(without_refresh, 0); + } + + /// Every picture a temporal unit decodes is still addressable once the unit ends. + /// + /// A precondition of the Windows AV1 parity harness rather than of this crate. + /// That harness drives the production entry point, which takes a whole temporal + /// unit and plans it internally, so it reaches a HIDDEN frame's pixels by asking + /// the slot map where that picture went after the unit is done. Sound only if a + /// unit never displaces a picture it decoded itself — a fact about this vector, + /// not about AV1 — and the harness needs a GPU while this does not, so the check + /// lives here where every leg runs it. + #[test] + fn no_unit_of_the_vector_displaces_a_picture_it_decoded_itself() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let (mut units, mut multi_frame) = (0u32, 0u32); + + for packet in IvfIterator::new(AV1_25FPS) { + let plans = planner.plan_au(packet).expect("the clean vector plans"); + units += 1; + let mut decoded = Vec::new(); + for plan in &plans { + if plan.dpb.stored.is_none() { + continue; + } + let dx = plan_to_dxva_av1(packet, plan, &mut slots).expect("converts"); + decoded.push((dx.setup_id, dx.setup_slot)); + } + if decoded.len() > 1 { + multi_frame += 1; + } + for (id, slot) in decoded { + assert_eq!( + slots.slot_of(id), + Some(slot), + "unit {units}: picture {id} left surface {slot} before its own \ + unit finished, so a per-unit readback could not find it" + ); + } + } + assert_eq!(units, 250); + assert_eq!( + multi_frame, 24, + "24 units carry a hidden frame as well as the shown one — at zero this \ + check never saw the case it exists for" + ); + } + + /// A frame whose tile groups do not add up to its tile GRID is refused. + /// + /// The failure this stands in for is a dropped tile group: the OBU walk never + /// sees it, so no `TruncatedAu` warning is raised and nothing else in the rung + /// notices that the submission is short of what `pic_params.tiles` announces. + /// Simulated by removing a tile-group plan, which is what such a loss leaves + /// behind. + #[test] + fn a_frame_short_of_its_tile_grid_is_refused_rather_than_submitted() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let plans = planner.plan_au(first).expect("the first unit plans"); + let mut plan = plans.into_iter().next().expect("a frame"); + + // The unmodified frame converts, so the refusal below is about the tiles and + // not about the frame. + plan_to_dxva_av1(first, &plan, &mut slots).expect("the untouched frame converts"); + + // Now claim a two-tile grid the access unit has one tile for. + let header = std::rc::Rc::make_mut(&mut plan.header); + header.tile_info.tile_cols = 2; + header.tile_info.tile_rows = 1; + let mut slots = SlotMap::new(NUM_REF_SLOTS); + assert_eq!( + plan_to_dxva_av1(first, &plan, &mut slots).err(), + Some(PlanToDxvaAv1Error::TileCountMismatch { + records: 1, + walked: 1, + grid: 2, + }) + ); + } +} diff --git a/crates/pf-dxvadec/src/pic_h265.rs b/crates/pf-dxvadec/src/pic_h265.rs new file mode 100644 index 00000000..dbb32293 --- /dev/null +++ b/crates/pf-dxvadec/src/pic_h265.rs @@ -0,0 +1,1349 @@ +//! Per-AU H.265 conversion: one [`AuPlan`] into the `DXVA_PicParams_HEVC`, +//! `DXVA_Qmatrix_HEVC` and slice-control records +//! `ID3D11VideoContext::SubmitDecoderBuffers` takes — [`crate::pic`] one codec +//! over, and the DXVA twin of [`pf_vkdecode::pic_h265`]. +//! +//! The codec difference that shapes this module is the same one that shapes the +//! Vulkan H.265 converter: HEVC decode takes NO per-slice reference lists. The +//! hardware re-derives 8.3.4's lists itself from the slice bits, keyed by the +//! picture-level RPS index arrays (`RefPicSetStCurrBefore`/`StCurrAfter`/ +//! `LtCurr`), which are INDICES INTO `RefPicList` — so nothing here expresses +//! per-slice list ORDER, unlike H.264. The per-slice lists still exist and are +//! used as a cross-check: every entry must be a member of the current sets, or +//! the conversion fails closed. +//! +//! **Surfaces are slots** carries over verbatim from [`crate::pic`] and is +//! documented there rather than repeated: a `DXVA_PicEntry_HEVC` carries the decode +//! texture array's `ArraySlice`, which is what lets this module drive the Vulkan +//! rung's [`SlotMap`] unchanged. +//! +//! # `RefPicList` is the MARKED DPB, indexed by the RPS arrays +//! +//! `RefPicList` holds every picture currently marked used for reference — +//! libavcodec's DXVA HEVC path walks its whole DPB for +//! `HEVC_FRAME_FLAG_{LONG,SHORT}_REF` — and the `RefPicSetStCurrBefore`/ +//! `StCurrAfter`/`LtCurr` arrays are INDICES into it. The two questions are +//! therefore separate: which pictures the hardware may hold (the array), and which +//! of them THIS picture uses (the indices). +//! +//! The union of the three current sets is not the same thing, and the gap is +//! 8.3.2's *Foll* sets: a long-term anchor pinned for a later picture is marked in +//! the DPB while no current set names it. Binding only the current sets would drop +//! it from `RefPicList` for exactly the pictures between the pin and its use, and a +//! driver keeping per-reference state may treat that absence as a retirement — the +//! RFI failure shape. So the array is the plan's +//! [`dpb_refs`](pf_bitstream::h265::AuPlan::dpb_refs) snapshot, laid out with the +//! current sets first (which keeps the index arrays' values identical to what the +//! sets alone produced) and the remaining marked pictures appended. +//! +//! Vulkan's `pReferenceSlots` asks the other question — the slots THIS decode +//! operation uses — so the native Vulkan rung binds the current sets and is right +//! to; the two rungs differ here because the two specifications do. +//! +//! Concealment note: a lost reference is ABSENT from the plan's RPS sets (flagged +//! upstream via `PlanWarning::MissingReference`), so the index arrays compact past +//! it. That is deliberate — there is no surface to point at, `0xFF` padding keeps +//! the arrays well-formed, and the session layer has already been told to request +//! recovery. Fabricating an entry is the one thing this crate never does. + +use std::ops::Range; + +use cros_codecs::codec::h265::parser::Pps; +use cros_codecs::codec::h265::parser::Sps; +use pf_bitstream::h265::AuPlan; +use pf_bitstream::h265::PicId; +use pf_bitstream::h265::RefPic; +use pf_vkdecode::SlotError; +use pf_vkdecode::SlotMap; +use tracing::trace; + +use crate::dxva::HevcFormatFlags; +use crate::dxva::HevcPictureFlags; +use crate::dxva::HevcToolFlags; +use crate::dxva::PicEntry; +use crate::dxva::PicParamsHevc; +use crate::dxva::QmatrixHevc; +use crate::dxva::SliceHevcShort; +use crate::dxva::UNUSED_ENTRY; + +/// `RefPicList`'s length in `DXVA_PicParams_HEVC`. Fifteen, not sixteen: the +/// current picture is named separately by `CurrPic`, so the array only ever +/// holds the other DPB members. +const REF_PIC_LIST_LEN: usize = 15; + +/// Each `RefPicSet*` index array holds eight entries — the hard ceiling on how +/// many CURRENT references one set may carry through DXVA (H.265 itself allows +/// more; beyond eight is unexpressible here and rejected). +pub const RPS_LIST_SIZE: usize = 8; + +/// One active reference of the AU: its surface index and the planner id it +/// resolves. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DxvaRefH265 { + /// The DPB slot, which is the decode texture array's `ArraySlice`. + pub slot: u8, + pub id: PicId, + pub is_long_term: bool, + pub pic_order_cnt: i32, +} + +/// Everything CPU-derivable of one AU's DXVA submission. +#[derive(Debug, Clone, PartialEq)] +pub struct DecodePlanDxvaH265 { + /// The picture parameters. Their `RefPicSetStCurrBefore`/`StCurrAfter`/ + /// `LtCurr` arrays hold INDICES INTO [`Self::refs`] (`0xFF` = unused), which + /// is exactly how they index `RefPicList` — the two are laid out in the same + /// order by construction. + pub pic_params: PicParamsHevc, + /// The inverse-quantization matrices, or `None` when the sequence does not + /// enable scaling lists — in which case the buffer is NOT SUBMITTED AT ALL. + /// + /// That is libavcodec's own condition: `dxva2_hevc_end_frame` passes the + /// qmatrix buffer only when `dwCodingParamToolFlags & 1` + /// (`scaling_list_enabled_flag`). Submitting it unconditionally is not the + /// harmless belt-and-braces it looks like — with scaling lists disabled the + /// hardware is told to ignore the matrices, and any driver that honours a + /// buffer it was handed anyway would dequantize every residual against + /// whatever the buffer holds. Every punktfunk HEVC stream is in exactly that + /// shape. + pub qmatrix: Option, + /// Byte ranges of the AU's slice segment NALUs, start code included, in plan + /// order — what [`crate::pack::pack`] takes. + pub slice_ranges: Vec>, + /// The surface the decoded picture is written into. + pub setup_slot: u8, + pub setup_id: PicId, + /// Whether later pictures may reference the decoded picture (false for + /// sub-layer non-reference NALU types). + pub setup_is_reference: bool, + /// The marked DPB, resolved to surfaces and laid out identically in + /// `pic_params.RefPicList`: the plan's three current RPS sets first, in set + /// order (StCurrBefore, StCurrAfter, LtCurr) and first appearance first, then + /// every other marked picture the DPB holds (module docs). + pub refs: Vec, +} + +/// Conversion failures. Stream damage never lands here — pf-bitstream degrades it +/// to [`pf_bitstream::h265::PlanWarning`]s upstream. (`PlanError::RaslSkipped` +/// also never reaches this layer: it is an error OF planning, handled as an +/// Ok-skip by the client wiring, and no plan exists to convert.) +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanToDxvaH265Error { + /// The plan holds no slices; there is nothing to submit. + NoSlices, + /// The plan's `DpbUpdate.stored` is `None`. + NoStoredId, + /// An RPS entry's id holds no slot: an earlier plan never went through this + /// [`SlotMap`]. + UnresolvedReference(PicId), + /// A slice reference-list entry names a picture outside the plan's current + /// RPS sets. 8.3.4 builds every list FROM those sets, so this is a planner + /// contract violation — and no amount of `RefPicList` residency fixes it, + /// because the hardware derives its lists from the RPS INDEX ARRAYS, which + /// only the current sets populate. + ReferenceOutsideRps(PicId), + Slot(SlotError), + /// A current RPS set holds more entries than the index arrays' eight. + RpsSetOverflow { + set: &'static str, + len: usize, + }, + /// The AU references more distinct pictures than `RefPicList` holds (15). + TooManyReferences(usize), + /// The first slice's inline `st_ref_pic_set()` predicts from an SPS + /// candidate that does not exist — `ucNumDeltaPocsOfRefRpsIdx` cannot be + /// derived, and the hardware would misparse the slice header. + InvalidRefRpsIdx { + curr_rps_idx: u8, + delta_idx_minus1: u8, + }, + /// The inline `st_ref_pic_set()`'s bit count exceeds `u16` + /// (`wNumBitsForShortTermRPSInSlice`) — a header that large is corrupt. + StRpsBitsOverflow(u32), + /// The predicted-from candidate's `NumDeltaPocs` exceeds `u8`. Impossible + /// off a real parse (≤ 32); an error rather than a clamp, because a clamped + /// count makes the hardware misparse the slice header. + NumDeltaPocsOverflow(u32), + /// The map was built for a different DPB depth than this plan's + /// `max_dpb_frames` — an SPS renegotiation resized the DPB; rebuild decoder, + /// pool and map. + CapacityMismatch { + required: usize, + capacity: usize, + }, + /// A picture dimension in minimum coding blocks exceeds the `USHORT` the + /// picture parameters carry. + DimensionOverflow { + width: u32, + height: u32, + }, + /// `separate_colour_plane_flag`. Refused upstream by pf-bitstream's envelope + /// gate; checked again because the picture layout for it is a different + /// shape entirely. + SeparateColourPlanes, +} + +impl std::fmt::Display for PlanToDxvaH265Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlanToDxvaH265Error::NoSlices => write!(f, "the plan holds no slices"), + PlanToDxvaH265Error::NoStoredId => write!( + f, + "the plan stores no picture (flush updates go to SlotMap::apply)" + ), + PlanToDxvaH265Error::UnresolvedReference(id) => { + write!(f, "referenced picture {id} holds no DPB slot in this map") + } + PlanToDxvaH265Error::ReferenceOutsideRps(id) => write!( + f, + "a slice list names picture {id}, which is not in the AU's RPS sets" + ), + PlanToDxvaH265Error::Slot(err) => write!(f, "slot assignment failed: {err}"), + PlanToDxvaH265Error::RpsSetOverflow { set, len } => { + write!(f, "{set} holds {len} entries; DXVA's index arrays hold 8") + } + PlanToDxvaH265Error::TooManyReferences(count) => { + write!(f, "{count} references exceed DXVA's RefPicList of 15") + } + PlanToDxvaH265Error::InvalidRefRpsIdx { + curr_rps_idx, + delta_idx_minus1, + } => write!( + f, + "inline RPS {curr_rps_idx} predicts from delta_idx_minus1 \ + {delta_idx_minus1}, which names no SPS candidate" + ), + PlanToDxvaH265Error::StRpsBitsOverflow(bits) => { + write!(f, "inline st_ref_pic_set of {bits} bits exceeds u16") + } + PlanToDxvaH265Error::NumDeltaPocsOverflow(count) => { + write!(f, "candidate NumDeltaPocs {count} exceeds u8") + } + PlanToDxvaH265Error::CapacityMismatch { required, capacity } => write!( + f, + "the plan needs {required} slots but the map holds {capacity} — \ + an SPS renegotiation resized the DPB; rebuild decoder and map" + ), + PlanToDxvaH265Error::DimensionOverflow { width, height } => write!( + f, + "a {width}x{height} picture in min-CBs exceeds the DXVA picture parameters" + ), + PlanToDxvaH265Error::SeparateColourPlanes => { + write!(f, "separate_colour_plane_flag is outside this backend") + } + } + } +} + +impl std::error::Error for PlanToDxvaH265Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + PlanToDxvaH265Error::Slot(err) => Some(err), + _ => None, + } + } +} + +impl From for PlanToDxvaH265Error { + fn from(err: SlotError) -> Self { + PlanToDxvaH265Error::Slot(err) + } +} + +/// `ucNumDeltaPocsOfRefRpsIdx`: when the first slice's inline `st_ref_pic_set()` +/// uses inter-RPS prediction, the hardware re-parses those slice bits and needs +/// `NumDeltaPocs[RefRpsIdx]` of the SOURCE candidate to size the +/// `used_by_curr_pic_flag`/`use_delta_flag` loop (7.4.8); otherwise 0. +/// +/// Byte-for-byte the derivation `pf_vkdecode::pic_h265` makes for Vulkan's +/// `NumDeltaPocsOfRefRpsIdx` — the same field under a different name. It is +/// duplicated rather than shared because it is private there and the two crates +/// have no business exporting each other's internals; if it ever changes, both +/// copies' tests plan the same vendored vector and will disagree. +fn num_delta_pocs_of_ref_rps_idx(plan: &AuPlan) -> Result { + let hdr = &plan + .slices + .first() + .expect("caller validated the plan holds slices") + .header; + // Inline means CurrRpsIdx == num_short_term_ref_pic_sets (8.3.2 NOTE 2); an + // SPS-indexed RPS re-parses nothing in the slice header. + let inline = !hdr.short_term_ref_pic_set_sps_flag + && hdr.curr_rps_idx == plan.sps.num_short_term_ref_pic_sets; + if !inline || !hdr.short_term_ref_pic_set.inter_ref_pic_set_prediction_flag { + return Ok(0); + } + // RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1), stRpsIdx = CurrRpsIdx here + // (equation 7-59). u16 arithmetic so a hostile delta cannot wrap. + let delta = hdr.short_term_ref_pic_set.delta_idx_minus1; + let source = u16::from(hdr.curr_rps_idx) + .checked_sub(u16::from(delta) + 1) + .and_then(|idx| plan.sps.short_term_ref_pic_set.get(usize::from(idx))) + .ok_or(PlanToDxvaH265Error::InvalidRefRpsIdx { + curr_rps_idx: hdr.curr_rps_idx, + delta_idx_minus1: delta, + })?; + u8::try_from(source.num_delta_pocs) + .map_err(|_| PlanToDxvaH265Error::NumDeltaPocsOverflow(source.num_delta_pocs)) +} + +/// One marked DPB picture, resolved to its surface. +fn dxva_ref(slot: u8, rp: &RefPic) -> DxvaRefH265 { + DxvaRefH265 { + slot, + id: rp.id, + is_long_term: rp.is_long_term, + pic_order_cnt: rp.pic_order_cnt, + } +} + +/// `DXVA_Qmatrix_HEVC` for a sequence that enables scaling lists. +/// +/// # Which lists +/// +/// 7.4.5's activation, in the order the spec resolves it: +/// +/// 1. the PPS's, when it codes scaling list data; +/// 2. otherwise the SPS's, when IT codes scaling list data; +/// 3. otherwise the Table 7-5/7-6 DEFAULT lists. +/// +/// libavcodec writes the first two legs as one ternary and stops, because its own +/// parser seeds an SPS that codes nothing with the defaults — so leg 3 never has to +/// be spelled out there. The vendored cros-codecs parser does NOT: `ScalingLists` +/// defaults to all zeros and an SPS only ever fills it under +/// `scaling_list_enabled_flag && sps_scaling_list_data_present_flag`. Copying +/// libavcodec's ternary verbatim therefore hands the driver 64 zeros per list on a +/// stream that says "enabled, nothing coded, use the defaults" — a legal and +/// entirely ordinary shape — and every residual dequantizes to nothing. +/// +/// Leg 3 is served by the PPS's lists, which the same parser DOES default-fill +/// (Table 7-5/7-6 plus a DC of 8 + 8 = 16) whenever the PPS codes none. So: +/// SPS-coded data wins only when the PPS coded none, and the PPS's copy carries +/// both leg 1 and leg 3. +fn quantization_matrices(sps: &Sps, pps: &Pps) -> QmatrixHevc { + let sl = if sps.scaling_list_data_present_flag && !pps.scaling_list_data_present_flag { + &sps.scaling_list + } else { + &pps.scaling_list + }; + let mut qm = QmatrixHevc::zeroed(); + qm.ucScalingLists0 = sl.scaling_list_4x4; + qm.ucScalingLists1 = sl.scaling_list_8x8; + qm.ucScalingLists2 = sl.scaling_list_16x16; + // sizeId 3 codes only matrixId 0 and 3 (its loop steps by three), and DXVA + // carries exactly those two — so index k here is the parser's k * 3. + qm.ucScalingLists3[0] = sl.scaling_list_32x32[0]; + qm.ucScalingLists3[1] = sl.scaling_list_32x32[3]; + // The DC entries are the ScalingFactor DC VALUE (`…_minus8 + 8`), not the + // coded delta. Clamped rather than wrapped: the parser bounds the coded + // value to -7..=247, so the sum is 1..=255 and the clamp is unreachable. + for (dst, src) in qm + .ucScalingListDCCoefSizeID2 + .iter_mut() + .zip(sl.scaling_list_dc_coef_minus8_16x16) + { + *dst = (i32::from(src) + 8).clamp(0, 255) as u8; + } + for (k, dst) in qm.ucScalingListDCCoefSizeID3.iter_mut().enumerate() { + let src = sl.scaling_list_dc_coef_minus8_32x32[k * 3]; + *dst = (i32::from(src) + 8).clamp(0, 255) as u8; + } + qm +} + +/// Convert one planned AU, driving `slots` through the AU's slot lifecycle. +/// +/// `status_id` becomes `StatusReportFeedbackNumber` — see [`crate::pic::plan_to_dxva`]. +/// +/// Atomicity contract (identical to the H.264 module): every fallible step runs +/// before any mutation of `slots`, so an error leaves the map exactly as it was. +pub fn plan_to_dxva_h265( + plan: &AuPlan, + slots: &mut SlotMap, + status_id: u32, +) -> Result { + if plan.slices.is_empty() { + return Err(PlanToDxvaH265Error::NoSlices); + } + let setup_id = plan.dpb.stored.ok_or(PlanToDxvaH265Error::NoStoredId)?; + let sps = &plan.sps; + let pps = &plan.pps; + let pic = &plan.picture; + + if sps.separate_colour_plane_flag { + return Err(PlanToDxvaH265Error::SeparateColourPlanes); + } + + let required = pic.max_dpb_frames + 1; + if slots.capacity() != required { + return Err(PlanToDxvaH265Error::CapacityMismatch { + required, + capacity: slots.capacity(), + }); + } + + // The AU-level binding set: the union of the three current RPS sets, first + // appearance first, plus the index arrays that point into it. + let mut refs: Vec = Vec::new(); + let mut index_arrays = [[UNUSED_ENTRY; RPS_LIST_SIZE]; 3]; + let sets: [(&'static str, &[RefPic]); 3] = [ + ("RefPicSetStCurrBefore", &plan.rps.st_curr_before), + ("RefPicSetStCurrAfter", &plan.rps.st_curr_after), + ("RefPicSetLtCurr", &plan.rps.lt_curr), + ]; + for (array, (name, set)) in index_arrays.iter_mut().zip(sets) { + if set.len() > RPS_LIST_SIZE { + return Err(PlanToDxvaH265Error::RpsSetOverflow { + set: name, + len: set.len(), + }); + } + for (position, rp) in set.iter().enumerate() { + let index = match refs.iter().position(|existing| existing.id == rp.id) { + // A picture that appears in two sets binds ONCE and both index + // arrays point at that one entry. + Some(index) => index, + None => { + let slot = slots + .slot_of(rp.id) + .ok_or(PlanToDxvaH265Error::UnresolvedReference(rp.id))?; + // The DPB snapshot is the authority for the marking: an + // `RefPicSetLtCurr` index into a short-term-marked entry is an + // inconsistent DPB, and hardware treats the two differently. + // The set's own copy is the fallback and cannot be reached off + // a real plan (8.3.2 marks every set member before the sets + // are reported). + let marked = plan.dpb_refs.iter().find(|d| d.id == rp.id); + if marked.is_none() { + trace!( + id = rp.id, + "an RPS entry names a picture the marked DPB does not hold" + ); + } + refs.push(dxva_ref(slot, marked.unwrap_or(rp))); + refs.len() - 1 + } + }; + // The RPS-set overflow check above bounds this well inside u8 (and + // below the 0xFF sentinel). + array[position] = index as u8; + } + } + // The current sets are what the index arrays MUST be able to name, so their + // overflow is a refusal. + if refs.len() > REF_PIC_LIST_LEN { + return Err(PlanToDxvaH265Error::TooManyReferences(refs.len())); + } + + // Cross-check, against the CURRENT sets alone — which is what `refs` holds at + // this exact point, and why the check runs before the *Foll* pictures are + // appended. 8.3.4 builds every slice list from the current sets, so an entry + // outside them is a planner-contract violation: the hardware re-derives its + // lists from the RPS INDEX ARRAYS, and a picture merely present in + // `RefPicList` is not reachable by that derivation. + let current_set_refs = refs.len(); + for slice in &plan.slices { + for rp in slice.ref_list0.iter().chain(&slice.ref_list1) { + if !refs[..current_set_refs] + .iter() + .any(|existing| existing.id == rp.id) + { + return Err(PlanToDxvaH265Error::ReferenceOutsideRps(rp.id)); + } + } + } + + // Then the rest of the marked DPB — the *Foll* pictures (module docs) — in the + // planner's DPB order, which is libavcodec's. Overflow past the array is + // dropped rather than refused: nothing here is referenced by this picture, so + // the decode is unaffected and refusing would cost the whole frame. `RefPicList` + // holds fifteen while the DPB holds up to sixteen, so this is reachable, if + // only on a stream that has filled its DPB entirely with references. + for rp in &plan.dpb_refs { + if refs.len() == REF_PIC_LIST_LEN { + trace!( + marked = plan.dpb_refs.len(), + "the marked DPB exceeds RefPicList; the tail is not expressible" + ); + break; + } + if refs.iter().any(|existing| existing.id == rp.id) { + continue; + } + match slots.slot_of(rp.id) { + Some(slot) => refs.push(dxva_ref(slot, rp)), + None => trace!(id = rp.id, "a marked DPB picture holds no slot in this map"), + } + } + + // Everything else fallible, before any mutation. + let num_delta_pocs = num_delta_pocs_of_ref_rps_idx(plan)?; + let st_rps_bits = u16::try_from(pic.short_term_ref_pic_set_size_bits).map_err(|_| { + PlanToDxvaH265Error::StRpsBitsOverflow(pic.short_term_ref_pic_set_size_bits) + })?; + let min_cb = sps.min_cb_log2_size_y; + let width_in_min_cbs = u32::from(sps.pic_width_in_luma_samples) >> min_cb; + let height_in_min_cbs = u32::from(sps.pic_height_in_luma_samples) >> min_cb; + let (Ok(width_in_min_cbs), Ok(height_in_min_cbs)) = ( + u16::try_from(width_in_min_cbs), + u16::try_from(height_in_min_cbs), + ) else { + return Err(PlanToDxvaH265Error::DimensionOverflow { + width: width_in_min_cbs, + height: height_in_min_cbs, + }); + }; + + let mut pp = PicParamsHevc::zeroed(); + pp.PicWidthInMinCbsY = width_in_min_cbs; + pp.PicHeightInMinCbsY = height_in_min_cbs; + pp.wFormatAndSequenceInfoFlags = HevcFormatFlags { + chroma_format_idc: pic.chroma_format_idc, + separate_colour_plane_flag: false, // refused above + bit_depth_luma_minus8: pic.bit_depth_luma_minus8, + bit_depth_chroma_minus8: pic.bit_depth_chroma_minus8, + log2_max_pic_order_cnt_lsb_minus4: sps.log2_max_pic_order_cnt_lsb_minus4, + } + .pack(); + // The DPB depth of the HIGHEST temporal sub-layer — the only one whose + // buffering covers the whole stream. `max_sub_layers_minus1` indexes a + // seven-entry array, so the read cannot go out of bounds off any parse. + pp.sps_max_dec_pic_buffering_minus1 = sps + .max_dec_pic_buffering_minus1 + .get(usize::from(sps.max_sub_layers_minus1)) + .copied() + .unwrap_or(0); + pp.log2_min_luma_coding_block_size_minus3 = sps.log2_min_luma_coding_block_size_minus3; + pp.log2_diff_max_min_luma_coding_block_size = sps.log2_diff_max_min_luma_coding_block_size; + pp.log2_min_transform_block_size_minus2 = sps.log2_min_luma_transform_block_size_minus2; + pp.log2_diff_max_min_transform_block_size = sps.log2_diff_max_min_luma_transform_block_size; + pp.max_transform_hierarchy_depth_inter = sps.max_transform_hierarchy_depth_inter; + pp.max_transform_hierarchy_depth_intra = sps.max_transform_hierarchy_depth_intra; + pp.num_short_term_ref_pic_sets = sps.num_short_term_ref_pic_sets; + pp.num_long_term_ref_pics_sps = sps.num_long_term_ref_pics_sps; + pp.num_ref_idx_l0_default_active_minus1 = pps.num_ref_idx_l0_default_active_minus1; + pp.num_ref_idx_l1_default_active_minus1 = pps.num_ref_idx_l1_default_active_minus1; + pp.init_qp_minus26 = pps.init_qp_minus26; + pp.ucNumDeltaPocsOfRefRpsIdx = num_delta_pocs; + // 0 when the RPS came from the SPS by index — exactly DXVA's convention for + // this field, and pf-bitstream's for the value it derives from. + pp.wNumBitsForShortTermRPSInSlice = st_rps_bits; + pp.dwCodingParamToolFlags = HevcToolFlags { + scaling_list_enabled_flag: sps.scaling_list_enabled_flag, + amp_enabled_flag: sps.amp_enabled_flag, + sample_adaptive_offset_enabled_flag: sps.sample_adaptive_offset_enabled_flag, + pcm_enabled_flag: sps.pcm_enabled_flag, + pcm_sample_bit_depth_luma_minus1: sps.pcm_sample_bit_depth_luma_minus1, + pcm_sample_bit_depth_chroma_minus1: sps.pcm_sample_bit_depth_chroma_minus1, + log2_min_pcm_luma_coding_block_size_minus3: sps.log2_min_pcm_luma_coding_block_size_minus3, + log2_diff_max_min_pcm_luma_coding_block_size: sps + .log2_diff_max_min_pcm_luma_coding_block_size, + pcm_loop_filter_disabled_flag: sps.pcm_loop_filter_disabled_flag, + long_term_ref_pics_present_flag: sps.long_term_ref_pics_present_flag, + sps_temporal_mvp_enabled_flag: sps.temporal_mvp_enabled_flag, + strong_intra_smoothing_enabled_flag: sps.strong_intra_smoothing_enabled_flag, + dependent_slice_segments_enabled_flag: pps.dependent_slice_segments_enabled_flag, + output_flag_present_flag: pps.output_flag_present_flag, + num_extra_slice_header_bits: pps.num_extra_slice_header_bits, + sign_data_hiding_enabled_flag: pps.sign_data_hiding_enabled_flag, + cabac_init_present_flag: pps.cabac_init_present_flag, + } + .pack(); + pp.dwCodingSettingPicturePropertyFlags = HevcPictureFlags { + constrained_intra_pred_flag: pps.constrained_intra_pred_flag, + transform_skip_enabled_flag: pps.transform_skip_enabled_flag, + cu_qp_delta_enabled_flag: pps.cu_qp_delta_enabled_flag, + pps_slice_chroma_qp_offsets_present_flag: pps.slice_chroma_qp_offsets_present_flag, + weighted_pred_flag: pps.weighted_pred_flag, + weighted_bipred_flag: pps.weighted_bipred_flag, + transquant_bypass_enabled_flag: pps.transquant_bypass_enabled_flag, + tiles_enabled_flag: pps.tiles_enabled_flag, + entropy_coding_sync_enabled_flag: pps.entropy_coding_sync_enabled_flag, + uniform_spacing_flag: pps.uniform_spacing_flag, + loop_filter_across_tiles_enabled_flag: pps.loop_filter_across_tiles_enabled_flag, + pps_loop_filter_across_slices_enabled_flag: pps.loop_filter_across_slices_enabled_flag, + deblocking_filter_override_enabled_flag: pps.deblocking_filter_override_enabled_flag, + pps_deblocking_filter_disabled_flag: pps.deblocking_filter_disabled_flag, + lists_modification_present_flag: pps.lists_modification_present_flag, + slice_segment_header_extension_present_flag: pps + .slice_segment_header_extension_present_flag, + irap_pic_flag: pic.is_irap, + idr_pic_flag: pic.is_idr, + // HEVC states intra-ness at the picture level: an IRAP picture is + // intra-only by definition, and nothing else is guaranteed to be. Same + // derivation libavcodec's DXVA HEVC path makes. + intra_pic_flag: pic.is_irap, + } + .pack(); + pp.pps_cb_qp_offset = pps.cb_qp_offset; + pp.pps_cr_qp_offset = pps.cr_qp_offset; + if pps.tiles_enabled_flag { + pp.num_tile_columns_minus1 = pps.num_tile_columns_minus1; + pp.num_tile_rows_minus1 = pps.num_tile_rows_minus1; + if !pps.uniform_spacing_flag { + // Only the non-uniform case codes explicit widths; the uniform case + // leaves them zero and the hardware derives its own grid. + // `saturating_as` on each: a tile edge past 65535 min-CBs cannot + // come off a real PPS, and a clamp here is strictly better than a + // panic on a corrupt one. + for (dst, src) in pp + .column_width_minus1 + .iter_mut() + .zip(pps.column_width_minus1) + { + *dst = u16::try_from(src).unwrap_or(u16::MAX); + } + for (dst, src) in pp.row_height_minus1.iter_mut().zip(pps.row_height_minus1) { + *dst = u16::try_from(src).unwrap_or(u16::MAX); + } + } + } + pp.diff_cu_qp_delta_depth = if pps.cu_qp_delta_enabled_flag { + pps.diff_cu_qp_delta_depth + } else { + 0 + }; + pp.pps_beta_offset_div2 = pps.beta_offset_div2; + pp.pps_tc_offset_div2 = pps.tc_offset_div2; + pp.log2_parallel_merge_level_minus2 = pps.log2_parallel_merge_level_minus2; + pp.CurrPicOrderCntVal = pic.pic_order_cnt; + pp.StatusReportFeedbackNumber = status_id; + + pp.RefPicList = [PicEntry::UNUSED; REF_PIC_LIST_LEN]; + for (i, r) in refs.iter().enumerate() { + pp.RefPicList[i] = PicEntry::new(r.slot, r.is_long_term); + pp.PicOrderCntValList[i] = r.pic_order_cnt; + } + [ + pp.RefPicSetStCurrBefore, + pp.RefPicSetStCurrAfter, + pp.RefPicSetLtCurr, + ] = index_arrays; + + // The quantization matrices — built ONLY when the sequence enables scaling + // lists, because that is the only case in which the buffer is submitted (see + // `DecodePlanDxvaH265::qmatrix`). + let qm = sps + .scaling_list_enabled_flag + .then(|| quantization_matrices(sps, pps)); + + let slice_ranges: Vec> = plan.slices.iter().map(|s| s.data.clone()).collect(); + + // Mutations LAST, after every fallible step above. Removals first — they were + // real regardless of this AU's fate — then the setup assignment, released + // immediately when this very plan already evicted the stored picture (the + // surface must still exist for the decode itself). + let setup_evicted = plan.dpb.removed.contains(&setup_id); + for &id in &plan.dpb.removed { + if id == setup_id { + continue; + } + if !slots.release(id) { + trace!(id, "DpbUpdate removed an id this SlotMap never assigned"); + } + } + let setup_slot = slots.assign(setup_id)?; + if setup_evicted { + slots.release(setup_id); + } + pp.CurrPic = PicEntry::new(setup_slot, false); + + Ok(DecodePlanDxvaH265 { + pic_params: pp, + qmatrix: qm, + slice_ranges, + setup_slot, + setup_id, + setup_is_reference: pic.is_reference, + refs, + }) +} + +/// The slice-control records for a packed AU — [`crate::pic::slice_control`]'s +/// HEVC twin. +pub fn slice_control_h265(records: &[crate::pack::SliceRecord]) -> Vec { + records + .iter() + .map(|r| SliceHevcShort { + BSNALunitDataLocation: r.location, + SliceBytesInBuffer: r.bytes, + wBadSliceChopping: 0, + }) + .collect() +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use cros_codecs::codec::h265::parser::Nalu; + use pf_bitstream::h265::H265Planner; + + use super::*; + + /// The same vendored vectors pf-bitstream's and pf-vkdecode's h265 tests + /// plan, included from the same path. + const TEST_25FPS: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265" + ); + const TEST_64X64_I_P_B_P: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P-B-P.h265" + ); + + /// Test-only AU splitter, mirroring pf-vkdecode's (which mirrors + /// pf-bitstream's `#[cfg(test)]`-private helper). + fn split_into_aus(stream: &[u8]) -> Vec<&[u8]> { + let mut aus = Vec::new(); + let mut cursor = Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + + while let Ok(nalu) = Nalu::next(&mut cursor) { + let header_start = cursor.position() as usize; + let start = header_start - nalu.offset; + let is_slice = (nalu.header.type_ as u32) < 32; + let first_slice_flag = + is_slice && stream.get(header_start + 2).is_some_and(|b| b & 0x80 != 0); + + if au_has_slice && (!is_slice || first_slice_flag) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus + } + + fn convert_stream(stream: &[u8]) -> Vec<(AuPlan, DecodePlanDxvaH265)> { + let mut planner = H265Planner::new(); + let mut slots: Option = None; + let mut out = Vec::new(); + for (i, au) in split_into_aus(stream).into_iter().enumerate() { + let Ok(plan) = planner.plan_au(au) else { + continue; + }; + let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames)); + if map.capacity() != plan.picture.max_dpb_frames + 1 { + *map = SlotMap::new(plan.picture.max_dpb_frames); + } + let dxva = plan_to_dxva_h265(&plan, map, i as u32 + 1).expect("conversion"); + out.push((plan, dxva)); + } + out + } + + #[test] + fn the_whole_vendored_25fps_vector_converts_without_a_refusal() { + let converted = convert_stream(TEST_25FPS); + assert_eq!(converted.len(), 250); + } + + #[test] + fn the_index_arrays_point_at_the_ref_pic_list_entries_the_rps_sets_name() { + for (plan, dxva) in convert_stream(TEST_25FPS) { + for (array, set) in [ + ( + &dxva.pic_params.RefPicSetStCurrBefore, + &plan.rps.st_curr_before, + ), + ( + &dxva.pic_params.RefPicSetStCurrAfter, + &plan.rps.st_curr_after, + ), + (&dxva.pic_params.RefPicSetLtCurr, &plan.rps.lt_curr), + ] { + for (position, entry) in array.iter().enumerate() { + match set.get(position) { + Some(rp) => { + let index = usize::from(*entry); + // The index array points into RefPicList, and + // RefPicList is laid out in `refs` order — so both + // must agree about the picture. + assert_eq!(dxva.refs[index].id, rp.id); + assert_eq!(dxva.pic_params.PicOrderCntValList[index], rp.pic_order_cnt); + assert_eq!( + dxva.pic_params.RefPicList[index].index(), + dxva.refs[index].slot + ); + } + None => assert_eq!(*entry, UNUSED_ENTRY), + } + } + } + } + } + + /// The unique pictures the plan's three CURRENT sets name, in the order this + /// conversion binds them (set order, first appearance first). + fn current_set_ids(plan: &AuPlan) -> Vec { + let mut ids = Vec::new(); + for rp in plan + .rps + .st_curr_before + .iter() + .chain(&plan.rps.st_curr_after) + .chain(&plan.rps.lt_curr) + { + if !ids.contains(&rp.id) { + ids.push(rp.id); + } + } + ids + } + + #[test] + fn the_reference_list_holds_every_marked_picture_and_the_current_sets_lead_it() { + for (plan, dxva) in convert_stream(TEST_25FPS) { + let mut listed: Vec = dxva.refs.iter().map(|r| r.id).collect(); + let mut marked: Vec = plan.dpb_refs.iter().map(|r| r.id).collect(); + listed.sort_unstable(); + marked.sort_unstable(); + assert_eq!(listed, marked, "RefPicList must be the marked DPB"); + // The current sets lead, so the index arrays never point past them — + // which is what makes appending the rest of the DPB safe. + let current = current_set_ids(&plan); + assert_eq!( + dxva.refs + .iter() + .take(current.len()) + .map(|r| r.id) + .collect::>(), + current + ); + } + } + + #[test] + fn a_marked_picture_no_current_set_names_is_appended_after_them() { + // 8.3.2's *Foll* shape, which the vendored vectors never produce (their RPS + // names every marked picture they hold) and which every long-term anchor + // lives in: a picture the DPB keeps marked for a LATER picture. Injected + // into the plan's snapshot rather than synthesised as a bitstream, because + // the thing under test is what this conversion does with a snapshot wider + // than the current sets. + let aus = split_into_aus(TEST_25FPS); + let mut planner = H265Planner::new(); + let plans: Vec = aus + .iter() + .take(3) + .map(|au| planner.plan_au(au).expect("plan")) + .collect(); + let mut slots = SlotMap::new(plans[0].picture.max_dpb_frames); + let baseline: Vec = plans + .iter() + .enumerate() + .map(|(i, plan)| plan_to_dxva_h265(plan, &mut slots, i as u32 + 1).expect("convert")) + .collect(); + let last = baseline.last().expect("three conversions"); + let current_sets = last.refs.len(); + assert!(current_sets > 0, "the third AU must reference something"); + + // Re-plan and re-convert with the third AU's snapshot widened by one marked + // picture that no current set names. + let mut planner = H265Planner::new(); + let mut plans: Vec = aus + .iter() + .take(3) + .map(|au| planner.plan_au(au).expect("plan")) + .collect(); + let mut slots = SlotMap::new(plans[0].picture.max_dpb_frames); + for (i, plan) in plans.iter().take(2).enumerate() { + plan_to_dxva_h265(plan, &mut slots, i as u32 + 1).expect("convert"); + } + // A picture the map already holds and the third AU does not name, else a + // fresh id parked in a free slot — either is a marked DPB entry with a + // surface, which is all `RefPicList` needs. + let setup = plans[2].dpb.stored.expect("stored"); + let named = current_set_ids(&plans[2]); + let existing = slots + .held() + .map(|(_, id)| id) + .find(|id| *id != setup && !named.contains(id)); + let foll = match existing { + Some(id) => id, + None => { + let id = 9_999; + slots + .assign(id) + .expect("a free slot for the synthetic entry"); + id + } + }; + plans[2].dpb_refs.push(RefPic { + id: foll, + pic_order_cnt: -4242, + is_long_term: true, + }); + let dxva = plan_to_dxva_h265(&plans[2], &mut slots, 3).expect("convert"); + + assert_eq!(dxva.refs.len(), current_sets + 1, "appended, not merged"); + let appended = &dxva.refs[current_sets]; + assert_eq!(appended.id, foll); + assert!(appended.is_long_term); + assert_eq!(appended.pic_order_cnt, -4242); + // …and it reaches the wire as a long-term entry with its own POC. + assert_eq!( + dxva.pic_params.RefPicList[current_sets], + PicEntry::new(appended.slot, true) + ); + assert_eq!(dxva.pic_params.PicOrderCntValList[current_sets], -4242); + // The index arrays are untouched by the append: every live index still + // points inside the current sets, which is the property that makes the + // whole-DPB `RefPicList` a safe superset. + for array in [ + dxva.pic_params.RefPicSetStCurrBefore, + dxva.pic_params.RefPicSetStCurrAfter, + dxva.pic_params.RefPicSetLtCurr, + ] { + for entry in array { + assert!( + entry == UNUSED_ENTRY || usize::from(entry) < current_sets, + "an index array reached the appended entry" + ); + } + } + } + + #[test] + fn unused_ref_pic_list_entries_are_the_sentinel_with_a_zero_poc() { + for (_, dxva) in convert_stream(TEST_25FPS) { + for i in dxva.refs.len()..REF_PIC_LIST_LEN { + assert_eq!(dxva.pic_params.RefPicList[i], PicEntry::UNUSED); + assert_eq!(dxva.pic_params.PicOrderCntValList[i], 0); + } + } + } + + #[test] + fn the_current_picture_is_named_by_curr_pic_and_never_aliases_a_reference() { + for (plan, dxva) in convert_stream(TEST_25FPS) { + assert_eq!(dxva.pic_params.CurrPic.index(), dxva.setup_slot); + assert!(!dxva.pic_params.CurrPic.associated()); + assert_eq!( + dxva.pic_params.CurrPicOrderCntVal, + plan.picture.pic_order_cnt + ); + for r in &dxva.refs { + assert_ne!(r.slot, dxva.setup_slot, "a reference aliases the target"); + } + } + } + + #[test] + fn the_irap_picture_sets_all_three_picture_type_flags_and_the_others_set_none() { + let converted = convert_stream(TEST_25FPS); + let (plan, first) = &converted[0]; + assert!(plan.picture.is_irap && plan.picture.is_idr); + let flags = first.pic_params.dwCodingSettingPicturePropertyFlags; + assert_ne!(flags & (1 << 16), 0, "IrapPicFlag"); + assert_ne!(flags & (1 << 17), 0, "IdrPicFlag"); + assert_ne!(flags & (1 << 18), 0, "IntraPicFlag"); + assert!(first.refs.is_empty()); + + let (plan, second) = &converted[1]; + assert!(!plan.picture.is_irap); + let flags = second.pic_params.dwCodingSettingPicturePropertyFlags; + assert_eq!(flags & (0b111 << 16), 0); + assert!(!second.refs.is_empty()); + } + + #[test] + fn the_picture_parameters_carry_the_active_sps_and_pps_verbatim() { + let converted = convert_stream(TEST_25FPS); + let (plan, dxva) = &converted[0]; + let pp = &dxva.pic_params; + let sps = &plan.sps; + let pps = &plan.pps; + assert_eq!( + u32::from(pp.PicWidthInMinCbsY) << sps.min_cb_log2_size_y, + u32::from(sps.pic_width_in_luma_samples) + ); + assert_eq!( + u32::from(pp.PicHeightInMinCbsY) << sps.min_cb_log2_size_y, + u32::from(sps.pic_height_in_luma_samples) + ); + assert_eq!( + pp.log2_min_luma_coding_block_size_minus3, + sps.log2_min_luma_coding_block_size_minus3 + ); + assert_eq!( + pp.log2_diff_max_min_luma_coding_block_size, + sps.log2_diff_max_min_luma_coding_block_size + ); + assert_eq!( + pp.log2_min_transform_block_size_minus2, + sps.log2_min_luma_transform_block_size_minus2 + ); + assert_eq!( + pp.log2_diff_max_min_transform_block_size, + sps.log2_diff_max_min_luma_transform_block_size + ); + assert_eq!( + pp.max_transform_hierarchy_depth_inter, + sps.max_transform_hierarchy_depth_inter + ); + assert_eq!( + pp.max_transform_hierarchy_depth_intra, + sps.max_transform_hierarchy_depth_intra + ); + assert_eq!( + pp.num_short_term_ref_pic_sets, + sps.num_short_term_ref_pic_sets + ); + assert_eq!( + pp.num_long_term_ref_pics_sps, + sps.num_long_term_ref_pics_sps + ); + assert_eq!(pp.init_qp_minus26, pps.init_qp_minus26); + assert_eq!(pp.pps_cb_qp_offset, pps.cb_qp_offset); + assert_eq!(pp.pps_cr_qp_offset, pps.cr_qp_offset); + assert_eq!(pp.pps_beta_offset_div2, pps.beta_offset_div2); + assert_eq!(pp.pps_tc_offset_div2, pps.tc_offset_div2); + assert_eq!( + pp.log2_parallel_merge_level_minus2, + pps.log2_parallel_merge_level_minus2 + ); + assert_eq!( + pp.sps_max_dec_pic_buffering_minus1, + sps.max_dec_pic_buffering_minus1[usize::from(sps.max_sub_layers_minus1)] + ); + assert_eq!(pp.StatusReportFeedbackNumber, 1); + // Reserved fields stay zero, as both the spec and every driver expect. + assert_eq!(pp.ReservedBits2, 0); + assert_eq!(pp.ReservedBits5, 0); + assert_eq!(pp.ReservedBits6, 0); + assert_eq!(pp.ReservedBits7, 0); + } + + #[test] + fn the_format_word_carries_the_streams_chroma_format_and_bit_depths() { + let converted = convert_stream(TEST_25FPS); + let (plan, dxva) = &converted[0]; + let expected = HevcFormatFlags { + chroma_format_idc: plan.picture.chroma_format_idc, + separate_colour_plane_flag: false, + bit_depth_luma_minus8: plan.picture.bit_depth_luma_minus8, + bit_depth_chroma_minus8: plan.picture.bit_depth_chroma_minus8, + log2_max_pic_order_cnt_lsb_minus4: plan.sps.log2_max_pic_order_cnt_lsb_minus4, + } + .pack(); + assert_eq!(dxva.pic_params.wFormatAndSequenceInfoFlags, expected); + // 8-bit 4:2:0: chroma_format_idc 1 at bit 0, both depths zero. + assert_eq!(dxva.pic_params.wFormatAndSequenceInfoFlags & 0x3, 1); + assert_eq!(dxva.pic_params.wFormatAndSequenceInfoFlags >> 3 & 0x7, 0); + assert_eq!(dxva.pic_params.wFormatAndSequenceInfoFlags >> 6 & 0x7, 0); + } + + /// H.265 Table 7-6's default 8x8 list for INTRA prediction (matrixId 0..2), in + /// the up-right diagonal order both the coded syntax and DXVA use. + /// + /// Transcribed from the specification rather than imported from the vendored + /// parser's own constant: a test that reads the same array the code reads + /// cannot tell "the defaults" from "whatever that array happens to hold", and + /// the shape this guards — an enabled sequence that codes no list anywhere — is + /// exactly the one where a wrong default is a picture drifting to flat grey. + const DEFAULT_INTRA_8X8: [u8; 64] = [ + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 16, 17, 16, 17, 18, 17, 18, 18, 17, 18, 21, 19, + 20, 21, 20, 19, 21, 24, 22, 22, 24, 24, 22, 22, 24, 25, 25, 27, 30, 27, 25, 25, 29, 31, 35, + 35, 31, 29, 36, 41, 44, 41, 36, 47, 54, 54, 47, 65, 70, 65, 88, 88, 115, + ]; + /// Table 7-6's default 8x8 list for INTER prediction (matrixId 3..5). + const DEFAULT_INTER_8X8: [u8; 64] = [ + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 20, 20, + 20, 20, 20, 20, 20, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 28, 28, 28, + 28, 28, 28, 33, 33, 33, 33, 33, 41, 41, 41, 41, 54, 54, 54, 71, 71, 91, + ]; + + /// The vector's first plan with its parameter sets rewritten to a chosen + /// scaling-list shape. + /// + /// The parameter sets are the REAL ones the parser produced and only the + /// scaling-list fields move, which is what makes the "coded nowhere" shape + /// meaningful: `pps.scaling_list` then still holds the parser's own default + /// fill, and `sps.scaling_list` still holds the all-zero `ScalingLists:: + /// default()` an uncoded SPS is left with. Synthesising a whole HEVC parameter + /// set would only put those two facts back by hand. + fn plan_with_scaling_lists( + enabled: bool, + sps_coded: Option, + pps_coded: Option, + ) -> (AuPlan, DecodePlanDxvaH265) { + let mut planner = H265Planner::new(); + let aus = split_into_aus(TEST_25FPS); + let mut plan = planner.plan_au(aus[0]).expect("plan"); + + let mut sps = (*plan.sps).clone(); + sps.scaling_list_enabled_flag = enabled; + sps.scaling_list_data_present_flag = sps_coded.is_some(); + if let Some(fill) = sps_coded { + sps.scaling_list.scaling_list_4x4 = [[fill; 16]; 6]; + sps.scaling_list.scaling_list_8x8 = [[fill; 64]; 6]; + sps.scaling_list.scaling_list_16x16 = [[fill; 64]; 6]; + sps.scaling_list.scaling_list_32x32 = [[fill; 64]; 6]; + sps.scaling_list.scaling_list_dc_coef_minus8_16x16 = [i16::from(fill); 6]; + sps.scaling_list.scaling_list_dc_coef_minus8_32x32 = [i16::from(fill); 6]; + } + let mut pps = (*plan.pps).clone(); + pps.scaling_list_data_present_flag = pps_coded.is_some(); + if let Some(fill) = pps_coded { + pps.scaling_list.scaling_list_4x4 = [[fill; 16]; 6]; + pps.scaling_list.scaling_list_8x8 = [[fill; 64]; 6]; + pps.scaling_list.scaling_list_16x16 = [[fill; 64]; 6]; + pps.scaling_list.scaling_list_32x32 = [[fill; 64]; 6]; + pps.scaling_list.scaling_list_dc_coef_minus8_16x16 = [i16::from(fill); 6]; + pps.scaling_list.scaling_list_dc_coef_minus8_32x32 = [i16::from(fill); 6]; + } + plan.sps = std::rc::Rc::new(sps); + plan.pps = std::rc::Rc::new(pps); + + let mut slots = SlotMap::new(plan.picture.max_dpb_frames); + let dxva = plan_to_dxva_h265(&plan, &mut slots, 1).expect("convert"); + (plan, dxva) + } + + #[test] + fn a_sequence_that_disables_scaling_lists_submits_no_quantization_matrix_at_all() { + // The shape every punktfunk HEVC stream is in, and the vendored vector + // with it. libavcodec's `dxva2_hevc_end_frame` passes the buffer only when + // `dwCodingParamToolFlags & 1`; handing a driver a matrix it was told to + // ignore is a bet on the driver ignoring it. + let converted = convert_stream(TEST_25FPS); + let (plan, dxva) = &converted[0]; + assert!(!plan.sps.scaling_list_enabled_flag); + assert_eq!(dxva.pic_params.dwCodingParamToolFlags & 1, 0); + assert_eq!(dxva.qmatrix, None); + for (_, dxva) in &converted { + assert_eq!(dxva.qmatrix, None); + } + } + + #[test] + fn an_enabled_sequence_that_codes_no_list_anywhere_gets_the_table_7_5_and_7_6_defaults() { + // The bug this guards: the vendored parser leaves an SPS that codes no + // scaling list data with an ALL-ZERO `ScalingLists` (unlike libavcodec's, + // which seeds the defaults), so selecting the SPS here would dequantize + // every residual to nothing while bit 0 of dwCodingParamToolFlags tells the + // driver the matrix is authoritative. + let (plan, dxva) = plan_with_scaling_lists(true, None, None); + assert!(plan.sps.scaling_list.scaling_list_4x4[0] + .iter() + .all(|&v| v == 0)); + assert_eq!(dxva.pic_params.dwCodingParamToolFlags & 1, 1); + let qm = dxva + .qmatrix + .expect("an enabled sequence submits the matrix"); + + // Table 7-5: every 4x4 list is flat 16. + for list in qm.ucScalingLists0 { + assert_eq!(list, [16u8; 16]); + } + // Table 7-6: matrixId 0..2 are the intra default, 3..5 the inter one, at + // every size from 8x8 up. + for (m, list) in qm.ucScalingLists1.iter().enumerate() { + let want = if m < 3 { + DEFAULT_INTRA_8X8 + } else { + DEFAULT_INTER_8X8 + }; + assert_eq!(*list, want, "8x8 matrixId {m}"); + } + for (m, list) in qm.ucScalingLists2.iter().enumerate() { + let want = if m < 3 { + DEFAULT_INTRA_8X8 + } else { + DEFAULT_INTER_8X8 + }; + assert_eq!(*list, want, "16x16 matrixId {m}"); + } + // sizeId 3 carries only matrixId 0 (intra) and 3 (inter). + assert_eq!(qm.ucScalingLists3[0], DEFAULT_INTRA_8X8); + assert_eq!(qm.ucScalingLists3[1], DEFAULT_INTER_8X8); + // The inferred DC is 8, and DXVA takes the VALUE — 8 + 8. + assert_eq!(qm.ucScalingListDCCoefSizeID2, [16u8; 6]); + assert_eq!(qm.ucScalingListDCCoefSizeID3, [16u8; 2]); + } + + #[test] + fn a_coded_pps_list_wins_and_a_coded_sps_list_is_taken_only_when_the_pps_codes_none() { + // 7.4.5's activation order, with a distinct fill per source so the + // selection is visible rather than inferred. + let (_, pps_wins) = plan_with_scaling_lists(true, Some(7), Some(9)); + let qm = pps_wins.qmatrix.expect("enabled"); + assert_eq!(qm.ucScalingLists0[0], [9u8; 16]); + assert_eq!(qm.ucScalingLists1[2], [9u8; 64]); + assert_eq!(qm.ucScalingLists2[5], [9u8; 64]); + assert_eq!(qm.ucScalingLists3[0], [9u8; 64]); + assert_eq!(qm.ucScalingLists3[1], [9u8; 64]); + // The DC entries are the coded delta plus 8, and sizeId 3 takes the + // parser's matrixId 0 and 3 rather than 0 and 1. + assert_eq!(qm.ucScalingListDCCoefSizeID2, [17u8; 6]); + assert_eq!(qm.ucScalingListDCCoefSizeID3, [17u8; 2]); + + let (_, sps_wins) = plan_with_scaling_lists(true, Some(7), None); + let qm = sps_wins.qmatrix.expect("enabled"); + assert_eq!(qm.ucScalingLists0[0], [7u8; 16]); + assert_eq!(qm.ucScalingLists1[2], [7u8; 64]); + assert_eq!(qm.ucScalingListDCCoefSizeID2, [15u8; 6]); + + // …and neither source reaches the driver when the sequence disables + // scaling lists, however much data the parameter sets carry. + let (_, disabled) = plan_with_scaling_lists(false, Some(7), Some(9)); + assert_eq!(disabled.qmatrix, None); + } + + #[test] + fn the_sizeid_3_entries_take_the_parsers_matrix_ids_0_and_3() { + // The index arithmetic (`k * 3`) that would otherwise be invisible: with + // matrixId 0 and 3 given different values, taking 0 and 1 reads the wrong + // matrix for the inter slot. + let (_, dxva) = { + let mut planner = H265Planner::new(); + let aus = split_into_aus(TEST_25FPS); + let mut plan = planner.plan_au(aus[0]).expect("plan"); + let mut sps = (*plan.sps).clone(); + sps.scaling_list_enabled_flag = true; + let mut pps = (*plan.pps).clone(); + pps.scaling_list_data_present_flag = true; + let mut lists = [[0u8; 64]; 6]; + let mut dc = [0i16; 6]; + for (m, list) in lists.iter_mut().enumerate() { + *list = [(m as u8 + 1) * 10; 64]; + dc[m] = m as i16 + 1; + } + pps.scaling_list.scaling_list_32x32 = lists; + pps.scaling_list.scaling_list_dc_coef_minus8_32x32 = dc; + plan.sps = std::rc::Rc::new(sps); + plan.pps = std::rc::Rc::new(pps); + let mut slots = SlotMap::new(plan.picture.max_dpb_frames); + let dxva = plan_to_dxva_h265(&plan, &mut slots, 1).expect("convert"); + (plan, dxva) + }; + let qm = dxva.qmatrix.expect("enabled"); + assert_eq!(qm.ucScalingLists3[0], [10u8; 64], "matrixId 0"); + assert_eq!(qm.ucScalingLists3[1], [40u8; 64], "matrixId 3"); + assert_eq!(qm.ucScalingListDCCoefSizeID3, [1 + 8, 4 + 8]); + } + + #[test] + fn the_b_frame_vector_converts_and_binds_both_directions_of_its_rps() { + let converted = convert_stream(TEST_64X64_I_P_B_P); + assert!(!converted.is_empty()); + // The B picture of I-P-B-P has references on both sides, so at least one + // AU must populate both StCurrBefore and StCurrAfter. + let both = converted.iter().any(|(plan, _)| { + !plan.rps.st_curr_before.is_empty() && !plan.rps.st_curr_after.is_empty() + }); + assert!(both, "the I-P-B-P vector must exercise bidirectional RPS"); + for (plan, dxva) in &converted { + // Everything the slices name is bound; that is the cross-check the + // conversion enforces, asserted here from the outside. + for slice in &plan.slices { + for rp in slice.ref_list0.iter().chain(&slice.ref_list1) { + assert!(dxva.refs.iter().any(|r| r.id == rp.id)); + } + } + } + } + + #[test] + fn slice_ranges_ride_through_in_plan_order_on_start_code_boundaries() { + let aus = split_into_aus(TEST_25FPS); + let mut planner = H265Planner::new(); + let mut slots: Option = None; + for (i, au) in aus.iter().enumerate() { + let plan = planner.plan_au(au).expect("plan"); + let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames)); + let dxva = plan_to_dxva_h265(&plan, map, i as u32 + 1).expect("convert"); + assert_eq!(dxva.slice_ranges.len(), plan.slices.len()); + for range in &dxva.slice_ranges { + let at = &au[range.start..]; + assert!(at.starts_with(&[0, 0, 1]) || at.starts_with(&[0, 0, 0, 1])); + } + } + } + + #[test] + fn a_capacity_mismatch_is_refused_and_leaves_the_map_untouched() { + let aus = split_into_aus(TEST_25FPS); + let mut planner = H265Planner::new(); + let plan = planner.plan_au(aus[0]).expect("plan"); + let mut slots = SlotMap::new(plan.picture.max_dpb_frames + 1); + assert_eq!( + plan_to_dxva_h265(&plan, &mut slots, 1), + Err(PlanToDxvaH265Error::CapacityMismatch { + required: plan.picture.max_dpb_frames + 1, + capacity: plan.picture.max_dpb_frames + 2, + }) + ); + assert_eq!(slots.active(), 0); + } + + #[test] + fn a_reference_the_map_never_saw_is_refused_and_leaves_the_map_untouched() { + let aus = split_into_aus(TEST_25FPS); + let mut planner = H265Planner::new(); + let first = planner.plan_au(aus[0]).expect("plan 0"); + let second = planner.plan_au(aus[1]).expect("plan 1"); + let mut slots = SlotMap::new(second.picture.max_dpb_frames); + let missing = second.rps.st_curr_before[0].id; + assert_eq!(first.dpb.stored, Some(missing)); + assert_eq!( + plan_to_dxva_h265(&second, &mut slots, 1), + Err(PlanToDxvaH265Error::UnresolvedReference(missing)) + ); + assert_eq!(slots.active(), 0); + } + + #[test] + fn slice_control_records_carry_the_packers_locations_verbatim() { + let records = [ + crate::pack::SliceRecord { + location: 0, + bytes: 128, + }, + crate::pack::SliceRecord { + location: 128, + bytes: 256, + }, + ]; + let control = slice_control_h265(&records); + // Read by VALUE, in braces: the record is `#[repr(C, packed)]` (ten bytes), + // so a reference to a `u32` member would be unaligned — and `assert_eq!` + // takes references. See `dxva.rs`'s alignment section. + assert_eq!({ control[0].BSNALunitDataLocation }, 0); + assert_eq!({ control[0].SliceBytesInBuffer }, 128); + assert_eq!({ control[0].wBadSliceChopping }, 0); + // A SECOND record, because the ten-vs-twelve byte defect is invisible on a + // single-record buffer — the vendored HEVC vector is one slice segment per + // picture, which is precisely the shape that hid it. + assert_eq!({ control[1].BSNALunitDataLocation }, 128); + let bytes = crate::dxva::slice_bytes(&control); + assert_eq!(bytes.len(), 20); + assert_eq!(&bytes[10..14], &128u32.to_le_bytes()); + assert_eq!(&bytes[14..18], &256u32.to_le_bytes()); + } + + #[test] + fn a_slot_is_reused_only_after_its_picture_leaves_the_dpb() { + let aus = split_into_aus(TEST_25FPS); + let mut planner = H265Planner::new(); + let mut slots: Option = None; + let mut live: Vec<(PicId, u8)> = Vec::new(); + for (i, au) in aus.iter().enumerate() { + let Ok(plan) = planner.plan_au(au) else { + continue; + }; + let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames)); + let removed = plan.dpb.removed.clone(); + let dxva = plan_to_dxva_h265(&plan, map, i as u32 + 1).expect("convert"); + live.retain(|&(id, _)| !removed.contains(&id)); + assert!( + live.iter().all(|&(_, slot)| slot != dxva.setup_slot), + "AU {i} decodes into a surface a live picture still holds" + ); + live.push((dxva.setup_id, dxva.setup_slot)); + } + } +} diff --git a/crates/pf-dxvadec/tests/libav_picparams_parity.rs b/crates/pf-dxvadec/tests/libav_picparams_parity.rs new file mode 100644 index 00000000..f5b770e9 --- /dev/null +++ b/crates/pf-dxvadec/tests/libav_picparams_parity.rs @@ -0,0 +1,3180 @@ +//! The parity harness: pf-dxvadec's DXVA submission against libavcodec's, for the same access +//! units of the same elementary stream — picture parameters, quantization matrices AND the +//! buffer descriptors, for both codecs. +//! +//! (The file is still called `libav_picparams_parity` although it long since outgrew the picture +//! parameters: the name is what the milestone's plan, the capture recipe below and the gate +//! `cargo test -p pf-dxvadec --test libav_picparams_parity` all say, and a rename would cost +//! every one of those a stale reference to buy nothing.) +//! +//! # Why this exists +//! +//! M3's WP-D closed the native Vulkan rung by comparing DECODED PIXELS against libavcodec — +//! 250 AUs, bit-exact, three drivers. This milestone has no equivalent. Every claim it makes +//! about the DXVA structures rests on reading the specification and reading libavcodec, and +//! reading is exactly the method that produced the defects review 13 found: a quantization +//! matrix submitted unconditionally, a `NumMBsInBuffer` left at zero, a `RefFrameList` holding +//! the wrong set. None of those is visible in a smoke test, and all three are visible in one +//! comparison against the path every Windows player exercises. +//! +//! Two halves, and the split matters: +//! +//! * **What needs no capture at all** — the descriptor set's internal consistency, that the +//! quantization-matrix buffer is submitted exactly when the stream has one, that +//! `NumMBsInBuffer` is `mb_width * mb_height` for H.264 and 0 for HEVC, that the slice +//! records tile the bitstream buffer exactly. Those are ORDINARY tests here, not `#[ignore]`d, +//! because they run on any host over every AU of the vendored vectors. Two of review 13's +//! three structural defects would have failed one of them. +//! * **What needs libavcodec's own bytes** — the picture parameters, the matrices' contents, +//! and the descriptor VALUES as libav computes them. Those tests are `#[ignore]`d because they +//! need a capture the repository cannot carry: a patched FFmpeg on a Windows box with a +//! D3D11VA-capable GPU. +//! +//! # Capturing the libavcodec side +//! +//! Verified against **FFmpeg n8.1**, which is the version the Windows CI runs; the names below +//! are that tree's (they changed — the fill functions are non-static and codec-prefixed now). +//! On the Windows box (192.168.1.173 — see the box notes for the ssh identity), with an FFmpeg +//! source tree: +//! +//! **1. One AU counter, shared by every line.** In `libavcodec/dxva2.c`, at file scope above +//! `ff_dxva2_commit_buffer` (dxva2.c:802): +//! +//! ```c +//! static unsigned pf_au_index; +//! ``` +//! +//! and bump it exactly once per picture, at the TOP of `ff_dxva2_common_end_frame` — that +//! function runs once per submitted picture, so `pf_au_index` is the same number for all of one +//! AU's lines: +//! +//! ```c +//! const unsigned pf_au = pf_au_index++; /* first line of the function body */ +//! ``` +//! +//! (The lines below that live in other functions read the file-scope `pf_au_index - 1`; the +//! block for each says which.) Two assumptions, both of which the harness's preflight catches if +//! they fail: the DXVA hwaccel decodes one picture at a time (`start_frame` → `decode_slice`* → +//! `end_frame`, no frame threading), and no picture is refused BETWEEN its +//! `fill_picture_parameters` and its `ff_dxva2_common_end_frame` — a codec-level `end_frame` that +//! returns early on `slice_count <= 0` would log a `PFPP` line with no matching descriptors, and +//! the preflight refuses a capture whose AU indices are not exactly `0..250`. +//! +//! **2. The buffer descriptors.** `ff_dxva2_commit_buffer` (dxva2.c:802) is the choke point for +//! three of the four buffers — it writes `dsc11->BufferType/DataSize/NumMBsInBuffer` at +//! dxva2.c:836-840. Immediately AFTER that write: +//! +//! ```c +//! av_log(NULL, AV_LOG_INFO, "PFBD %s %u %u %u %u %u\n", +//! avcodec_get_name(avctx->codec_id), pf_au_index - 1, +//! (unsigned)type, (unsigned)size, (unsigned)mb_count, 0u); +//! ``` +//! +//! ⚠ **The BITSTREAM descriptor does NOT pass through that function** — the bitstream buffer is +//! packed in place, so each codec's `commit_bitstream_and_slice_buffer` fills its descriptor +//! itself (`dxva2_h264.c:412` D3D11 / `:425` DXVA2, `dxva2_hevc.c:338` / `:349`). Add the same +//! line after each of those two fills, with the codec spelled literally: +//! +//! ```c +//! av_log(NULL, AV_LOG_INFO, "PFBD h264 %u 6 %u %u 0\n", +//! pf_au_index - 1, (unsigned)current, mb_count); /* dxva2_h264.c */ +//! av_log(NULL, AV_LOG_INFO, "PFBD hevc %u 6 %u 0 0\n", +//! pf_au_index - 1, (unsigned)current); /* dxva2_hevc.c */ +//! ``` +//! +//! A capture whose AUs carry three `PFBD` lines instead of four is this patch site missed, and +//! the harness says so by name rather than reporting a missing buffer as a defect. +//! +//! **3. The picture parameters.** At the very END of `ff_dxva2_h264_fill_picture_parameters` +//! (`dxva2_h264.c:51`) — after the `RefFrameList` loop and the `UsedForReferenceFlags` writes, +//! so every field is final: +//! +//! ```c +//! { +//! const uint8_t *raw = (const uint8_t *)pp; +//! char line[2 * sizeof(*pp) + 1]; +//! unsigned i; +//! for (i = 0; i < sizeof(*pp); i++) +//! snprintf(line + 2 * i, 3, "%02x", raw[i]); +//! av_log(NULL, AV_LOG_INFO, "PFPP h264 %u %s\n", pf_au_index, line); +//! } +//! ``` +//! +//! `pf_au_index` un-decremented here on purpose: `fill_picture_parameters` runs from +//! `start_frame`, BEFORE `ff_dxva2_common_end_frame` bumps the counter for the same picture. +//! The identical block goes at the end of `ff_dxva2_hevc_fill_picture_parameters` +//! (`dxva2_hevc.c:60`) with `h264` replaced by `hevc`. +//! +//! **4. The quantization matrices, including whether they are submitted at all.** In +//! `ff_dxva2_common_end_frame`, where its `qm`/`qm_size` arguments are in scope (that is where +//! the codec's decision arrives: `dxva2_h264.c:513-516` passes `&ctx_pic->qm` with `sizeof(qm)` +//! UNCONDITIONALLY, while `dxva2_hevc.c:417,423-426` passes `NULL`/0 unless +//! `pp.dwCodingParamToolFlags & 1`): +//! +//! ```c +//! if (qm_size > 0) { +//! const uint8_t *raw = qm; +//! char *line = av_malloc(2 * qm_size + 1); +//! unsigned i; +//! for (i = 0; i < qm_size; i++) +//! snprintf(line + 2 * i, 3, "%02x", raw[i]); +//! av_log(NULL, AV_LOG_INFO, "PFQM %s %u %s\n", +//! avcodec_get_name(avctx->codec_id), pf_au, line); +//! av_free(line); +//! } else { +//! av_log(NULL, AV_LOG_INFO, "PFQM %s %u absent\n", +//! avcodec_get_name(avctx->codec_id), pf_au); +//! } +//! ``` +//! +//! The `absent` spelling is required rather than an omitted line: an omitted line is +//! indistinguishable from a missed patch, and "was the buffer submitted" is the single fact +//! review 13's HEVC defect turned on. +//! +//! **5. The slice-control format.** One line per AU (or one for the whole run — the parser takes +//! either), from the same place, so an inverted short/long-format number cannot pass unseen: +//! +//! ```c +//! av_log(NULL, AV_LOG_INFO, "PFCFG %s %u %u\n", +//! avcodec_get_name(avctx->codec_id), pf_au, +//! (unsigned)DXVA_CONTEXT_CFG_BITSTREAM(avctx, ctx)); +//! ``` +//! +//! `ConfigBitstreamRaw`'s short-format value is **2 for H.264 and 1 for HEVC** — one number +//! with two spellings, and an inverted pair swaps which slice-control STRUCT the driver reads +//! while every other byte still looks right. If the macro is spelled differently in the tree, +//! any expression yielding the negotiated config's `ConfigBitstreamRaw` will do. +//! +//! **6. Run it.** `--enable-d3d11va` is on by default on Windows. Decode the SAME elementary +//! streams this test plans — the vendored vectors, in the repository at +//! `crates/pf-bitstream/vendor/cros-codecs/src/codec/{h264,h265}/test_data/test-25fps.{h264,h265}`: +//! +//! ```text +//! ffmpeg -hwaccel d3d11va -hwaccel_output_format d3d11 -i test-25fps.h264 -f null - 2> h264.log +//! ffmpeg -hwaccel d3d11va -hwaccel_output_format d3d11 -i test-25fps.h265 -f null - 2> hevc.log +//! grep -oE 'PF(PP|QM|BD|CFG) .*' h264.log > libav-h264.capture +//! grep -oE 'PF(PP|QM|BD|CFG) .*' hevc.log > libav-hevc.capture +//! ``` +//! +//! `av_log(NULL, …)` rather than `av_log(avctx, …)` throughout, and `grep -o` rather than an +//! anchored match, for one reason: FFmpeg's logger prefixes a message logged against a context +//! with `[h264 @ 0x…] `, which no anchored grep would match. The parser finds its marker anywhere +//! in a line, so a capture made either way is readable — but the flat form is what the recipe +//! asks for, because a capture that greps cleanly is a capture whose format can be eyeballed. +//! +//! A software fallback produces no lines at all, which is the check that the hwaccel actually +//! engaged: 250 `PFPP` lines per stream or the capture is void. Then: +//! +//! ```text +//! PF_LIBAV_CAPTURE_H264=libav-h264.capture PF_LIBAV_CAPTURE_HEVC=libav-hevc.capture \ +//! cargo test -p pf-dxvadec --test libav_picparams_parity -- --ignored --nocapture +//! ``` +//! +//! `PF_DXVA_DUMP=` writes THIS side in the same format, both codecs, without needing a +//! capture — so the two files can also be diffed by hand. +//! +//! # Differences that are EXPECTED, and must not be read as defects +//! +//! A raw `memcmp` of the picture parameters differs by design, which is why this harness does +//! not do one. Each expected divergence is handled STRUCTURALLY instead, so what is left over is +//! a finding: +//! +//! * **Surface indices.** `CurrPic` and every reference entry carry a decode-surface index. +//! libavcodec's comes from its own frame pool's allocation order; ours from +//! [`pf_dxvadec::SlotMap`]. The two are a BIJECTION over the same pictures, never equal +//! numbers. The harness therefore tracks the mapping per PICTURE — identified by the pair-key +//! DXVA itself resolves references by — and reports only a mapping that CHANGES while the +//! picture is still in the DPB, or two live pictures collapsing onto one surface. A differing +//! index alone proves nothing; an index that stops agreeing does. +//! * **Reference-array ORDER.** For H.264 libavcodec emits `short_ref` then `long_ref`; this +//! crate emits the AU's own references first and the rest of the marked DPB after (see +//! `pic.rs`'s module docs for why). For HEVC libavcodec walks its DPB array in slot order +//! while this crate leads with the three current RPS sets (`pic_h265.rs`'s docs). Both are +//! correct — DXVA imposes no order, a driver resolves an entry by its keys — so the arrays are +//! compared as SETS of `(marking, key, POC, use-flags)` tuples, with the per-entry bits of +//! `UsedForReferenceFlags`/`NonExistingFrameFlags` carried inside each tuple, which is what +//! "re-indexed to the compared order" amounts to. +//! * **HEVC's RPS index arrays** (`RefPicSetStCurrBefore`/`StCurrAfter`/`LtCurr`) hold INDICES +//! into `RefPicList`, so a different array order means different index VALUES for the same +//! pictures. They are compared by resolving each index through its own side's `RefPicList` and +//! comparing the PICTURES named, position by position — position order is 8.3.4's and does +//! matter. +//! * **The bitstream buffer's `DataSize`** may legitimately differ; see the descriptor section +//! below, which states exactly how a legitimate difference is told from a defect. (On the +//! capture described below it does not differ at all, on any of 500 AUs.) +//! * **libavcodec's POC BASE.** Measured, not predicted: every `CurrFieldOrderCnt` and +//! `FieldOrderCntList` value in the H.264 capture is the specification's plus exactly **65536** +//! — FFmpeg seeds `prev_poc_msb = 1 << 16` at each IDR. The progression is identical; only the +//! base differs, and it is uniform across the current picture and every reference entry, so +//! every difference a driver computes from these fields (temporal direct, implicit weighted +//! prediction, co-located selection) is unaffected. This crate keeps 8.2.1's values and the +//! harness compares POCs RELATIVE to a base it derives from the first AU and then REQUIRES of +//! every AU after it — so a genuinely wrong POC still reports. libavcodec's HEVC POCs carry no +//! such offset (measured: base 0 on all 250 AUs). See `PocBase`. +//! * **HEVC `loop_filter_across_tiles_enabled_flag`** (bit 10 of +//! `dwCodingSettingPicturePropertyFlags`): ours 1, libavcodec's 0, on all 250 AUs. 7.4.3.3.1 +//! infers 1 when the PPS codes no tiles, which is what the vendored parser reports; libav's +//! parser evidently leaves it 0. Inert either way — with `tiles_enabled_flag` clear there is no +//! tile boundary for a loop filter to cross — so it is DOCUMENTED rather than changed: matching +//! libav would mean overriding a spec inference on the strength of one measurement of another +//! decoder's parser default. The allowance is exactly bit 10, exactly ours-set-theirs-clear, and +//! only while both sides agree tiles are off; see `hevc_allowance`. +//! +//! The last two are reported on every run as DOCUMENTED divergences with their AU counts, never +//! silently dropped, and each one's allowance is narrow enough that the next difference in the same +//! field is still a finding — which two non-ignored tests +//! (`libavcodecs_constant_poc_base_is_documented_and_anything_else_about_a_poc_is_a_finding`, +//! `the_hevc_tiles_flag_allowance_is_exactly_bit_ten_with_tiles_disabled_and_nothing_else`) prove +//! by synthesising the differences an allowance must NOT absorb. +//! +//! Everything else — every parameter-set field, every flag word, `frame_num`, `ContinuationFlag`, +//! `StatusReportFeedbackNumber` (both count from 1 per picture), and every reserved field — must +//! match byte for byte, and a difference there is the finding this harness exists to produce. +//! +//! # What the first real run said +//! +//! Run on 2026-08-06 against a patched FFmpeg n8.1 capture from the RTX 4090 box, 250 AUs per +//! codec, both vendored vectors. **Four comparisons, zero undocumented divergences**: H.264 +//! picture parameters, HEVC picture parameters, the buffer descriptors of both codecs, and the +//! quantization matrices of both. The two divergences above were the entire delta. +//! +//! The measured ground truth, so the next reader needs no capture to know what libavcodec emits: +//! +//! | | H.264 | HEVC | +//! |---|---|---| +//! | picture parameters | 1040 bytes | 232 bytes | +//! | `ConfigBitstreamRaw` | 2 | 1 | +//! | IQ matrix | submitted on all 250 (224 bytes) | `absent` on all 250 | +//! | descriptors per AU | 4 (types 0, 4, 6, 5) | 3 (types 0, 6, 5) | +//! | `SLICE_CONTROL.DataSize` | 20 (2 slices × 10) | 10 (1 slice × 10) | +//! | `BITSTREAM.DataSize` | 256..6272, all ≡ 0 (mod 128) | 128..8320, all ≡ 0 (mod 128) | +//! | `NumMBsInBuffer` | 300 on BITSTREAM and SLICE_CONTROL, 0 on the other two | 0 on all | +//! | `DataOffset` | 0 on all | 0 on all | +//! +//! Three things that settles beyond this harness: the short slice record is **ten** bytes (20/2 and +//! 10/1, two codecs and two slice counts agreeing); every `BITSTREAM.DataSize` matches this crate's +//! packer exactly, so the start-code/rebase/padding rules were right; and the HEVC vector exercises +//! case 1 of the quantization matrix's three cases (`scaling_list_enabled_flag` clear) — cases 2 +//! and 3 remain CPU-only, which is stated rather than papered over. +//! +//! ## The two libavcodec workarounds, and why a capture can be VOID +//! +//! `Reserved16Bits = 3` is the notable field: libavcodec writes 3 for every standard profile and +//! 0 only under one of two workarounds, both of which also change other bytes. +//! +//! * `FF_DXVA2_WORKAROUND_INTEL_CLEARVIDEO` is set iff the negotiated decoder GUID is the legacy +//! `ff_DXVADDI_Intel_ModeH264_E` (dxva2.c:302-303), and it changes two H.264 things +//! (dxva2_h264.c:128 and :257). **Our side cannot select that GUID**: config.rs's table holds +//! three standard GUIDs — [`pf_dxvadec::H264_VLD_NOFGT`] is `DXVA2_ModeH264_E` — and +//! `video_d3d11_native.rs` asks the device for nothing else. The exposure is entirely on the +//! CAPTURE side: on an old Intel part, the FFmpeg producing the capture may negotiate +//! ClearVideo, and then its bytes are not ours to compare against. ⚠ Worth re-reading at the +//! Intel bring-up: modern parts negotiate the standard GUID, so this should not fire — but +//! "should not" is what a preflight is for. +//! * `FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG` (old ATI/AMD UVD) is never auto-set in the modern +//! hwaccel path — it is user-set through the legacy context only — so libav takes the +//! `ff_zigzag_scan`-indexed branch of `ff_dxva2_h264_fill_scaling_lists`, which emits the +//! matrices in CODED (zig-zag) order. That is the order this crate's matrices are already in +//! (the vendored parser stores each list as coded), which is what makes the H.264 +//! quantization-matrix comparison a straight byte compare. +//! +//! A capture whose `Reserved16Bits` is 0 was therefore made against a workaround path and is VOID +//! for comparison; the harness refuses it up front rather than reporting 250 findings. +//! +//! # The buffer descriptors +//! +//! [`pf_dxvadec::descriptors`] models them, and its module docs carry the value table and the +//! libavcodec citation for every field. What matters for a COMPARISON: +//! +//! * `CompressedBufferType` (D3D11's `BufferType`), the buffer SET and its ORDER cannot +//! legitimately differ. A type present on one side only is a finding — and for HEVC's +//! quantization matrix that finding IS review 13's defect. +//! * `DataOffset` is 0 on both sides, always. +//! * `NumMBsInBuffer` cannot legitimately differ: it is `mb_width * mb_height` on H.264's +//! bitstream and slice-control buffers, 0 everywhere else and on all of HEVC's. +//! * `DataSize` for the picture parameters, the matrices and the slice control cannot +//! legitimately differ either — they are `sizeof` a structure and `slices * 10` (the short +//! slice record is TEN bytes, packed; see `dxva.rs`'s alignment section for the measurement and +//! for what twelve would have cost). The slice control's size is therefore also a slice COUNT: +//! if it differs, the two sides disagree about how many slices the AU has, which voids that +//! AU's bitstream comparison and is reported as its own finding. +//! * `DataSize` for the BITSTREAM buffer is the one field with a legitimate divergence class. +//! Both sides pack slice NALUs only, each behind a normalised three-byte start code, and pad +//! the total to 128 bytes — this crate because the DXVA specs say so and because non-VCL NALUs +//! inside the decode range hang AMD's VCN firmware (the same discipline pf-vkdecode's +//! recording layer follows), libavcodec in `commit_bitstream_and_slice_buffer` for its own +//! reasons. So the sizes normally match exactly. They may differ by a FEW bytes per slice when +//! the two NALU splitters delimit a slice differently — trailing `zero_byte`s ahead of the next +//! start code belong to neither NALU, and a splitter may keep or drop them. That difference is +//! legitimate, and it is recognisable: the slice COUNT agrees, and the difference is under four +//! bytes per slice before padding. Anything else — a differing slice count, a size that is not +//! a multiple of 128 (which means the driver's mapping was too small for the padding), a +//! difference of hundreds of bytes — is a defect, and the harness classifies the two cases +//! apart rather than lumping them into one "DataSize differs". +//! +//! # Provenance, and what a reviewer must re-check +//! +//! There is no FFmpeg source in this worktree, so nothing here can verify a claim about +//! libavcodec. Two tiers, and the difference matters: +//! +//! * **Read out of an FFmpeg n8.1 tree** by this work package's coordinator: the function names +//! and every `file:line`, the qmatrix predicate's codec-asymmetry, the `NumMBsInBuffer` +//! asymmetry, and both workaround conditions. +//! * **Read out of the same tree, then CONFIRMED by the capture**: that +//! `commit_bitstream_and_slice_buffer` writes a three-byte start code ahead of each slice, +//! rebases `BSNALunitDataLocation` into the buffer, counts the start code in +//! `SliceBytesInBuffer`, pads with `FFMIN(128 - ((current - dxva_data) & 127), end - current)` +//! and charges that padding to the LAST record (`slice->SliceBytesInBuffer += padding`). This was +//! flagged as an unverified assumption in the first revision of this file, because the +//! `BITSTREAM.DataSize` classification rests on it; the capture then matched this crate's packed +//! size on all 500 AUs of both codecs, which is that assumption's proof. +//! +//! Everything in either tier is a contract this harness parses and compares against, not a fact +//! it establishes. The capture is the authority; a disagreement between a capture and a claim +//! above is a claim to fix. + +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::fmt::Write as _; +use std::io::Cursor; +use std::mem::offset_of; +use std::mem::size_of; +use std::ops::Range; + +use pf_dxvadec::descriptors::BUFFER_BITSTREAM; +use pf_dxvadec::descriptors::BUFFER_INVERSE_QUANTIZATION_MATRIX; +use pf_dxvadec::descriptors::BUFFER_PICTURE_PARAMETERS; +use pf_dxvadec::descriptors::BUFFER_SLICE_CONTROL; +use pf_dxvadec::dxva::PicParamsH264; +use pf_dxvadec::dxva::PicParamsHevc; +use pf_dxvadec::dxva::QmatrixH264; +use pf_dxvadec::dxva::QmatrixHevc; +use pf_dxvadec::dxva::SliceH264Short; +use pf_dxvadec::dxva::SliceHevcShort; +use pf_dxvadec::dxva::UNUSED_ENTRY; +use pf_dxvadec::AuPlan; +use pf_dxvadec::BufferDescriptor; +use pf_dxvadec::Codec; +use pf_dxvadec::H264Planner; +use pf_dxvadec::H265Planner; +use pf_dxvadec::SliceRecord; +use pf_dxvadec::SlotMap; + +const TEST_25FPS_H264: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264" +); +const TEST_25FPS_H265: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265" +); + +/// Both vendored vectors carry exactly this many access units — pf-bitstream's own golden, and +/// the number of `PFPP` lines a valid capture holds. +const VENDORED_AUS: usize = 250; + +/// A generous stand-in for the driver's bitstream mapping. Real mappings are a few MiB; the +/// vendored vectors are 320x240, so nothing here comes close to the tail-padding clamp (which +/// [`pf_dxvadec::pack`]'s own unit tests cover). +const MAPPING_BYTES: usize = 1 << 20; + +// --------------------------------------------------------------------------- +// Access-unit splitting +// --------------------------------------------------------------------------- + +/// The same AU splitter every H.264 test in this program uses: a new AU starts at a non-slice +/// NALU following a slice, or at a slice whose `first_mb_in_slice` is 0 following a slice. +fn split_into_aus(stream: &[u8]) -> Vec<&[u8]> { + use cros_codecs::codec::h264::parser::Nalu; + use cros_codecs::codec::h264::parser::NaluType; + + let mut aus = Vec::new(); + let mut cursor = Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + + while let Ok(nalu) = Nalu::next(&mut cursor) { + let nalu_offset = cursor.position() as usize; + let start = nalu_offset - nalu.offset; + let is_slice = matches!(nalu.header.type_, NaluType::Slice | NaluType::SliceIdr); + let first_mb_zero = is_slice && stream.get(nalu_offset + 1).is_some_and(|b| b & 0x80 != 0); + + if au_has_slice && (!is_slice || first_mb_zero) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus +} + +/// The H.265 splitter, which is NOT the H.264 one: the flag that starts a picture is +/// `first_slice_segment_in_pic_flag`, the first bit after the TWO-byte NAL header, and "slice" is +/// every NALU type below 32. Copied from the tested implementation in +/// `crates/pf-bitstream/src/h265.rs` (`fn split_into_aus`, test-private there) rather than +/// re-derived, because a splitter that disagrees with pf-bitstream's would make every AU index +/// in a capture point at a different picture. +fn split_into_aus_h265(stream: &[u8]) -> Vec<&[u8]> { + use cros_codecs::codec::h265::parser::Nalu; + + let mut aus = Vec::new(); + let mut cursor = Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + + while let Ok(nalu) = Nalu::next(&mut cursor) { + let header_start = cursor.position() as usize; + let start = header_start - nalu.offset; + let is_slice = (nalu.header.type_ as u32) < 32; + let first_slice_flag = + is_slice && stream.get(header_start + 2).is_some_and(|b| b & 0x80 != 0); + + if au_has_slice && (!is_slice || first_slice_flag) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus +} + +// --------------------------------------------------------------------------- +// This crate's side +// --------------------------------------------------------------------------- + +/// Everything one AU's `SubmitDecoderBuffers` call would carry, from this crate. +struct OurSubmission { + /// The picture-parameters buffer's bytes. + pic_params: Vec, + /// The quantization-matrix buffer's bytes, or `None` when the buffer is not submitted at + /// all — which for HEVC is the whole of review 13's defect. + qmatrix: Option>, + /// The descriptor set, in submission order. + descriptors: Vec, + /// The packer's slice records, for the internal-consistency checks. + records: Vec, + /// Bytes the packer wrote BEFORE the tail padding. + unpadded: u32, + /// `mb_width * mb_height` (H.264) or 0 (HEVC) — the value the descriptors must carry. + mb_count: u32, +} + +/// Plan and convert the whole vendored H.264 vector, one entry per AU. +/// +/// Every AU must plan and convert: this vector is pf-bitstream's clean golden, so a skipped AU +/// is a regression rather than a stream fact. Swallowing an error here — the shape the scaffold +/// this replaced had — is how a harness reports a clean bill of health while comparing nothing. +fn our_h264_submissions() -> Vec { + let mut planner = H264Planner::new(); + let mut slots: Option = None; + let mut mapping = vec![0u8; MAPPING_BYTES]; + let mut out = Vec::new(); + for (i, au) in split_into_aus(TEST_25FPS_H264).into_iter().enumerate() { + let plan: AuPlan = planner + .plan_au(au) + .unwrap_or_else(|e| panic!("AU {i} of the vendored H.264 vector must plan: {e}")); + let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames)); + if map.capacity() != plan.picture.max_dpb_frames + 1 { + *map = SlotMap::new(plan.picture.max_dpb_frames); + } + // `StatusReportFeedbackNumber` counts planned pictures from 1, which is exactly what + // libavcodec's `1 + report_id++` produces for a decoder that saw only this stream. + let dxva = pf_dxvadec::plan_to_dxva(&plan, map, out.len() as u32 + 1) + .unwrap_or_else(|e| panic!("AU {i} must convert: {e}")); + let packed = pf_dxvadec::pack(au, &dxva.slice_ranges, &mut mapping) + .unwrap_or_else(|e| panic!("AU {i} must pack: {e}")); + let unpadded = pf_dxvadec::packed_size(au, &dxva.slice_ranges).expect("packed size") as u32; + out.push(OurSubmission { + pic_params: pf_dxvadec::as_bytes(&dxva.pic_params).to_vec(), + qmatrix: Some(pf_dxvadec::as_bytes(&dxva.qmatrix).to_vec()), + descriptors: pf_dxvadec::descriptors_h264(&dxva, &packed), + records: packed.records, + unpadded, + mb_count: dxva.mb_count, + }); + } + assert_eq!(out.len(), VENDORED_AUS); + out +} + +/// Plan and convert the whole vendored HEVC vector, one entry per AU. Same no-skipping contract +/// as the H.264 side — `RaslSkipped` cannot arise on a vector that starts at an IDR. +fn our_hevc_submissions() -> Vec { + let mut planner = H265Planner::new(); + let mut slots: Option = None; + let mut mapping = vec![0u8; MAPPING_BYTES]; + let mut out = Vec::new(); + for (i, au) in split_into_aus_h265(TEST_25FPS_H265).into_iter().enumerate() { + let plan = planner + .plan_au(au) + .unwrap_or_else(|e| panic!("AU {i} of the vendored HEVC vector must plan: {e}")); + let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames)); + if map.capacity() != plan.picture.max_dpb_frames + 1 { + *map = SlotMap::new(plan.picture.max_dpb_frames); + } + let dxva = pf_dxvadec::plan_to_dxva_h265(&plan, map, out.len() as u32 + 1) + .unwrap_or_else(|e| panic!("AU {i} must convert: {e}")); + let packed = pf_dxvadec::pack(au, &dxva.slice_ranges, &mut mapping) + .unwrap_or_else(|e| panic!("AU {i} must pack: {e}")); + let unpadded = pf_dxvadec::packed_size(au, &dxva.slice_ranges).expect("packed size") as u32; + out.push(OurSubmission { + pic_params: pf_dxvadec::as_bytes(&dxva.pic_params).to_vec(), + qmatrix: dxva + .qmatrix + .as_ref() + .map(|qm| pf_dxvadec::as_bytes(qm).to_vec()), + descriptors: pf_dxvadec::descriptors_h265(&dxva, &packed), + records: packed.records, + unpadded, + mb_count: 0, + }); + } + assert_eq!(out.len(), VENDORED_AUS); + out +} + +// --------------------------------------------------------------------------- +// Offset → field name +// --------------------------------------------------------------------------- + +/// A field table for a hand-declared DXVA struct: `(name, offset)` per field, in declaration +/// order, built from the field IDENTIFIERS so a name and the offset it reports cannot drift +/// apart — the whole point of the table is to turn a differing byte into a field name, and a +/// table with a copy-pasted mismatch would name the wrong one. +macro_rules! field_table { + ($ty:ty, $($field:ident),+ $(,)?) => { + &[$((stringify!($field), offset_of!($ty, $field))),+] + }; +} + +/// Every field of `DXVA_PicParams_H264`. Lengths are DERIVED from the next field's offset rather +/// than written down: the struct has no interior padding (dxva.rs proves every offset at compile +/// time), so consecutive offsets tile it exactly — and a hand-typed length is one more thing that +/// can be wrong in a file whose whole job is to catch wrong numbers. +const H264_FIELDS: &[(&str, usize)] = field_table!( + PicParamsH264, + wFrameWidthInMbsMinus1, + wFrameHeightInMbsMinus1, + CurrPic, + num_ref_frames, + wBitFields, + bit_depth_luma_minus8, + bit_depth_chroma_minus8, + Reserved16Bits, + StatusReportFeedbackNumber, + RefFrameList, + CurrFieldOrderCnt, + FieldOrderCntList, + pic_init_qs_minus26, + chroma_qp_index_offset, + second_chroma_qp_index_offset, + ContinuationFlag, + pic_init_qp_minus26, + num_ref_idx_l0_active_minus1, + num_ref_idx_l1_active_minus1, + Reserved8BitsA, + FrameNumList, + UsedForReferenceFlags, + NonExistingFrameFlags, + frame_num, + log2_max_frame_num_minus4, + pic_order_cnt_type, + log2_max_pic_order_cnt_lsb_minus4, + delta_pic_order_always_zero_flag, + direct_8x8_inference_flag, + entropy_coding_mode_flag, + pic_order_present_flag, + num_slice_groups_minus1, + slice_group_map_type, + deblocking_filter_control_present_flag, + redundant_pic_cnt_present_flag, + Reserved8BitsB, + slice_group_change_rate_minus1, + SliceGroupMap, +); + +/// Every field of `DXVA_PicParams_HEVC`, same construction. +const HEVC_FIELDS: &[(&str, usize)] = field_table!( + PicParamsHevc, + PicWidthInMinCbsY, + PicHeightInMinCbsY, + wFormatAndSequenceInfoFlags, + CurrPic, + sps_max_dec_pic_buffering_minus1, + log2_min_luma_coding_block_size_minus3, + log2_diff_max_min_luma_coding_block_size, + log2_min_transform_block_size_minus2, + log2_diff_max_min_transform_block_size, + max_transform_hierarchy_depth_inter, + max_transform_hierarchy_depth_intra, + num_short_term_ref_pic_sets, + num_long_term_ref_pics_sps, + num_ref_idx_l0_default_active_minus1, + num_ref_idx_l1_default_active_minus1, + init_qp_minus26, + ucNumDeltaPocsOfRefRpsIdx, + wNumBitsForShortTermRPSInSlice, + ReservedBits2, + dwCodingParamToolFlags, + dwCodingSettingPicturePropertyFlags, + pps_cb_qp_offset, + pps_cr_qp_offset, + num_tile_columns_minus1, + num_tile_rows_minus1, + column_width_minus1, + row_height_minus1, + diff_cu_qp_delta_depth, + pps_beta_offset_div2, + pps_tc_offset_div2, + log2_parallel_merge_level_minus2, + CurrPicOrderCntVal, + RefPicList, + ReservedBits5, + PicOrderCntValList, + RefPicSetStCurrBefore, + RefPicSetStCurrAfter, + RefPicSetLtCurr, + ReservedBits6, + ReservedBits7, + StatusReportFeedbackNumber, +); + +/// `DXVA_Qmatrix_H264`'s two arrays. +const H264_QMATRIX_FIELDS: &[(&str, usize)] = + field_table!(QmatrixH264, bScalingLists4x4, bScalingLists8x8); + +/// The two short slice-control records, which are the structs the twelve-vs-ten defect was in. +const H264_SLICE_FIELDS: &[(&str, usize)] = field_table!( + SliceH264Short, + BSNALunitDataLocation, + SliceBytesInBuffer, + wBadSliceChopping, +); +const HEVC_SLICE_FIELDS: &[(&str, usize)] = field_table!( + SliceHevcShort, + BSNALunitDataLocation, + SliceBytesInBuffer, + wBadSliceChopping, +); + +/// `DXVA_Qmatrix_HEVC`'s six. +const HEVC_QMATRIX_FIELDS: &[(&str, usize)] = field_table!( + QmatrixHevc, + ucScalingLists0, + ucScalingLists1, + ucScalingLists2, + ucScalingLists3, + ucScalingListDCCoefSizeID2, + ucScalingListDCCoefSizeID3, +); + +/// Turn a field table into `(name, byte range)`, the last field running to `total`. +fn field_ranges( + fields: &[(&'static str, usize)], + total: usize, +) -> Vec<(&'static str, Range)> { + fields + .iter() + .enumerate() + .map(|(i, &(name, offset))| { + let end = fields.get(i + 1).map_or(total, |&(_, next)| next); + (name, offset..end) + }) + .collect() +} + +fn u16_at(bytes: &[u8], offset: usize) -> u16 { + u16::from_le_bytes([bytes[offset], bytes[offset + 1]]) +} + +fn u32_at(bytes: &[u8], offset: usize) -> u32 { + u32::from_le_bytes([ + bytes[offset], + bytes[offset + 1], + bytes[offset + 2], + bytes[offset + 3], + ]) +} + +fn i32_at(bytes: &[u8], offset: usize) -> i32 { + u32_at(bytes, offset) as i32 +} + +// --------------------------------------------------------------------------- +// Findings +// --------------------------------------------------------------------------- + +/// One kind of divergence, and how often it happened. +struct Finding { + count: usize, + first_au: usize, + detail: String, +} + +/// Divergences, grouped by the FIELD they belong to rather than by byte offset. A byte offset is +/// nearly useless on a hand-declared struct; the crate proves its offsets at compile time, so +/// this maps them back to names and reports those. +/// +/// Two channels, and the split is the whole reason this type exists rather than a `Vec`: +/// [`Findings::note`] records a FINDING (the run fails), [`Findings::document`] records a +/// divergence this program has already decided is not a defect. Documented ones are printed on +/// every run with their reason and their AU count — never dropped, because a divergence nobody +/// prints is a divergence nobody re-reads, and each of them is only allowed within a stated +/// allowance that the comparison itself enforces. +#[derive(Default)] +struct Findings { + by_field: BTreeMap, + documented: BTreeMap, +} + +impl Findings { + fn note(&mut self, field: impl Into, au: usize, detail: impl Into) { + let entry = self + .by_field + .entry(field.into()) + .or_insert_with(|| Finding { + count: 0, + first_au: au, + detail: detail.into(), + }); + entry.count += 1; + } + + /// A divergence the module docs list, with the reason it is not a defect. + fn document(&mut self, field: impl Into, au: usize, reason: impl Into) { + let entry = self + .documented + .entry(field.into()) + .or_insert_with(|| Finding { + count: 0, + first_au: au, + detail: reason.into(), + }); + entry.count += 1; + } + + fn is_empty(&self) -> bool { + self.by_field.is_empty() + } + + fn fields(&self) -> Vec<&str> { + self.by_field.keys().map(String::as_str).collect() + } + + fn documented_fields(&self) -> Vec<&str> { + self.documented.keys().map(String::as_str).collect() + } + + /// Print the verdict and fail if there is one. Never silently passes: a run that classified + /// nothing prints the AU count it did compare, so "no findings" cannot be confused with + /// "nothing was compared". + fn verdict(&self, what: &str, aus: usize) { + for (field, documented) in &self.documented { + println!( + "{what}: {field} diverges on {} of {aus} AUs (first at AU {}) — DOCUMENTED, not a \ + defect: {}", + documented.count, documented.first_au, documented.detail + ); + } + if self.is_empty() { + println!("{what}: {aus} AUs compared, no undocumented divergence"); + return; + } + println!( + "{what}: {aus} AUs compared, {} fields diverge:", + self.by_field.len() + ); + for (field, finding) in &self.by_field { + println!( + " {field}: {} AUs, first at AU {} — {}", + finding.count, finding.first_au, finding.detail + ); + } + panic!( + "{what}: {} fields diverge ({}) — read each against the module docs' list of \ + expected divergences before treating it as a defect", + self.by_field.len(), + self.fields().join(", ") + ); + } +} + +// --------------------------------------------------------------------------- +// Reference entries as pictures +// --------------------------------------------------------------------------- + +/// A picture's identity as the reference arrays express it, and the key the surface mapping is +/// tracked by: `(long-term, FrameNum or LongTermFrameIdx, TopFieldOrderCnt, +/// BottomFieldOrderCnt)`. HEVC leaves the second and fourth members at 0 — it identifies a +/// reference by POC alone. +/// +/// It is DXVA's own key, deliberately, so both sides express it the same way. The one consequence +/// worth naming: a picture that is re-marked long-term changes key (`FrameNum` becomes +/// `LongTermFrameIdx`), so the surface mapping loses the link to its earlier self rather than +/// reporting a change — a missed check, never a false finding. Both sides re-key identically, so +/// the SET comparison is unaffected. +type PictureKey = (bool, u16, i32, i32); + +/// A reference entry with its surface index REMOVED: the identity DXVA resolves a reference by, +/// plus the per-entry flag bits that belong to it. Comparing the array as a multiset of these is +/// what makes the two sides' different orders irrelevant while keeping every fact — the flag +/// bits travel with their entry, which is "re-indexed to the compared order" in practice. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] +struct RefEntry { + long_term: bool, + /// `FrameNumList[i]` (H.264): `frame_num`, or `LongTermFrameIdx` for a long-term entry. + /// Always 0 for HEVC, which has no such array. + frame_num_or_lt_idx: u16, + /// `FieldOrderCntList[i]` (H.264) or `PicOrderCntValList[i]` (HEVC, in `top`). + top: i32, + bottom: i32, + used_top: bool, + used_bottom: bool, + non_existing: bool, +} + +impl RefEntry { + /// The identity half alone — what follows a picture across AUs, and therefore what the + /// surface mapping is keyed by. + fn key(self) -> PictureKey { + ( + self.long_term, + self.frame_num_or_lt_idx, + self.top, + self.bottom, + ) + } +} + +/// FFmpeg's H.264 POC base, and the proof that it is a CONSTANT. +/// +/// libavcodec's H.264 decoder seeds `prev_poc_msb = 1 << 16` at every IDR, so every +/// `TopFieldOrderCnt`/`BottomFieldOrderCnt` it hands DXVA is the specification's plus 65536 — +/// measured, on the RTX 4090 capture: AUs 0..7 carry 65536, 65540, 65538, 65544, … where 8.2.1 +/// (and this crate) derive 0, 4, 2, 8, …. The PROGRESSION is identical; only the base differs. +/// +/// This crate keeps the spec's values, deliberately: the offset is an artefact of another +/// decoder's POC bookkeeping, not a DXVA requirement, and importing it would mean writing a magic +/// 65536 into a derivation that pf-bitstream shares with the Vulkan rung. It is harmless on the +/// wire because every use a driver makes of these fields is a DIFFERENCE — temporal direct +/// scaling, implicit weighted prediction, co-located picture selection — and the offset is uniform +/// across `CurrPic` and every `RefFrameList` entry of a stream, so it cancels. (References are +/// matched by `FrameNumList`, not by POC.) That it is uniform is exactly what this type checks. +/// +/// So POCs are compared RELATIVE: the offset is derived from the first AU and then required to +/// hold for every POC of every later AU. A wrong POC on either side changes the offset and is +/// reported. Only 0 and 65536 are accepted as the base itself — any other constant is a finding, +/// because then it is not the quirk documented here. +#[derive(Default)] +struct PocBase { + offset: Option, +} + +impl PocBase { + /// The offset in force, or 0 before the first AU has established one. + fn offset(&self) -> i64 { + self.offset.unwrap_or(0) + } + + /// Check one POC pair, establishing the base on the first call. + fn check(&mut self, au: usize, field: &str, ours: i32, theirs: i32, findings: &mut Findings) { + let delta = i64::from(theirs) - i64::from(ours); + match self.offset { + None => { + if delta != 0 && delta != 65536 { + findings.note( + format!("{field}[POC base]"), + au, + format!( + "libav's first POC is ours {ours} + {delta}, which is neither 0 nor \ + FFmpeg's documented 65536 `prev_poc_msb` seed — an unexplained POC \ + base is a finding, not a quirk to absorb" + ), + ); + } + if delta == 65536 { + findings.document( + "FieldOrderCnt[POC base]", + au, + "libavcodec seeds `prev_poc_msb = 1 << 16` at every IDR, so its POCs are \ + the specification's plus 65536; this crate keeps 8.2.1's values and the \ + harness compares POCs RELATIVE to that constant, which it requires to \ + hold on every AU", + ); + } + self.offset = Some(delta); + } + Some(offset) if delta != offset => findings.note( + format!("{field}[POC]"), + au, + format!( + "ours {ours}, libav {theirs}: a difference of {delta} where every earlier POC \ + of this stream differed by {offset} — the POC base is not constant, so this \ + is a real POC divergence rather than libav's base offset" + ), + ), + Some(_) => {} + } + } +} + +/// Subtract libav's POC base from a decoded reference array, so the set comparison compares +/// pictures rather than POC bases (see [`PocBase`]). +/// +/// `bottom_too` is true for H.264, whose entries carry a real `FieldOrderCntList[i][2]` PAIR, and +/// false for HEVC, whose `bottom` member is a placeholder this harness leaves at 0 on both sides — +/// shifting it would invent a difference rather than absorb one. +fn shift_poc(entries: &mut [(u8, RefEntry)], offset: i64, bottom_too: bool) { + for (_, entry) in entries.iter_mut() { + entry.top = (i64::from(entry.top) - offset) as i32; + if bottom_too { + entry.bottom = (i64::from(entry.bottom) - offset) as i32; + } + } +} + +/// A per-field allowance: `Some(reason)` when THIS difference in THIS field is a divergence the +/// module docs have already settled, rather than a finding. +/// +/// Deliberately per-DIFFERENCE and not per-field: a field with an allowance still reports anything +/// outside it. An allowlist keyed by field name alone would be the "vacuous green" this program +/// has been bitten by before — it would hide the next real difference in the same word. +type Allowance = fn(&str, &[u8], &[u8]) -> Option<&'static str>; + +/// H.264 has none: every difference in a scalar field is a finding. +fn no_allowance(_: &str, _: &[u8], _: &[u8]) -> Option<&'static str> { + None +} + +/// HEVC's one documented scalar divergence: bit 10 of `dwCodingSettingPicturePropertyFlags`, +/// `loop_filter_across_tiles_enabled_flag`, which this crate sets and libavcodec does not. +/// +/// 7.4.3.3.1 infers the flag to be 1 when the PPS does not code it, and the PPS only codes it +/// under `tiles_enabled_flag` — so with tiles disabled, 1 is what the specification says and what +/// the vendored parser reports. libavcodec's capture carries 0 on all 250 AUs of the vendored +/// vector. Neither can change a decoded picture: with `tiles_enabled_flag` clear there are no tile +/// boundaries for a loop filter to cross, which is why this is documented rather than fixed — +/// matching libavcodec here would mean overriding a spec inference on the strength of one +/// measurement of another decoder's parser default, and that default is not readable from this +/// worktree. +/// +/// The allowance is tight: ONLY bit 10, only ours-set-theirs-clear, and only while both sides +/// agree tiles are disabled. Any other difference in the same word — including bit 10 with tiles +/// ENABLED, where the flag stops being inert — is a finding. +fn hevc_allowance(field: &str, ours: &[u8], theirs: &[u8]) -> Option<&'static str> { + /// `tiles_enabled_flag`. + const TILES: u32 = 1 << 7; + /// `loop_filter_across_tiles_enabled_flag`. + const ACROSS_TILES: u32 = 1 << 10; + + if field != "dwCodingSettingPicturePropertyFlags" { + return None; + } + let (Ok(ours), Ok(theirs)) = ( + <[u8; 4]>::try_from(ours).map(u32::from_le_bytes), + <[u8; 4]>::try_from(theirs).map(u32::from_le_bytes), + ) else { + return None; + }; + let only_bit_10 = ours ^ theirs == ACROSS_TILES; + let ours_sets_it = ours & ACROSS_TILES != 0; + let tiles_off = (ours | theirs) & TILES == 0; + (only_bit_10 && ours_sets_it && tiles_off).then_some( + "loop_filter_across_tiles_enabled_flag (bit 10): 7.4.3.3.1 infers 1 when the PPS codes no \ + tiles and the vendored parser reports that; libavcodec emits 0. Inert either way — with \ + tiles_enabled_flag clear there is no tile boundary for a loop filter to cross", + ) +} + +/// One side's H.264 reference array, decoded: the in-use entries with their surface indices. +fn h264_ref_entries(pp: &[u8]) -> Vec<(u8, RefEntry)> { + let list = offset_of!(PicParamsH264, RefFrameList); + let poc = offset_of!(PicParamsH264, FieldOrderCntList); + let nums = offset_of!(PicParamsH264, FrameNumList); + let used = u32_at(pp, offset_of!(PicParamsH264, UsedForReferenceFlags)); + let missing = u16_at(pp, offset_of!(PicParamsH264, NonExistingFrameFlags)); + (0..16) + .filter(|i| pp[list + i] != UNUSED_ENTRY) + .map(|i| { + ( + pp[list + i] & 0x7F, + RefEntry { + long_term: pp[list + i] & 0x80 != 0, + frame_num_or_lt_idx: u16_at(pp, nums + 2 * i), + top: i32_at(pp, poc + 8 * i), + bottom: i32_at(pp, poc + 8 * i + 4), + used_top: used >> (2 * i) & 1 != 0, + used_bottom: used >> (2 * i + 1) & 1 != 0, + non_existing: missing >> i & 1 != 0, + }, + ) + }) + .collect() +} + +/// One side's HEVC reference array, decoded. HEVC's array carries no `FrameNum` and no use +/// flags — residency IS the statement — so those members stay at their neutral values. +fn hevc_ref_entries(pp: &[u8]) -> Vec<(u8, RefEntry)> { + let list = offset_of!(PicParamsHevc, RefPicList); + let poc = offset_of!(PicParamsHevc, PicOrderCntValList); + (0..15) + .filter(|i| pp[list + i] != UNUSED_ENTRY) + .map(|i| { + ( + pp[list + i] & 0x7F, + RefEntry { + long_term: pp[list + i] & 0x80 != 0, + frame_num_or_lt_idx: 0, + top: i32_at(pp, poc + 4 * i), + bottom: 0, + used_top: true, + used_bottom: true, + non_existing: false, + }, + ) + }) + .collect() +} + +/// The surface mapping between the two sides, tracked per PICTURE. +/// +/// A global index-to-index bijection over a whole stream is the wrong model: both sides reuse a +/// surface once its picture leaves the DPB, and they need not reuse it at the same moment. What +/// must hold is that while a picture is live, its two surface numbers keep agreeing — so the +/// mapping is keyed by the picture and dropped when a side reassigns the index. +#[derive(Default)] +struct SurfaceMapping { + live: BTreeMap, +} + +impl SurfaceMapping { + /// Record one AU's pairs, reporting a mapping that changed under a live picture and two + /// pictures collapsing onto one surface. + fn observe( + &mut self, + au: usize, + field: &str, + pairs: &[(PictureKey, (u8, u8))], + findings: &mut Findings, + ) { + let mut ours_seen: BTreeMap = BTreeMap::new(); + let mut theirs_seen: BTreeMap = BTreeMap::new(); + for &(key, (ours, theirs)) in pairs { + if let Some(&(known_ours, known_theirs)) = self.live.get(&key) { + if (known_ours, known_theirs) != (ours, theirs) { + findings.note( + format!("{field}[surface mapping]"), + au, + format!( + "picture {key:?} was surface {known_ours} (ours) = {known_theirs} \ + (libav) and is now {ours} = {theirs}: the mapping is not a \ + bijection over this picture's lifetime" + ), + ); + } + } + if let Some(other) = ours_seen.insert(ours, key) { + if other != key { + findings.note( + format!("{field}[surface aliasing]"), + au, + format!("our surface {ours} carries both {other:?} and {key:?}"), + ); + } + } + if let Some(other) = theirs_seen.insert(theirs, key) { + if other != key { + findings.note( + format!("{field}[surface aliasing]"), + au, + format!("libav's surface {theirs} carries both {other:?} and {key:?}"), + ); + } + } + self.live.insert(key, (ours, theirs)); + } + // A surface either side has just reassigned no longer says anything about the picture + // that used to hold it, so the stale entries go — that is the difference between + // "the mapping broke" and "the pool moved on". + self.live.retain(|key, &mut (ours, theirs)| { + let ours_now = ours_seen.get(&ours); + let theirs_now = theirs_seen.get(&theirs); + ours_now.is_none_or(|k| k == key) && theirs_now.is_none_or(|k| k == key) + }); + } +} + +// --------------------------------------------------------------------------- +// The capture +// --------------------------------------------------------------------------- + +/// One captured buffer descriptor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct CapturedDescriptor { + buffer_type: u32, + data_size: u32, + num_mbs_in_buffer: u32, + data_offset: u32, +} + +/// A parsed capture, for ONE codec. +#[derive(Default)] +struct Capture { + pic_params: BTreeMap>, + /// `Some(bytes)` for a submitted matrix, `None` for the explicit `absent` spelling. An AU + /// missing from this map was never reported either way, which is itself a finding. + qmatrix: BTreeMap>>, + descriptors: BTreeMap>, + config_bitstream_raw: BTreeMap, + /// Lines that carry one of this harness's prefixes and could not be read. Never dropped + /// silently: a capture whose format drifted must fail loudly, not compare less. + unreadable: Vec, +} + +/// `hex` → bytes, or `None` when it is not an even-length hex string. +fn from_hex(hex: &str) -> Option> { + if hex.len() % 2 != 0 || hex.is_empty() { + return None; + } + (0..hex.len() / 2) + .map(|i| u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).ok()) + .collect() +} + +fn to_hex(bytes: &[u8]) -> String { + let mut out = String::with_capacity(2 * bytes.len()); + for b in bytes { + let _ = write!(out, "{b:02x}"); + } + out +} + +/// Parse the lines of one codec out of a capture. `codec` is the token FFmpeg's +/// `avcodec_get_name` produces: `h264` or `hevc`. +fn parse_capture(text: &str, codec: &str) -> Capture { + /// The markers, with their trailing space so a bare word cannot match one. + const MARKERS: [&str; 4] = ["PFPP ", "PFQM ", "PFBD ", "PFCFG "]; + + let mut out = Capture::default(); + for raw in text.lines() { + // The marker is found ANYWHERE in the line, not required at its start: FFmpeg's logger + // prefixes a message logged against a codec context with `[h264 @ 0x…] `, and a capture + // made that way must still be readable (the recipe asks for `av_log(NULL, …)` so it is + // not, but a capture is expensive and this costs nothing). + let Some(start) = MARKERS.iter().filter_map(|m| raw.find(m)).min() else { + continue; + }; + let line = raw[start..].trim(); + let Some((prefix, rest)) = line.split_once(' ') else { + continue; + }; + if !matches!(prefix, "PFPP" | "PFQM" | "PFBD" | "PFCFG") { + continue; + } + let fields: Vec<&str> = rest.split_whitespace().collect(); + // Every line is ` …`, so anything shorter is malformed. + let (Some(line_codec), Some(au)) = (fields.first(), fields.get(1)) else { + out.unreadable.push(line.to_string()); + continue; + }; + if *line_codec != codec { + continue; + } + let Ok(au) = au.parse::() else { + out.unreadable.push(line.to_string()); + continue; + }; + let ok = match (prefix, &fields[2..]) { + ("PFPP", [hex]) => match from_hex(hex) { + Some(bytes) => out.pic_params.insert(au, bytes).is_none(), + None => false, + }, + ("PFQM", ["absent"]) => out.qmatrix.insert(au, None).is_none(), + ("PFQM", [hex]) => match from_hex(hex) { + Some(bytes) => out.qmatrix.insert(au, Some(bytes)).is_none(), + None => false, + }, + ("PFBD", [kind, size, mbs, offset]) => { + match ( + kind.parse::(), + size.parse::(), + mbs.parse::(), + offset.parse::(), + ) { + (Ok(buffer_type), Ok(data_size), Ok(num_mbs_in_buffer), Ok(data_offset)) => { + out.descriptors + .entry(au) + .or_default() + .push(CapturedDescriptor { + buffer_type, + data_size, + num_mbs_in_buffer, + data_offset, + }); + true + } + _ => false, + } + } + ("PFCFG", [raw]) => match raw.parse::() { + Ok(raw) => { + out.config_bitstream_raw.insert(au, raw); + true + } + Err(_) => false, + }, + _ => false, + }; + if !ok { + out.unreadable.push(line.to_string()); + } + } + out +} + +/// Read the capture named by `var`, or `None` when it is unset. +fn capture_from_env(var: &str, codec: &str) -> Option { + let path = std::env::var(var).ok()?; + let text = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("{var}={path} could not be read: {e}")); + Some(parse_capture(&text, codec)) +} + +/// The checks every comparison needs before it compares anything: the capture is readable, it +/// covers the same AUs, and it was not made against a workaround path. +fn preflight(capture: &Capture, ours: usize, codec: &str, reserved16: Option) { + assert!( + capture.unreadable.is_empty(), + "the capture holds {} unreadable line(s) — the first is:\n {}\nthe recipe in this \ + file's module docs is the format", + capture.unreadable.len(), + capture.unreadable[0] + ); + assert!( + !capture.pic_params.is_empty(), + "the capture holds no `PFPP {codec}` lines: either the patch did not apply or the \ + hwaccel never engaged (a software fallback logs nothing)" + ); + assert_eq!( + capture.pic_params.len(), + ours, + "the capture covers {} AUs and this crate plans {ours} — the two sides must decode the \ + same elementary stream, split the same way", + capture.pic_params.len() + ); + let expected: BTreeSet = (0..ours).collect(); + let seen: BTreeSet = capture.pic_params.keys().copied().collect(); + assert_eq!( + seen, expected, + "the capture's AU indices are not 0..{ours}; pairing by index would compare different \ + pictures" + ); + if let Some(offset) = reserved16 { + let zeroed = capture + .pic_params + .values() + .filter(|pp| pp.len() > offset + 1 && u16_at(pp, offset) == 0) + .count(); + assert_eq!( + zeroed, 0, + "{zeroed} of {ours} captured pictures carry Reserved16Bits = 0, which libavcodec \ + writes only under FF_DXVA2_WORKAROUND_INTEL_CLEARVIDEO or \ + FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG — this capture is against a workaround \ + path and is VOID for comparison (module docs)" + ); + } + // A `PFCFG` line is optional, but a wrong one voids the slice-control comparison: it means + // the driver read the other slice-control struct entirely. + let want = pf_dxvadec::short_slice_config(match codec { + "h264" => Codec::H264, + _ => Codec::H265, + }); + for (au, &raw) in &capture.config_bitstream_raw { + assert_eq!( + raw, want, + "AU {au}: the capture's ConfigBitstreamRaw is {raw}, and short format for {codec} \ + is {want} — the capture used the other slice-control format, so its slice-control \ + sizes describe a different struct" + ); + } +} + +// --------------------------------------------------------------------------- +// The comparisons +// --------------------------------------------------------------------------- + +/// How two versions of one field differ, in a form a reader can act on: the whole value in hex +/// when it is small enough to read, and a count plus the first differing byte when it is an array +/// (a `SliceGroupMap` printed in full is 1620 characters of nothing). +fn byte_diff_detail(ours: &[u8], theirs: &[u8]) -> String { + if ours.len() <= 8 { + return format!("ours {}, libav {}", to_hex(ours), to_hex(theirs)); + } + let first = ours + .iter() + .zip(theirs) + .position(|(a, b)| a != b) + .unwrap_or(0); + let differing = ours.iter().zip(theirs).filter(|(a, b)| a != b).count(); + format!( + "{differing} of {} bytes differ, first at byte {first} of the field (ours {:#04x}, libav \ + {:#04x})", + ours.len(), + ours[first], + theirs[first] + ) +} + +/// Compare the scalar fields — everything that is neither a surface index nor part of a +/// reference array — byte for byte, reporting by field name. +fn compare_scalars( + au: usize, + ours: &[u8], + theirs: &[u8], + ranges: &[(&'static str, Range)], + structural: &[&str], + allowance: Allowance, + findings: &mut Findings, +) { + let mut classified = vec![false; ours.len()]; + for (name, range) in ranges { + for byte in range.clone() { + classified[byte] = true; + } + if structural.contains(name) { + continue; + } + if ours[range.clone()] != theirs[range.clone()] { + match allowance(name, &ours[range.clone()], &theirs[range.clone()]) { + Some(reason) => findings.document(*name, au, reason), + None => findings.note( + *name, + au, + byte_diff_detail(&ours[range.clone()], &theirs[range.clone()]), + ), + } + } + } + // The field table must tile the struct; if a byte ever falls outside it, report it as a raw + // offset rather than pass over it. That is the fallback the module docs promise, and the + // reason this harness can never silently ignore a difference it cannot name. + for (offset, covered) in classified.iter().enumerate() { + if !covered && ours[offset] != theirs[offset] { + findings.note( + format!(""), + au, + format!("ours {:#04x}, libav {:#04x}", ours[offset], theirs[offset]), + ); + } + } +} + +/// Compare one AU's reference array as a SET, and feed the surface mapping. +fn compare_ref_array( + au: usize, + field: &str, + ours: &[(u8, RefEntry)], + theirs: &[(u8, RefEntry)], + mapping: &mut SurfaceMapping, + findings: &mut Findings, +) { + let mut ours_sorted: Vec = ours.iter().map(|&(_, e)| e).collect(); + let mut theirs_sorted: Vec = theirs.iter().map(|&(_, e)| e).collect(); + ours_sorted.sort_unstable(); + theirs_sorted.sort_unstable(); + if ours_sorted != theirs_sorted { + let only_ours: Vec<&RefEntry> = ours_sorted + .iter() + .filter(|e| !theirs_sorted.contains(e)) + .collect(); + let only_theirs: Vec<&RefEntry> = theirs_sorted + .iter() + .filter(|e| !ours_sorted.contains(e)) + .collect(); + findings.note( + format!("{field}[set]"), + au, + format!( + "{} entries ours vs {} libav; only ours: {only_ours:?}; only libav: {only_theirs:?}", + ours.len(), + theirs.len() + ), + ); + } + + // The pairs the mapping is built from: a picture present on both sides, identified by its + // key. An ambiguous key (two entries claiming the same identity) is skipped and reported — + // it cannot happen off a conformant stream, and guessing which is which would invent a + // mapping. + let mut pairs = Vec::new(); + for &(our_slot, entry) in ours { + let key = entry.key(); + let ours_same = ours.iter().filter(|(_, e)| e.key() == key).count(); + let matches: Vec = theirs + .iter() + .filter(|(_, e)| e.key() == key) + .map(|&(slot, _)| slot) + .collect(); + if ours_same > 1 || matches.len() > 1 { + findings.note( + format!("{field}[ambiguous key]"), + au, + format!( + "{key:?} appears {ours_same} times ours and {} libav", + matches.len() + ), + ); + continue; + } + if let Some(&their_slot) = matches.first() { + pairs.push((key, (our_slot, their_slot))); + } + } + mapping.observe(au, field, &pairs, findings); +} + +/// The whole H.264 picture-parameter comparison. +fn compare_h264_picparams(ours: &[OurSubmission], capture: &Capture) -> Findings { + let ranges = field_ranges(H264_FIELDS, size_of::()); + let structural = [ + "CurrPic", + // The POC fields are compared RELATIVE to libavcodec's base offset rather than byte for + // byte — see `PocBase` for the measurement and for why this crate keeps 8.2.1's values. + "CurrFieldOrderCnt", + "RefFrameList", + "FieldOrderCntList", + "FrameNumList", + "UsedForReferenceFlags", + "NonExistingFrameFlags", + ]; + let mut findings = Findings::default(); + let mut mapping = SurfaceMapping::default(); + let mut poc = PocBase::default(); + for (au, sub) in ours.iter().enumerate() { + let Some(theirs) = capture.pic_params.get(&au) else { + findings.note( + "", + au, + "the capture holds no PFPP line for this AU", + ); + continue; + }; + if theirs.len() != sub.pic_params.len() { + findings.note( + "", + au, + format!( + "the capture's picture parameters are {} bytes and ours are {} — the \ + hand-declared layout and the header disagree, which is a finding on its own", + theirs.len(), + sub.pic_params.len() + ), + ); + continue; + } + compare_scalars( + au, + &sub.pic_params, + theirs, + &ranges, + &structural, + no_allowance, + &mut findings, + ); + // The POC fields, compared RELATIVE to libavcodec's base offset (`PocBase`). The base is + // established from the CURRENT picture's own count, which is the one POC both sides + // certainly report for the same picture, and then required of every POC after it. + let poc_at = offset_of!(PicParamsH264, CurrFieldOrderCnt); + poc.check( + au, + "CurrFieldOrderCnt[0]", + i32_at(&sub.pic_params, poc_at), + i32_at(theirs, poc_at), + &mut findings, + ); + poc.check( + au, + "CurrFieldOrderCnt[1]", + i32_at(&sub.pic_params, poc_at + 4), + i32_at(theirs, poc_at + 4), + &mut findings, + ); + let mut their_entries = h264_ref_entries(theirs); + shift_poc(&mut their_entries, poc.offset(), true); + compare_ref_array( + au, + "RefFrameList", + &h264_ref_entries(&sub.pic_params), + &their_entries, + &mut mapping, + &mut findings, + ); + // `CurrPic` needs no matching: the same AU is the same picture on both sides. It is fed + // through the mapping under the current picture's own key, so that when this picture + // shows up as a REFERENCE later, the two surfaces are checked against this pairing. + let curr = offset_of!(PicParamsH264, CurrPic); + let frame_num = offset_of!(PicParamsH264, frame_num); + if sub.pic_params[curr] & 0x80 != theirs[curr] & 0x80 { + findings.note( + "CurrPic[AssociatedFlag]", + au, + format!( + "ours {:#04x}, libav {:#04x} — the bottom-field flag, which is 0 for every \ + picture inside this backend's progressive envelope", + sub.pic_params[curr], theirs[curr] + ), + ); + } + let key = ( + false, + u16_at(&sub.pic_params, frame_num), + i32_at(&sub.pic_params, poc_at), + i32_at(&sub.pic_params, poc_at + 4), + ); + mapping.observe( + au, + "CurrPic", + &[(key, (sub.pic_params[curr] & 0x7F, theirs[curr] & 0x7F))], + &mut findings, + ); + } + findings +} + +/// One HEVC RPS index array, resolved through its own side's `RefPicList` into the pictures it +/// names — which is the only form in which the two sides' arrays are comparable. +fn hevc_rps_pictures(pp: &[u8], array: usize, entries: &[(u8, RefEntry)]) -> Vec> { + let list = offset_of!(PicParamsHevc, RefPicList); + (0..8) + .map(|i| { + let index = pp[array + i]; + // The array holds an index INTO `RefPicList` (15 entries), so anything outside that + // — the `0xFF` sentinel included — names nothing, and resolving it is how a stale or + // out-of-range index becomes a reported difference rather than a garbage read. + if usize::from(index) >= 15 { + return None; + } + let slot = pp[list + usize::from(index)]; + if slot == UNUSED_ENTRY { + return None; + } + entries + .iter() + .find(|(s, _)| *s == slot & 0x7F) + .map(|&(_, entry)| entry) + }) + .collect() +} + +/// The whole HEVC picture-parameter comparison. +fn compare_hevc_picparams(ours: &[OurSubmission], capture: &Capture) -> Findings { + let ranges = field_ranges(HEVC_FIELDS, size_of::()); + let structural = [ + "CurrPic", + // Relative, like H.264's — though libavcodec's HEVC POCs carry no base offset (measured: + // 0 on all 250 AUs of the vendored vector). `PocBase` derives whatever offset exists + // rather than assuming this one, so a future FFmpeg that grows one produces a documented + // line instead of 250 findings. + "CurrPicOrderCntVal", + "RefPicList", + "PicOrderCntValList", + "RefPicSetStCurrBefore", + "RefPicSetStCurrAfter", + "RefPicSetLtCurr", + ]; + let mut findings = Findings::default(); + let mut mapping = SurfaceMapping::default(); + let mut poc = PocBase::default(); + for (au, sub) in ours.iter().enumerate() { + let Some(theirs) = capture.pic_params.get(&au) else { + findings.note( + "", + au, + "the capture holds no PFPP line for this AU", + ); + continue; + }; + if theirs.len() != sub.pic_params.len() { + findings.note( + "", + au, + format!( + "the capture's picture parameters are {} bytes and ours are {}", + theirs.len(), + sub.pic_params.len() + ), + ); + continue; + } + compare_scalars( + au, + &sub.pic_params, + theirs, + &ranges, + &structural, + hevc_allowance, + &mut findings, + ); + let poc_at = offset_of!(PicParamsHevc, CurrPicOrderCntVal); + poc.check( + au, + "CurrPicOrderCntVal", + i32_at(&sub.pic_params, poc_at), + i32_at(theirs, poc_at), + &mut findings, + ); + let our_entries = hevc_ref_entries(&sub.pic_params); + let mut their_entries = hevc_ref_entries(theirs); + // HEVC's entries carry one POC each, in `top`; `bottom` is a placeholder. + shift_poc(&mut their_entries, poc.offset(), false); + compare_ref_array( + au, + "RefPicList", + &our_entries, + &their_entries, + &mut mapping, + &mut findings, + ); + for (name, offset) in [ + ( + "RefPicSetStCurrBefore", + offset_of!(PicParamsHevc, RefPicSetStCurrBefore), + ), + ( + "RefPicSetStCurrAfter", + offset_of!(PicParamsHevc, RefPicSetStCurrAfter), + ), + ( + "RefPicSetLtCurr", + offset_of!(PicParamsHevc, RefPicSetLtCurr), + ), + ] { + let ours_named = hevc_rps_pictures(&sub.pic_params, offset, &our_entries); + let theirs_named = hevc_rps_pictures(theirs, offset, &their_entries); + for (position, (a, b)) in ours_named.iter().zip(&theirs_named).enumerate() { + if a != b { + findings.note( + format!("{name}[{position}]"), + au, + format!("ours names {a:?}, libav names {b:?}"), + ); + } + } + } + let curr = offset_of!(PicParamsHevc, CurrPic); + let key = (false, 0u16, i32_at(&sub.pic_params, poc_at), 0); + mapping.observe( + au, + "CurrPic", + &[(key, (sub.pic_params[curr] & 0x7F, theirs[curr] & 0x7F))], + &mut findings, + ); + } + findings +} + +/// The quantization-matrix comparison: presence FIRST, then contents by field. +fn compare_qmatrix( + ours: &[OurSubmission], + capture: &Capture, + fields: &[(&'static str, usize)], + total: usize, +) -> Findings { + let ranges = field_ranges(fields, total); + let mut findings = Findings::default(); + for (au, sub) in ours.iter().enumerate() { + let Some(theirs) = capture.qmatrix.get(&au) else { + findings.note( + "", + au, + "the capture reports the matrix neither present nor `absent` for this AU — the \ + PFQM patch (recipe step 4) is missing", + ); + continue; + }; + match (&sub.qmatrix, theirs) { + (None, None) => {} + (Some(_), None) => findings.note( + "", + au, + "we submit an inverse-quantization-matrix buffer where libavcodec submits NONE — \ + for HEVC this is review 13's defect: the picture parameters have told the \ + driver to ignore the matrix, and a driver that honours it anyway dequantizes \ + every residual against it", + ), + (None, Some(_)) => findings.note( + "", + au, + "libavcodec submits an inverse-quantization-matrix buffer and we submit none — \ + the hardware is left to dequantize against whatever it last held", + ), + (Some(mine), Some(theirs)) => { + if mine.len() != theirs.len() { + findings.note( + "", + au, + format!("ours {} bytes, libav {}", mine.len(), theirs.len()), + ); + continue; + } + for (name, range) in &ranges { + if mine[range.clone()] != theirs[range.clone()] { + findings.note( + *name, + au, + byte_diff_detail(&mine[range.clone()], &theirs[range.clone()]), + ); + } + } + } + } + } + findings +} + +/// A descriptor buffer type as a name, for reports. +fn buffer_name(buffer_type: u32) -> &'static str { + match buffer_type { + BUFFER_PICTURE_PARAMETERS => "PICTURE_PARAMETERS", + BUFFER_INVERSE_QUANTIZATION_MATRIX => "INVERSE_QUANTIZATION_MATRIX", + BUFFER_SLICE_CONTROL => "SLICE_CONTROL", + BUFFER_BITSTREAM => "BITSTREAM", + _ => "", + } +} + +/// The descriptor comparison. Everything but the bitstream buffer's `DataSize` must match +/// exactly; that one field has a legitimate divergence class, which is classified apart rather +/// than reported as one undifferentiated difference (module docs). +fn compare_descriptors(ours: &[OurSubmission], capture: &Capture) -> Findings { + let mut findings = Findings::default(); + for (au, sub) in ours.iter().enumerate() { + let Some(theirs) = capture.descriptors.get(&au) else { + findings.note( + "", + au, + "the capture holds no PFBD lines for this AU", + ); + continue; + }; + let our_types: Vec = sub.descriptors.iter().map(|d| d.buffer_type).collect(); + let their_types: Vec = theirs.iter().map(|d| d.buffer_type).collect(); + if our_types != their_types { + // A missing BITSTREAM on their side is the one shape that is a MISSED PATCH rather + // than a divergence — the bitstream descriptor is the one libavcodec fills outside + // the choke point (recipe step 2) — so it is named as such. + let detail = if !their_types.contains(&BUFFER_BITSTREAM) { + "the capture carries no BITSTREAM descriptor at all: recipe step 2's SECOND \ + patch site (the inline fill in commit_bitstream_and_slice_buffer) was missed" + .to_string() + } else { + format!( + "ours {:?}, libav {:?}", + our_types + .iter() + .map(|&t| buffer_name(t)) + .collect::>(), + their_types + .iter() + .map(|&t| buffer_name(t)) + .collect::>() + ) + }; + findings.note("", au, detail); + } + for our_desc in &sub.descriptors { + let name = buffer_name(our_desc.buffer_type); + let Some(their_desc) = theirs + .iter() + .find(|d| d.buffer_type == our_desc.buffer_type) + else { + continue; // already reported by the set comparison + }; + if our_desc.data_offset != their_desc.data_offset { + findings.note( + format!("{name}.DataOffset"), + au, + format!( + "ours {}, libav {}", + our_desc.data_offset, their_desc.data_offset + ), + ); + } + if our_desc.num_mbs_in_buffer != their_desc.num_mbs_in_buffer { + findings.note( + format!("{name}.NumMBsInBuffer"), + au, + format!( + "ours {}, libav {} — this field cannot legitimately differ: \ + mb_width*mb_height on H.264's bitstream and slice-control buffers, 0 \ + everywhere else", + our_desc.num_mbs_in_buffer, their_desc.num_mbs_in_buffer + ), + ); + } + if our_desc.data_size == their_desc.data_size { + continue; + } + if our_desc.buffer_type != BUFFER_BITSTREAM { + findings.note( + format!("{name}.DataSize"), + au, + format!( + "ours {}, libav {} — a fixed-size buffer ({} is `sizeof` a structure or \ + slices * 10), so this cannot legitimately differ", + our_desc.data_size, their_desc.data_size, name + ), + ); + continue; + } + // The bitstream buffer. Their slice COUNT is readable from their slice-control + // size, which is what separates "the two sides split the AU differently" from "the + // two sides delimit each slice a couple of bytes apart". (Both codecs' short + // records are TEN bytes — asserted in + // `the_slice_control_descriptor_is_one_ten_byte_short_format_record_per_slice_for_both_codecs` + // — so one divisor serves both; a slice-control buffer that is NOT a multiple of it + // has already been reported as a `SLICE_CONTROL.DataSize` difference above.) + let their_slices = theirs + .iter() + .find(|d| d.buffer_type == BUFFER_SLICE_CONTROL) + .map(|d| d.data_size as usize / size_of::()); + let our_slices = sub.records.len(); + match their_slices { + Some(count) if count != our_slices => findings.note( + "BITSTREAM.DataSize[slice count]", + au, + format!( + "ours {} bytes over {our_slices} slices, libav {} over {count} — the two \ + sides disagree about how many slices this AU has, which voids the size \ + comparison and is the finding itself", + our_desc.data_size, their_desc.data_size + ), + ), + _ if their_desc.data_size % 128 != 0 => findings.note( + "BITSTREAM.DataSize[unpadded]", + au, + format!( + "libav's {} is not a multiple of 128, which means its tail padding was \ + clamped by a mapping too small for the AU", + their_desc.data_size + ), + ), + _ => { + // Classify on the UNPADDED sizes, which is the only way a small difference + // can be told from a large one: padding rounds up to 128, so an eight-byte + // delimitation difference shows as a delta of 0 or of a whole 128 depending + // on which side of a granule the two land. Theirs is not captured directly, + // but padding is 1..=128 bytes, so it lies in a known window — and the + // difference is legitimate exactly when that window reaches to within four + // bytes per slice of our own unpadded size. + let delta = i64::from(our_desc.data_size) - i64::from(their_desc.data_size); + let tolerance = 4 * our_slices.max(1) as i64; + let their_low = i64::from(their_desc.data_size) - 128; + let their_high = i64::from(their_desc.data_size) - 1; + let ours_unpadded = i64::from(sub.unpadded); + let legitimate = their_low <= ours_unpadded + tolerance + && ours_unpadded - tolerance <= their_high; + findings.note( + if legitimate { + "BITSTREAM.DataSize[delimitation]" + } else { + "BITSTREAM.DataSize" + }, + au, + format!( + "ours {} (unpadded {ours_unpadded}), libav {} (unpadded \ + {their_low}..={their_high}), delta {delta} over {our_slices} \ + slices — {}", + our_desc.data_size, + their_desc.data_size, + if legitimate { + "within the trailing-zero delimitation class the module docs \ + describe, but read it once rather than assume it" + } else { + "OUTSIDE the legitimate delimitation class: too large to be \ + trailing zeros" + } + ), + ); + } + } + } + } + findings +} + +// --------------------------------------------------------------------------- +// This side, in the capture's own format +// --------------------------------------------------------------------------- + +/// Write our submissions as the capture format, so a dump of ours and a capture of libav's can +/// be diffed by any tool — and so the parser above is exercised against a writer that shares no +/// code with it. +fn dump(codec: &str, ours: &[OurSubmission]) -> String { + let mut text = String::new(); + let raw = pf_dxvadec::short_slice_config(match codec { + "h264" => Codec::H264, + _ => Codec::H265, + }); + let _ = writeln!(text, "PFCFG {codec} 0 {raw}"); + for (au, sub) in ours.iter().enumerate() { + let _ = writeln!(text, "PFPP {codec} {au} {}", to_hex(&sub.pic_params)); + match &sub.qmatrix { + Some(qm) => { + let _ = writeln!(text, "PFQM {codec} {au} {}", to_hex(qm)); + } + None => { + let _ = writeln!(text, "PFQM {codec} {au} absent"); + } + } + for desc in &sub.descriptors { + let _ = writeln!( + text, + "PFBD {codec} {au} {} {} {} {}", + desc.buffer_type, desc.data_size, desc.num_mbs_in_buffer, desc.data_offset + ); + } + } + text +} + +// =========================================================================== +// CPU-provable: no capture, ordinary CI +// =========================================================================== + +/// Every hand-declared struct's field table must tile it exactly — no gap anywhere, and nothing +/// left over at the end. +/// +/// Two jobs in one. It is what makes the reports above name the right field (a gap would name the +/// wrong one, or none at all). And it is the AUDIT the twelve-vs-ten slice-record defect asked +/// for, in executable form: a struct tiled exactly by its members has no padding, interior OR +/// tail, so its Rust layout is the C declaration under 1-byte packing — which is how `dxva.h` +/// declares all six. The slice records are in the list precisely because they are the pair that +/// got it wrong; the other four were confirmed against libavcodec's runtime `sizeof` as well. +#[test] +fn every_hand_declared_dxva_struct_is_tiled_exactly_by_its_fields() { + for (what, fields, total) in [ + ("PicParamsH264", H264_FIELDS, size_of::()), + ("PicParamsHevc", HEVC_FIELDS, size_of::()), + ("QmatrixH264", H264_QMATRIX_FIELDS, size_of::()), + ("QmatrixHevc", HEVC_QMATRIX_FIELDS, size_of::()), + ( + "SliceH264Short", + H264_SLICE_FIELDS, + size_of::(), + ), + ( + "SliceHevcShort", + HEVC_SLICE_FIELDS, + size_of::(), + ), + ] { + assert_eq!(fields[0].1, 0, "{what}: the first field must start at 0"); + let ranges = field_ranges(fields, total); + let mut next = 0usize; + for (name, range) in &ranges { + assert_eq!( + range.start, next, + "{what}: {name} leaves a gap — the struct has no interior padding, so \ + consecutive offsets must tile it" + ); + assert!(range.end > range.start, "{what}: {name} is empty"); + next = range.end; + } + assert_eq!(next, total, "{what}: the table stops short of the struct"); + } +} + +#[test] +fn every_h264_au_submits_four_buffers_in_libavcodecs_order() { + for (au, sub) in our_h264_submissions().iter().enumerate() { + assert_eq!( + sub.descriptors + .iter() + .map(|d| d.buffer_type) + .collect::>(), + vec![ + BUFFER_PICTURE_PARAMETERS, + BUFFER_INVERSE_QUANTIZATION_MATRIX, + BUFFER_BITSTREAM, + BUFFER_SLICE_CONTROL, + ], + "AU {au}" + ); + } +} + +#[test] +fn every_h264_bitstream_and_slice_control_descriptor_carries_mb_width_times_mb_height() { + // Review 13's defect, over the whole vector: `NumMBsInBuffer` was 0 where libavcodec's + // H.264 path writes `h->mb_width * h->mb_height`, on the exact call this codebase has + // already seen an Intel driver reject a hand-built variant on. The vendored vector is + // 320x240 — 20x15 macroblocks. + for (au, sub) in our_h264_submissions().iter().enumerate() { + assert_eq!(sub.mb_count, 20 * 15, "AU {au}"); + for desc in &sub.descriptors { + let expected = match desc.buffer_type { + BUFFER_BITSTREAM | BUFFER_SLICE_CONTROL => sub.mb_count, + _ => 0, + }; + assert_eq!( + desc.num_mbs_in_buffer, + expected, + "AU {au}, {}", + buffer_name(desc.buffer_type) + ); + } + } +} + +#[test] +fn every_h264_au_submits_the_quantization_matrix_buffer() { + // H.264's predicate is UNCONDITIONAL in libavcodec (it passes `&ctx_pic->qm` with + // `sizeof(qm)` every time), and the PPS's lists are always meaningful because the vendored + // parser has applied Table 7-2's fallback rules. This is the codec where omitting the + // buffer would be the defect — the mirror image of HEVC's. + for (au, sub) in our_h264_submissions().iter().enumerate() { + assert!(sub.qmatrix.is_some(), "AU {au}"); + let desc = sub + .descriptors + .iter() + .find(|d| d.buffer_type == BUFFER_INVERSE_QUANTIZATION_MATRIX) + .unwrap_or_else(|| panic!("AU {au} submits no matrix buffer")); + assert_eq!(desc.data_size, size_of::() as u32); + assert_eq!(desc.num_mbs_in_buffer, 0); + } +} + +#[test] +fn the_whole_vendored_hevc_vector_omits_the_quantization_matrix_buffer() { + // Case 1 of the three the qmatrix predicate has: `scaling_list_enabled_flag` clear. The + // buffer is not submitted AT ALL — three descriptors, not four with an empty one. + let ours = our_hevc_submissions(); + for (au, sub) in ours.iter().enumerate() { + assert!(sub.qmatrix.is_none(), "AU {au}"); + assert_eq!( + sub.descriptors + .iter() + .map(|d| d.buffer_type) + .collect::>(), + vec![ + BUFFER_PICTURE_PARAMETERS, + BUFFER_BITSTREAM, + BUFFER_SLICE_CONTROL, + ], + "AU {au}" + ); + // …and the picture parameters say so, which is the predicate libavcodec reads. + let flags = u32_at( + &sub.pic_params, + offset_of!(PicParamsHevc, dwCodingParamToolFlags), + ); + assert_eq!(flags & 1, 0, "AU {au}: scaling_list_enabled_flag"); + } +} + +#[test] +fn no_hevc_descriptor_ever_carries_a_macroblock_count() { + // The asymmetry, over the whole vector: libavcodec's HEVC path writes 0 where its H.264 + // path writes mb_width*mb_height, so a CTB count here would be a fresh divergence in the + // other direction. + for (au, sub) in our_hevc_submissions().iter().enumerate() { + for desc in &sub.descriptors { + assert_eq!( + desc.num_mbs_in_buffer, + 0, + "AU {au}, {}", + buffer_name(desc.buffer_type) + ); + } + } +} + +#[test] +fn every_descriptor_of_both_codecs_starts_at_offset_zero_and_names_a_distinct_buffer() { + for (codec, subs) in [ + ("h264", our_h264_submissions()), + ("hevc", our_hevc_submissions()), + ] { + for (au, sub) in subs.iter().enumerate() { + let mut seen = BTreeSet::new(); + for desc in &sub.descriptors { + assert_eq!(desc.data_offset, 0, "{codec} AU {au}"); + assert!( + seen.insert(desc.buffer_type), + "{codec} AU {au}: buffer type {} submitted twice", + desc.buffer_type + ); + assert!(desc.data_size > 0, "{codec} AU {au}: an empty buffer"); + } + } + } +} + +#[test] +fn the_bitstream_descriptor_is_the_packers_padded_size_and_the_slice_records_tile_it_exactly() { + // The internal consistency a driver reads: every `BSNALunitDataLocation` lands inside the + // buffer, the records are contiguous from byte 0, and the last one ends EXACTLY at + // `DataSize` — which is what makes the tail padding charged to it rather than dangling past + // the end (see pack.rs's module docs). + for (codec, subs) in [ + ("h264", our_h264_submissions()), + ("hevc", our_hevc_submissions()), + ] { + for (au, sub) in subs.iter().enumerate() { + let bitstream = sub + .descriptors + .iter() + .find(|d| d.buffer_type == BUFFER_BITSTREAM) + .unwrap_or_else(|| panic!("{codec} AU {au} submits no bitstream buffer")); + assert_eq!(bitstream.data_size % 128, 0, "{codec} AU {au}: padded size"); + assert!( + bitstream.data_size >= sub.unpadded, + "{codec} AU {au}: {} bytes of slices in a {}-byte buffer", + sub.unpadded, + bitstream.data_size + ); + assert!( + bitstream.data_size - sub.unpadded <= 128, + "{codec} AU {au}: {} bytes of padding", + bitstream.data_size - sub.unpadded + ); + assert!(!sub.records.is_empty(), "{codec} AU {au}: no slices"); + let mut cursor = 0u32; + for (i, record) in sub.records.iter().enumerate() { + assert_eq!( + record.location, cursor, + "{codec} AU {au}: slice {i} location" + ); + assert!( + record.bytes > 3, + "{codec} AU {au}: slice {i} is start code only" + ); + cursor = record.location + record.bytes; + assert!( + cursor <= bitstream.data_size, + "{codec} AU {au}: slice {i} runs past DataSize" + ); + } + assert_eq!( + cursor, bitstream.data_size, + "{codec} AU {au}: the records must tile the whole buffer, padding included" + ); + } + } +} + +#[test] +fn the_slice_control_descriptor_is_one_ten_byte_short_format_record_per_slice_for_both_codecs() { + // Two facts in one, both measured against libavcodec on the RTX 4090 box rather than + // derived: + // + // * **The short record is TEN bytes.** The capture's slice-control `DataSize` is 20 on the + // H.264 vector, which is two slices per picture, and 10 on the HEVC vector, which is one + // slice segment per picture — two codecs, two slice counts, one record size. `dxva.h` + // packs these wire structures to a byte; a `#[repr(C)]` `{u32, u32, u16}` would be twelve + // and would displace every record after the first (see `dxva.rs`'s alignment section). + // * **The `ConfigBitstreamRaw` hazard**: short format is 2 for H.264 and 1 for HEVC — one + // number with two spellings — and these are the short records for both. A buffer sized for + // one format against a config negotiated for the other is a driver reading a different + // struct at every offset. + assert_eq!(pf_dxvadec::short_slice_config(Codec::H264), 2); + assert_eq!(pf_dxvadec::short_slice_config(Codec::H265), 1); + assert_eq!(size_of::(), 10); + assert_eq!(size_of::(), 10); + for (codec, subs, slices_per_picture, capture_data_size) in [ + ("h264", our_h264_submissions(), 2usize, 20u32), + ("hevc", our_hevc_submissions(), 1, 10), + ] { + for (au, sub) in subs.iter().enumerate() { + let control = sub + .descriptors + .iter() + .find(|d| d.buffer_type == BUFFER_SLICE_CONTROL) + .unwrap_or_else(|| panic!("{codec} AU {au} submits no slice-control buffer")); + assert_eq!( + control.data_size as usize, + 10 * sub.records.len(), + "{codec} AU {au}" + ); + // The vectors' slice counts, so the record size above is anchored to the capture's + // own number rather than to an arithmetic identity: if our splitter ever produced a + // different slice count for these streams, the two sides would stop being + // comparable and 10 would no longer follow from 20 and 10. + assert_eq!( + sub.records.len(), + slices_per_picture, + "{codec} AU {au}: the vendored vector is {slices_per_picture} slice(s) per picture" + ); + assert_eq!( + control.data_size, capture_data_size, + "{codec} AU {au}: libavcodec's captured slice-control DataSize" + ); + } + } +} + +#[test] +fn the_tail_padding_is_charged_to_the_last_slice_record_and_to_no_other() { + // libavcodec's `commit_bitstream_and_slice_buffer` zero-fills + // `FFMIN(128 - ((current - dxva_data) & 127), end - current)` bytes and then does + // `slice->SliceBytesInBuffer += padding` — on the FINAL loop iteration's record. So the last + // record's `SliceBytesInBuffer` counts the padding and every earlier record's does not, and + // a driver reading the records back must find them tiling the buffer exactly. + // [`pf_dxvadec::pack`] implements the same rule; this is where it is checked on the real + // vectors, on the H.264 one because that is the one with more than one slice per picture — + // the shape a single-slice vector cannot distinguish. + for (codec, subs) in [ + ("h264", our_h264_submissions()), + ("hevc", our_hevc_submissions()), + ] { + for (au, sub) in subs.iter().enumerate() { + let bitstream = sub + .descriptors + .iter() + .find(|d| d.buffer_type == BUFFER_BITSTREAM) + .unwrap_or_else(|| panic!("{codec} AU {au} submits no bitstream buffer")); + let padding = bitstream.data_size - sub.unpadded; + assert!( + (1..=128).contains(&padding), + "{codec} AU {au}: {padding} bytes of padding" + ); + let (last, earlier) = sub + .records + .split_last() + .unwrap_or_else(|| panic!("{codec} AU {au}: no slices")); + // Every earlier record stops exactly where the next slice's start code begins, so + // none of them carries any of the padding. + for (i, record) in earlier.iter().enumerate() { + assert_eq!( + record.location + record.bytes, + sub.records[i + 1].location, + "{codec} AU {au}: record {i} does not end where record {} begins", + i + 1 + ); + } + // The last one runs to the end of the buffer… + assert_eq!( + last.location + last.bytes, + bitstream.data_size, + "{codec} AU {au}: the last record must reach DataSize" + ); + // …and stripping the padding off it leaves exactly the slice bytes the packer wrote, + // which is the statement that the padding is in THAT record and nowhere else. + assert_eq!( + sub.records.iter().map(|r| r.bytes).sum::() - padding, + sub.unpadded, + "{codec} AU {au}: the padding is charged more than once, or not at all" + ); + assert!( + last.bytes > padding, + "{codec} AU {au}: the last record is padding only" + ); + } + } +} + +#[test] +fn the_picture_parameter_buffer_is_the_whole_hand_declared_struct_for_both_codecs() { + for (codec, subs, size) in [ + ("h264", our_h264_submissions(), size_of::()), + ("hevc", our_hevc_submissions(), size_of::()), + ] { + for (au, sub) in subs.iter().enumerate() { + assert_eq!(sub.pic_params.len(), size, "{codec} AU {au}"); + assert_eq!( + sub.descriptors[0].buffer_type, BUFFER_PICTURE_PARAMETERS, + "{codec} AU {au}" + ); + assert_eq!( + sub.descriptors[0].data_size as usize, size, + "{codec} AU {au}" + ); + } + } +} + +/// The vector's first HEVC plan with its parameter sets rewritten to a chosen scaling-list +/// shape, converted and packed — the three cases of 7.4.5's activation, at the descriptor level. +/// +/// Only the scaling-list fields move; everything else is the parser's own output, which is what +/// makes the "coded nowhere" case meaningful (`pps.scaling_list` then holds the parser's Table +/// 7-5/7-6 default fill, and `sps.scaling_list` the all-zero `ScalingLists::default()` an +/// uncoded SPS is left with). +fn hevc_case(enabled: bool, sps_coded: Option, pps_coded: Option) -> OurSubmission { + use std::rc::Rc; + + let aus = split_into_aus_h265(TEST_25FPS_H265); + let mut planner = H265Planner::new(); + let mut plan = planner.plan_au(aus[0]).expect("plan"); + + let mut sps = (*plan.sps).clone(); + sps.scaling_list_enabled_flag = enabled; + sps.scaling_list_data_present_flag = sps_coded.is_some(); + if let Some(fill) = sps_coded { + sps.scaling_list.scaling_list_4x4 = [[fill; 16]; 6]; + sps.scaling_list.scaling_list_8x8 = [[fill; 64]; 6]; + sps.scaling_list.scaling_list_16x16 = [[fill; 64]; 6]; + sps.scaling_list.scaling_list_32x32 = [[fill; 64]; 6]; + sps.scaling_list.scaling_list_dc_coef_minus8_16x16 = [i16::from(fill); 6]; + sps.scaling_list.scaling_list_dc_coef_minus8_32x32 = [i16::from(fill); 6]; + } + let mut pps = (*plan.pps).clone(); + pps.scaling_list_data_present_flag = pps_coded.is_some(); + if let Some(fill) = pps_coded { + pps.scaling_list.scaling_list_4x4 = [[fill; 16]; 6]; + pps.scaling_list.scaling_list_8x8 = [[fill; 64]; 6]; + pps.scaling_list.scaling_list_16x16 = [[fill; 64]; 6]; + pps.scaling_list.scaling_list_32x32 = [[fill; 64]; 6]; + pps.scaling_list.scaling_list_dc_coef_minus8_16x16 = [i16::from(fill); 6]; + pps.scaling_list.scaling_list_dc_coef_minus8_32x32 = [i16::from(fill); 6]; + } + plan.sps = Rc::new(sps); + plan.pps = Rc::new(pps); + + let mut slots = SlotMap::new(plan.picture.max_dpb_frames); + let dxva = pf_dxvadec::plan_to_dxva_h265(&plan, &mut slots, 1).expect("convert"); + let mut mapping = vec![0u8; MAPPING_BYTES]; + let packed = pf_dxvadec::pack(aus[0], &dxva.slice_ranges, &mut mapping).expect("pack"); + let unpadded = pf_dxvadec::packed_size(aus[0], &dxva.slice_ranges).expect("size") as u32; + OurSubmission { + pic_params: pf_dxvadec::as_bytes(&dxva.pic_params).to_vec(), + qmatrix: dxva + .qmatrix + .as_ref() + .map(|qm| pf_dxvadec::as_bytes(qm).to_vec()), + descriptors: pf_dxvadec::descriptors_h265(&dxva, &packed), + records: packed.records, + unpadded, + mb_count: 0, + } +} + +#[test] +fn an_hevc_sequence_that_disables_scaling_lists_submits_no_matrix_however_much_is_coded() { + // Case 1 again, with data coded in BOTH parameter sets: the flag decides, not the data. + let sub = hevc_case(false, Some(7), Some(9)); + assert!(sub.qmatrix.is_none()); + assert!(!sub + .descriptors + .iter() + .any(|d| d.buffer_type == BUFFER_INVERSE_QUANTIZATION_MATRIX)); +} + +#[test] +fn an_hevc_sequence_that_enables_scaling_lists_and_codes_them_submits_the_coded_lists() { + // Case 2: the buffer travels, sized to the whole struct, carrying the coded data. + let sub = hevc_case(true, Some(7), Some(9)); + let qm = sub + .qmatrix + .as_ref() + .expect("an enabled sequence submits the matrix"); + assert_eq!(qm.len(), size_of::()); + let desc = sub + .descriptors + .iter() + .find(|d| d.buffer_type == BUFFER_INVERSE_QUANTIZATION_MATRIX) + .expect("the matrix buffer is in the set"); + assert_eq!(desc.data_size as usize, size_of::()); + assert_eq!(desc.num_mbs_in_buffer, 0); + // The PPS's data wins over the SPS's (7.4.5), which is visible in the bytes themselves. + assert!( + qm.iter().all(|&b| b == 9 || b == 17), + "the PPS's fill of 9 (DC 9 + 8)" + ); + assert_eq!( + sub.descriptors + .iter() + .map(|d| d.buffer_type) + .collect::>(), + vec![ + BUFFER_PICTURE_PARAMETERS, + BUFFER_INVERSE_QUANTIZATION_MATRIX, + BUFFER_BITSTREAM, + BUFFER_SLICE_CONTROL, + ] + ); +} + +#[test] +fn an_hevc_sequence_that_enables_scaling_lists_but_codes_none_submits_the_defaults_not_zeros() { + // Case 3, and the one that is a live defect if it regresses: `scaling_list_enabled_flag` set + // with no scaling-list data in either parameter set is a legal, ordinary shape, and 7.4.5 + // says the Table 7-5/7-6 DEFAULTS apply. FFmpeg's parser seeds those defaults; the vendored + // cros-codecs parser leaves an uncoded SPS all-ZERO — so a conversion that read the SPS + // here would submit a matrix of zeros while bit 0 of dwCodingParamToolFlags told the driver + // it was authoritative, and every residual would dequantize to nothing. + // + // pic_h265.rs checks the CONTENTS against the spec tables transcribed by hand; this checks + // the two facts the descriptor level owns — the buffer is submitted, and what it carries is + // not zeros. + let sub = hevc_case(true, None, None); + let qm = sub + .qmatrix + .as_ref() + .expect("an enabled sequence submits the matrix even with nothing coded"); + let desc = sub + .descriptors + .iter() + .find(|d| d.buffer_type == BUFFER_INVERSE_QUANTIZATION_MATRIX) + .expect("the matrix buffer is in the set"); + assert_eq!(desc.data_size as usize, size_of::()); + assert!( + !qm.iter().all(|&b| b == 0), + "an all-zero matrix dequantizes every residual to nothing" + ); + // Table 7-5's 4x4 lists are flat 16, and the inferred DC is 8 + 8 = 16. + let lists0 = offset_of!(QmatrixHevc, ucScalingLists0); + assert!(qm[lists0..lists0 + 96].iter().all(|&b| b == 16)); + let dc2 = offset_of!(QmatrixHevc, ucScalingListDCCoefSizeID2); + assert!(qm[dc2..dc2 + 6].iter().all(|&b| b == 16)); + let dc3 = offset_of!(QmatrixHevc, ucScalingListDCCoefSizeID3); + assert!(qm[dc3..dc3 + 2].iter().all(|&b| b == 16)); + // …and every 8x8-and-up list carries a real curve rather than zeros. + let lists1 = offset_of!(QmatrixHevc, ucScalingLists1); + let lists3_end = offset_of!(QmatrixHevc, ucScalingListDCCoefSizeID2); + assert!(qm[lists1..lists3_end].iter().all(|&b| b != 0)); +} + +#[test] +fn the_dump_and_the_parser_agree_and_the_comparison_finds_nothing_against_ourselves() { + // The comparators, exercised on real 250-AU data with a known answer. This is not a + // tautology dressed as a test: the writer and the parser share no code, so a format drift + // on either side fails here, and — with the next two tests, which mutate the "capture" and + // require a NAMED finding — it is what keeps this file from reporting a clean bill of + // health while comparing nothing. + let ours = our_h264_submissions(); + let capture = parse_capture(&dump("h264", &ours), "h264"); + preflight( + &capture, + ours.len(), + "h264", + Some(offset_of!(PicParamsH264, Reserved16Bits)), + ); + assert_eq!(capture.pic_params.len(), VENDORED_AUS); + assert_eq!(capture.qmatrix.len(), VENDORED_AUS); + assert_eq!(capture.descriptors.len(), VENDORED_AUS); + for findings in [ + compare_h264_picparams(&ours, &capture), + compare_descriptors(&ours, &capture), + compare_qmatrix( + &ours, + &capture, + H264_QMATRIX_FIELDS, + size_of::(), + ), + ] { + assert!( + findings.is_empty(), + "comparing our own bytes against themselves must find nothing, got {:?}", + findings.fields() + ); + // Nor may anything be DOCUMENTED away: an allowance that fires on identical bytes is an + // allowance that would hide a real difference. + assert!( + findings.documented_fields().is_empty(), + "identical bytes documented a divergence: {:?}", + findings.documented_fields() + ); + } + + let ours = our_hevc_submissions(); + let capture = parse_capture(&dump("hevc", &ours), "hevc"); + // No Reserved16Bits check for HEVC: the ClearVideo workaround is an H.264-only path. + preflight(&capture, ours.len(), "hevc", None); + for findings in [ + compare_hevc_picparams(&ours, &capture), + compare_descriptors(&ours, &capture), + compare_qmatrix( + &ours, + &capture, + HEVC_QMATRIX_FIELDS, + size_of::(), + ), + ] { + assert!( + findings.is_empty(), + "comparing our own HEVC bytes against themselves must find nothing, got {:?}", + findings.fields() + ); + assert!( + findings.documented_fields().is_empty(), + "identical bytes documented a divergence: {:?}", + findings.documented_fields() + ); + } + // The HEVC matrices are `absent` on this vector, and the parser must carry that fact rather + // than losing it — the whole of review 13's defect is the difference between the two. + assert!(capture.qmatrix.values().all(Option::is_none)); +} + +/// A submission holding only what [`compare_descriptors`] reads, for the bitstream-size +/// classifier: `slices` slice records tiling a `padded`-byte buffer whose slice data (before the +/// tail padding) is `unpadded` bytes. +fn descriptor_only_submission(unpadded: u32, padded: u32, slices: usize) -> OurSubmission { + let each = unpadded / slices as u32; + let mut records: Vec = (0..slices) + .map(|i| SliceRecord { + location: i as u32 * each, + bytes: each, + }) + .collect(); + // The last record carries the remainder and the padding, exactly as the packer charges it. + let last = records.last_mut().expect("at least one slice"); + last.bytes = padded - last.location; + OurSubmission { + pic_params: vec![0u8; size_of::()], + qmatrix: None, + descriptors: vec![ + BufferDescriptor { + buffer_type: BUFFER_BITSTREAM, + data_offset: 0, + data_size: padded, + num_mbs_in_buffer: 300, + }, + BufferDescriptor { + buffer_type: BUFFER_SLICE_CONTROL, + data_offset: 0, + data_size: 10 * slices as u32, + num_mbs_in_buffer: 300, + }, + ], + records, + unpadded, + mb_count: 300, + } +} + +/// Rewrite every `PFPP ` line of a capture through `f`, which receives the AU index and +/// the picture-parameter bytes. The instrument the two absorbers below are tested with: a +/// divergence this harness ABSORBS must be reproducible on demand, or its allowance is untested. +fn map_picparams(text: &str, codec: &str, mut f: impl FnMut(usize, &mut Vec)) -> String { + let prefix = format!("PFPP {codec} "); + let mut out = String::new(); + for line in text.lines() { + match line.strip_prefix(&prefix) { + Some(rest) => { + let (au, hex) = rest.split_once(' ').expect("our own dump is well formed"); + let au: usize = au.parse().expect("a decimal AU index"); + let mut bytes = from_hex(hex).expect("our own dump is hex"); + f(au, &mut bytes); + let _ = writeln!(out, "{prefix}{au} {}", to_hex(&bytes)); + } + None => { + let _ = writeln!(out, "{line}"); + } + } + } + out +} + +/// Add `delta` to every POC an H.264 picture-parameters buffer carries: the current picture's pair +/// and every IN-USE reference entry's (an unused entry's counts are zero on both sides and must +/// stay that way). This is libavcodec's `prev_poc_msb` offset, synthesised. +fn shift_h264_capture_poc(pp: &mut [u8], delta: i32) { + let curr = offset_of!(PicParamsH264, CurrFieldOrderCnt); + let list = offset_of!(PicParamsH264, RefFrameList); + let focl = offset_of!(PicParamsH264, FieldOrderCntList); + for field in [curr, curr + 4] { + let shifted = i32_at(pp, field).wrapping_add(delta); + pp[field..field + 4].copy_from_slice(&shifted.to_le_bytes()); + } + for i in 0..16 { + if pp[list + i] == UNUSED_ENTRY { + continue; + } + for field in [focl + 8 * i, focl + 8 * i + 4] { + let shifted = i32_at(pp, field).wrapping_add(delta); + pp[field..field + 4].copy_from_slice(&shifted.to_le_bytes()); + } + } +} + +/// A one-AU capture of `(buffer type, DataSize, NumMBsInBuffer)` descriptors. +fn descriptor_capture(descs: &[(u32, u32, u32)]) -> String { + let mut text = String::new(); + for (buffer_type, data_size, mbs) in descs { + let _ = writeln!(text, "PFBD h264 0 {buffer_type} {data_size} {mbs} 0"); + } + text +} + +#[test] +fn libavcodecs_constant_poc_base_is_documented_and_anything_else_about_a_poc_is_a_finding() { + // The absorber that lets the H.264 comparison pass against the real capture, and the three + // ways it must NOT absorb. Without this test the POC fields would simply be excluded from the + // comparison, which is the "green gate that proves nothing" shape this program has been bitten + // by: an excluded field cannot report a wrong POC either. + let ours = our_h264_submissions(); + let base = dump("h264", &ours); + + // 1. Every POC offset by FFmpeg's 65536 — what the RTX 4090 capture actually carries. + let shifted = map_picparams(&base, "h264", |_, pp| shift_h264_capture_poc(pp, 65536)); + let findings = compare_h264_picparams(&ours, &parse_capture(&shifted, "h264")); + assert!( + findings.is_empty(), + "a constant POC base is not a finding, got {:?}", + findings.fields() + ); + assert_eq!( + findings.documented_fields(), + vec!["FieldOrderCnt[POC base]"] + ); + + // 2. A base that is neither 0 nor 65536 is unexplained, and unexplained is a finding. + let odd = map_picparams(&base, "h264", |_, pp| shift_h264_capture_poc(pp, 7)); + let findings = compare_h264_picparams(&ours, &parse_capture(&odd, "h264")); + assert_eq!(findings.fields(), vec!["CurrFieldOrderCnt[0][POC base]"]); + assert!(findings.documented_fields().is_empty()); + + // 3. A base that stops holding is a real POC divergence: 65536 everywhere except one AU, + // whose current picture is four counts adrift. + let drifting = map_picparams(&base, "h264", |au, pp| { + shift_h264_capture_poc(pp, if au == 10 { 65536 - 4 } else { 65536 }) + }); + let findings = compare_h264_picparams(&ours, &parse_capture(&drifting, "h264")); + assert_eq!( + findings.fields(), + vec![ + "CurrFieldOrderCnt[0][POC]", + "CurrFieldOrderCnt[1][POC]", + "RefFrameList[set]", + ] + ); + assert_eq!(findings.by_field["CurrFieldOrderCnt[0][POC]"].first_au, 10); +} + +#[test] +fn the_hevc_tiles_flag_allowance_is_exactly_bit_ten_with_tiles_disabled_and_nothing_else() { + // The other absorber, and the reason it is written per-DIFFERENCE rather than per-field: the + // same word carries eighteen other flags, and a difference in any of them — or in bit 10 while + // tiles are ENABLED, where the flag stops being inert — must still be a finding. + let ours = our_hevc_submissions(); + let base = dump("hevc", &ours); + let at = offset_of!(PicParamsHevc, dwCodingSettingPicturePropertyFlags); + let rewrite = |text: &str, mask_off: u32, mask_on: u32| { + map_picparams(text, "hevc", |_, pp| { + let flags = (u32_at(pp, at) & !mask_off) | mask_on; + pp[at..at + 4].copy_from_slice(&flags.to_le_bytes()); + }) + }; + + // Bit 10 clear on libav's side, tiles disabled on both: the documented divergence, which is + // what the real capture carries on all 250 AUs. + let capture = parse_capture(&rewrite(&base, 1 << 10, 0), "hevc"); + let findings = compare_hevc_picparams(&ours, &capture); + assert!( + findings.is_empty(), + "the documented tiles-flag divergence is not a finding, got {:?}", + findings.fields() + ); + assert_eq!( + findings.documented_fields(), + vec!["dwCodingSettingPicturePropertyFlags"] + ); + + // A different bit of the same word is a finding: bit 11 is + // pps_loop_filter_across_slices_enabled_flag, which is not inert at all. + let capture = parse_capture(&rewrite(&base, 1 << 11, 0), "hevc"); + let findings = compare_hevc_picparams(&ours, &capture); + assert_eq!( + findings.fields(), + vec!["dwCodingSettingPicturePropertyFlags"] + ); + assert!(findings.documented_fields().is_empty()); + + // Bit 10 differing while BOTH sides say tiles are enabled: the flag now governs a real tile + // boundary, so the allowance must not apply. + let ours_with_tiles: Vec = ours + .iter() + .map(|sub| { + let mut pp = sub.pic_params.clone(); + let flags = u32_at(&pp, at) | (1 << 7) | (1 << 10); + pp[at..at + 4].copy_from_slice(&flags.to_le_bytes()); + OurSubmission { + pic_params: pp, + qmatrix: sub.qmatrix.clone(), + descriptors: sub.descriptors.clone(), + records: sub.records.clone(), + unpadded: sub.unpadded, + mb_count: sub.mb_count, + } + }) + .collect(); + let capture = parse_capture( + &rewrite(&dump("hevc", &ours_with_tiles), 1 << 10, 1 << 7), + "hevc", + ); + let findings = compare_hevc_picparams(&ours_with_tiles, &capture); + assert_eq!( + findings.fields(), + vec!["dwCodingSettingPicturePropertyFlags"] + ); + assert!(findings.documented_fields().is_empty()); +} + +#[test] +fn a_bitstream_size_difference_is_classified_by_the_unpadded_window_it_implies() { + // The one descriptor field with a legitimate divergence class, and the arithmetic that tells + // the two apart. Ours: 1026 bytes of slice data over two slices, padded to 1152. Their + // padding is not captured, but it is 1..=128 bytes, so a captured 1024 means their slice data + // was 896..=1023 — which reaches to within four bytes per slice of our 1026 (1022 is 4 bytes + // less over two slices), the trailing-zero delimitation shape. A captured 512 cannot: no + // padding puts their slice data anywhere near ours. + let ours = vec![descriptor_only_submission(1026, 1152, 2)]; + + let legitimate = descriptor_capture(&[ + (BUFFER_BITSTREAM, 1024, 300), + (BUFFER_SLICE_CONTROL, 20, 300), + ]); + let findings = compare_descriptors(&ours, &parse_capture(&legitimate, "h264")); + assert_eq!(findings.fields(), vec!["BITSTREAM.DataSize[delimitation]"]); + + let defect = descriptor_capture(&[ + (BUFFER_BITSTREAM, 512, 300), + (BUFFER_SLICE_CONTROL, 20, 300), + ]); + let findings = compare_descriptors(&ours, &parse_capture(&defect, "h264")); + assert_eq!(findings.fields(), vec!["BITSTREAM.DataSize"]); + + // A differing slice count outranks the size: the two sides split the AU differently, which + // voids the size comparison rather than needing a verdict of its own. Three ten-byte records + // where ours has two. + let split = descriptor_capture(&[ + (BUFFER_BITSTREAM, 1024, 300), + (BUFFER_SLICE_CONTROL, 30, 300), + ]); + let findings = compare_descriptors(&ours, &parse_capture(&split, "h264")); + assert_eq!( + findings.fields(), + vec!["BITSTREAM.DataSize[slice count]", "SLICE_CONTROL.DataSize"] + ); + + // And a NumMBsInBuffer difference is never in the legitimate class. + let zeroed = descriptor_capture(&[(BUFFER_BITSTREAM, 1152, 0), (BUFFER_SLICE_CONTROL, 20, 0)]); + let findings = compare_descriptors(&ours, &parse_capture(&zeroed, "h264")); + assert_eq!( + findings.fields(), + vec!["BITSTREAM.NumMBsInBuffer", "SLICE_CONTROL.NumMBsInBuffer"] + ); +} + +#[test] +fn a_changed_scalar_field_is_reported_by_its_name() { + let ours = our_h264_submissions(); + let mut text = dump("h264", &ours); + // Flip `pic_init_qp_minus26` (offset 172) on AU 7 of the "capture". + let offset = offset_of!(PicParamsH264, pic_init_qp_minus26); + text = mutate_capture_byte(&text, 7, offset, 0x5A); + let capture = parse_capture(&text, "h264"); + let findings = compare_h264_picparams(&ours, &capture); + assert_eq!(findings.fields(), vec!["pic_init_qp_minus26"]); + assert_eq!(findings.by_field["pic_init_qp_minus26"].first_au, 7); + + // …and a differing byte the field table does NOT cover is still reported, by raw offset. + // The table tiles both structs (asserted above), so this fallback is unreachable in + // practice; it exists so that a field added to the struct without being added to the table + // cannot pass unnoticed, and it is exercised here with a deliberately truncated table rather + // than left as an unproven claim in the module docs. + let mut findings = Findings::default(); + let mut theirs = ours[0].pic_params.clone(); + theirs[offset] = 0x5A; + compare_scalars( + 0, + &ours[0].pic_params, + &theirs, + &field_ranges(&H264_FIELDS[..2], 4), + &[], + no_allowance, + &mut findings, + ); + let expected = format!(""); + assert_eq!(findings.fields(), vec![expected.as_str()]); +} + +#[test] +fn a_reordered_reference_list_is_no_finding_but_a_changed_one_is() { + // The divergence this harness must NOT report, and the one it must. libavcodec emits its + // reference array in a different order from ours by construction; a set comparison sees + // through that. Dropping a reference — or renumbering one side's surfaces inconsistently — + // must still be caught. + let ours = our_h264_submissions(); + // An AU with at least two references, so a reversal is observable. + let (au, entries) = ours + .iter() + .enumerate() + .map(|(au, sub)| (au, h264_ref_entries(&sub.pic_params))) + .find(|(_, entries)| entries.len() >= 2) + .expect("the vector must reach two references"); + + let reordered = reverse_h264_reference_list(&ours[au].pic_params); + assert_ne!( + reordered, ours[au].pic_params, + "the reversal must change bytes" + ); + let capture = parse_capture( + &with_picparams(&dump("h264", &ours), au, &reordered), + "h264", + ); + let findings = compare_h264_picparams(&ours, &capture); + assert!( + findings.is_empty(), + "a reordered reference list is not a finding, got {:?}", + findings.fields() + ); + + // Now DROP the last reference from that AU: the set differs, and it must be named. + let mut dropped = ours[au].pic_params.clone(); + let list = offset_of!(PicParamsH264, RefFrameList); + let last = entries.len() - 1; + dropped[list + last] = UNUSED_ENTRY; + let used = offset_of!(PicParamsH264, UsedForReferenceFlags); + let cleared = u32_at(&dropped, used) & !(0b11 << (2 * last)); + dropped[used..used + 4].copy_from_slice(&cleared.to_le_bytes()); + let capture = parse_capture(&with_picparams(&dump("h264", &ours), au, &dropped), "h264"); + let findings = compare_h264_picparams(&ours, &capture); + assert!( + findings.fields().contains(&"RefFrameList[set]"), + "a dropped reference must be reported, got {:?}", + findings.fields() + ); +} + +#[test] +fn a_wholly_renumbered_surface_set_is_no_finding_and_an_inconsistent_one_is() { + // The bijection, both ways round. Renumbering EVERY surface index on the capture side is + // exactly what a different frame pool does, and it must pass; renumbering one AU's alone + // breaks the mapping under live pictures and must not. + let ours = our_h264_submissions(); + let base = dump("h264", &ours); + + let renumbered: Vec> = ours + .iter() + .map(|sub| renumber_h264_surfaces(&sub.pic_params, |slot| slot + 8)) + .collect(); + let mut text = base.clone(); + for (au, pp) in renumbered.iter().enumerate() { + text = with_picparams(&text, au, pp); + } + let capture = parse_capture(&text, "h264"); + let findings = compare_h264_picparams(&ours, &capture); + assert!( + findings.is_empty(), + "a consistently renumbered surface set is a bijection, not a finding, got {:?}", + findings.fields() + ); + + // One AU renumbered differently from the rest: the pictures it still holds change surface + // mid-life, which is precisely what a mis-resolved reference looks like. + let au = ours + .iter() + .position(|sub| h264_ref_entries(&sub.pic_params).len() >= 2) + .expect("two references"); + let mut text = base; + for (i, pp) in renumbered.iter().enumerate() { + if i != au { + text = with_picparams(&text, i, pp); + } + } + let capture = parse_capture(&text, "h264"); + let findings = compare_h264_picparams(&ours, &capture); + assert!( + findings + .fields() + .iter() + .any(|f| f.contains("surface mapping")), + "an inconsistent surface numbering must be reported, got {:?}", + findings.fields() + ); +} + +#[test] +fn an_omitted_hevc_matrix_buffer_is_reported_as_a_presence_difference() { + // Review 13's HEVC defect, in the form the harness would have caught it: the capture says + // `absent`, our side submits one. (Built by rewriting the capture rather than the crate, + // because the crate no longer has the defect — which is the point.) + let ours = our_hevc_submissions(); + let mut subs = ours; + subs[3].qmatrix = Some(vec![0u8; size_of::()]); + subs[3].descriptors = vec![ + BufferDescriptor { + buffer_type: BUFFER_PICTURE_PARAMETERS, + data_offset: 0, + data_size: size_of::() as u32, + num_mbs_in_buffer: 0, + }, + BufferDescriptor { + buffer_type: BUFFER_INVERSE_QUANTIZATION_MATRIX, + data_offset: 0, + data_size: size_of::() as u32, + num_mbs_in_buffer: 0, + }, + subs[3].descriptors[1], + subs[3].descriptors[2], + ]; + // The capture is the honest one: no matrix on any AU. + let honest = our_hevc_submissions(); + let capture = parse_capture(&dump("hevc", &honest), "hevc"); + + let findings = compare_qmatrix( + &subs, + &capture, + HEVC_QMATRIX_FIELDS, + size_of::(), + ); + assert_eq!(findings.fields(), vec![""]); + assert_eq!(findings.by_field[""].first_au, 3); + let findings = compare_descriptors(&subs, &capture); + assert_eq!(findings.fields(), vec![""]); +} + +#[test] +fn a_missing_bitstream_descriptor_is_reported_as_a_missed_patch_site_not_a_defect() { + // The one capture-side mistake that is likely, because libavcodec fills the bitstream + // descriptor outside the choke point: three PFBD lines per AU instead of four. The harness + // must name the patch site rather than accuse our submission of dropping a buffer. + let ours = our_h264_submissions(); + let text: String = dump("h264", &ours) + .lines() + .filter(|line| { + // Only the BITSTREAM descriptor, which is the FOURTH token: matching on " 6 " + // anywhere would also delete AU 6's whole descriptor set. + let fields: Vec<&str> = line.split_whitespace().collect(); + !(fields.first() == Some(&"PFBD") && fields.get(3) == Some(&"6")) + }) + .map(|line| format!("{line}\n")) + .collect(); + let capture = parse_capture(&text, "h264"); + let findings = compare_descriptors(&ours, &capture); + assert_eq!(findings.fields(), vec![""]); + assert!( + findings.by_field[""] + .detail + .contains("commit_bitstream_and_slice_buffer"), + "the report must name the patch site, got {:?}", + findings.by_field[""].detail + ); +} + +#[test] +fn an_unreadable_or_short_capture_is_refused_rather_than_partly_compared() { + let ours = our_h264_submissions(); + let good = dump("h264", &ours); + + // A line whose format drifted. + let broken = good.replace("PFPP h264 5 ", "PFPP h264 5 zz"); + let capture = parse_capture(&broken, "h264"); + assert_eq!(capture.unreadable.len(), 1); + + // A capture of the wrong stream length. + let short: String = good + .lines() + .filter(|line| !line.starts_with("PFPP h264 24 ")) + .map(|line| format!("{line}\n")) + .collect(); + let capture = parse_capture(&short, "h264"); + assert_eq!(capture.pic_params.len(), VENDORED_AUS - 1); + assert!(!capture.pic_params.contains_key(&24)); + + // Another codec's lines are not this codec's. + assert!(parse_capture(&good, "hevc").pic_params.is_empty()); + + // A capture logged against a codec context rather than NULL carries FFmpeg's own + // `[h264 @ 0x…] ` prefix, and must still read cleanly. + let prefixed: String = good + .lines() + .map(|line| format!("[h264 @ 0x7ff1c380a200] {line}\n")) + .collect(); + let capture = parse_capture(&prefixed, "h264"); + assert!(capture.unreadable.is_empty()); + assert_eq!(capture.pic_params.len(), VENDORED_AUS); + assert_eq!(capture.descriptors.len(), VENDORED_AUS); +} + +// --------------------------------------------------------------------------- +// Capture-side mutators, for the tests above +// --------------------------------------------------------------------------- + +/// Replace AU `au`'s `PFPP` line with `pp`. +fn with_picparams(text: &str, au: usize, pp: &[u8]) -> String { + let prefix = format!("PFPP h264 {au} "); + let hevc = format!("PFPP hevc {au} "); + text.lines() + .map(|line| { + if line.starts_with(&prefix) { + format!("{prefix}{}\n", to_hex(pp)) + } else if line.starts_with(&hevc) { + format!("{hevc}{}\n", to_hex(pp)) + } else { + format!("{line}\n") + } + }) + .collect() +} + +/// Set one byte of AU `au`'s captured picture parameters. +fn mutate_capture_byte(text: &str, au: usize, offset: usize, value: u8) -> String { + let prefix = format!("PFPP h264 {au} "); + let mut out = String::new(); + for line in text.lines() { + if let Some(hex) = line.strip_prefix(&prefix) { + let mut bytes = from_hex(hex).expect("our own dump is hex"); + bytes[offset] = value; + let _ = writeln!(out, "{prefix}{}", to_hex(&bytes)); + } else { + let _ = writeln!(out, "{line}"); + } + } + out +} + +/// Reverse the in-use entries of an H.264 reference array, carrying each entry's keys and flag +/// bits with it — the reordering libavcodec's own `short_ref`-then-`long_ref` walk produces, +/// synthesised so the set comparison can be tested without a capture. +fn reverse_h264_reference_list(pp: &[u8]) -> Vec { + let mut out = pp.to_vec(); + let list = offset_of!(PicParamsH264, RefFrameList); + let poc = offset_of!(PicParamsH264, FieldOrderCntList); + let nums = offset_of!(PicParamsH264, FrameNumList); + let used_at = offset_of!(PicParamsH264, UsedForReferenceFlags); + let missing_at = offset_of!(PicParamsH264, NonExistingFrameFlags); + let entries = h264_ref_entries(pp); + let slots: Vec = (0..entries.len()).map(|i| pp[list + i]).collect(); + let used = u32_at(pp, used_at); + let missing = u16_at(pp, missing_at); + let mut new_used = used & !((1u32 << (2 * entries.len())) - 1); + let mut new_missing = missing & !((1u16 << entries.len()) - 1); + for (i, source) in (0..entries.len()).rev().enumerate() { + out[list + i] = slots[source]; + out[nums + 2 * i..nums + 2 * i + 2] + .copy_from_slice(&pp[nums + 2 * source..nums + 2 * source + 2]); + out[poc + 8 * i..poc + 8 * i + 8] + .copy_from_slice(&pp[poc + 8 * source..poc + 8 * source + 8]); + new_used |= (used >> (2 * source) & 0b11) << (2 * i); + new_missing |= (missing >> source & 1) << i; + } + out[used_at..used_at + 4].copy_from_slice(&new_used.to_le_bytes()); + out[missing_at..missing_at + 2].copy_from_slice(&new_missing.to_le_bytes()); + out +} + +/// Rewrite every surface index of an H.264 picture-parameters buffer through `f`. +fn renumber_h264_surfaces(pp: &[u8], f: impl Fn(u8) -> u8) -> Vec { + let mut out = pp.to_vec(); + let curr = offset_of!(PicParamsH264, CurrPic); + out[curr] = (out[curr] & 0x80) | (f(out[curr] & 0x7F) & 0x7F); + let list = offset_of!(PicParamsH264, RefFrameList); + for i in 0..16 { + if out[list + i] != UNUSED_ENTRY { + out[list + i] = (out[list + i] & 0x80) | (f(out[list + i] & 0x7F) & 0x7F); + } + } + out +} + +// =========================================================================== +// Capture-dependent: #[ignore]d, and saying why +// =========================================================================== + +/// Emit this crate's whole submission — both codecs — in the capture's own format, so the two +/// files can be diffed by any tool without a capture at all. +#[test] +#[ignore = "writes a dump: PF_DXVA_DUMP="] +fn dump_our_submission_in_the_captures_own_format() { + let path = std::env::var("PF_DXVA_DUMP").expect("PF_DXVA_DUMP= names the output file"); + let mut text = dump("h264", &our_h264_submissions()); + text.push_str(&dump("hevc", &our_hevc_submissions())); + std::fs::write(&path, text).expect("write the dump"); + println!("wrote {path}"); +} + +#[test] +#[ignore = "needs a libavcodec capture: PF_LIBAV_CAPTURE_H264= (see the module docs)"] +fn our_h264_picture_parameters_match_libavcodecs() { + let capture = capture_from_env("PF_LIBAV_CAPTURE_H264", "h264") + .expect("PF_LIBAV_CAPTURE_H264= names a capture (see the module docs)"); + let ours = our_h264_submissions(); + preflight( + &capture, + ours.len(), + "h264", + Some(offset_of!(PicParamsH264, Reserved16Bits)), + ); + compare_h264_picparams(&ours, &capture).verdict("H.264 picture parameters", ours.len()); +} + +#[test] +#[ignore = "needs a libavcodec capture: PF_LIBAV_CAPTURE_HEVC= (see the module docs)"] +fn our_hevc_picture_parameters_match_libavcodecs() { + let capture = capture_from_env("PF_LIBAV_CAPTURE_HEVC", "hevc") + .expect("PF_LIBAV_CAPTURE_HEVC= names a capture (see the module docs)"); + let ours = our_hevc_submissions(); + preflight(&capture, ours.len(), "hevc", None); + compare_hevc_picparams(&ours, &capture).verdict("HEVC picture parameters", ours.len()); +} + +#[test] +#[ignore = "needs a libavcodec capture: PF_LIBAV_CAPTURE_H264/PF_LIBAV_CAPTURE_HEVC (module docs)"] +fn our_buffer_descriptors_match_libavcodecs() { + let h264 = capture_from_env("PF_LIBAV_CAPTURE_H264", "h264"); + let hevc = capture_from_env("PF_LIBAV_CAPTURE_HEVC", "hevc"); + assert!( + h264.is_some() || hevc.is_some(), + "PF_LIBAV_CAPTURE_H264= and/or PF_LIBAV_CAPTURE_HEVC= name a capture" + ); + if let Some(capture) = h264 { + let ours = our_h264_submissions(); + preflight( + &capture, + ours.len(), + "h264", + Some(offset_of!(PicParamsH264, Reserved16Bits)), + ); + compare_descriptors(&ours, &capture).verdict("H.264 buffer descriptors", ours.len()); + } + if let Some(capture) = hevc { + let ours = our_hevc_submissions(); + preflight(&capture, ours.len(), "hevc", None); + compare_descriptors(&ours, &capture).verdict("HEVC buffer descriptors", ours.len()); + } +} + +#[test] +#[ignore = "needs a libavcodec capture: PF_LIBAV_CAPTURE_H264/PF_LIBAV_CAPTURE_HEVC (module docs)"] +fn our_quantization_matrices_match_libavcodecs() { + let h264 = capture_from_env("PF_LIBAV_CAPTURE_H264", "h264"); + let hevc = capture_from_env("PF_LIBAV_CAPTURE_HEVC", "hevc"); + assert!( + h264.is_some() || hevc.is_some(), + "PF_LIBAV_CAPTURE_H264= and/or PF_LIBAV_CAPTURE_HEVC= name a capture" + ); + if let Some(capture) = h264 { + let ours = our_h264_submissions(); + preflight( + &capture, + ours.len(), + "h264", + Some(offset_of!(PicParamsH264, Reserved16Bits)), + ); + compare_qmatrix( + &ours, + &capture, + H264_QMATRIX_FIELDS, + size_of::(), + ) + .verdict("H.264 quantization matrices", ours.len()); + } + if let Some(capture) = hevc { + let ours = our_hevc_submissions(); + preflight(&capture, ours.len(), "hevc", None); + compare_qmatrix( + &ours, + &capture, + HEVC_QMATRIX_FIELDS, + size_of::(), + ) + .verdict("HEVC quantization matrices", ours.len()); + } +} diff --git a/crates/pf-encode/src/enc/codec.rs b/crates/pf-encode/src/enc/codec.rs index b09ebf3b..61188a3f 100644 --- a/crates/pf-encode/src/enc/codec.rs +++ b/crates/pf-encode/src/enc/codec.rs @@ -443,6 +443,20 @@ pub trait Encoder: Send { /// flagged [`EncodedFrame::chunk_aligned`] and the session marks them on the wire. /// Default: no-op (the H.26x backends' bitstreams cannot be cut losslessly). fn set_wire_chunking(&mut self, _shard_payload: usize) {} + /// How long a whole AU's packets currently take to leave the socket (µs, smoothed) — the + /// host's paced-send `spread_us`. + /// + /// Exists for ONE decision, and only the host can supply it. The Linux direct-NVENC split + /// arbitration compares single-engine against split, but on HEVC engaging split costs + /// sub-frame readback, and sub-frame's whole value is that the send overlaps the encode. So + /// the real comparison is `encode_1eng + send_of_last_slice` against + /// `encode_2eng + send_of_whole_AU`, and an encoder that measures only encode time would + /// reliably pick split and make end-to-end latency WORSE. The backend turns this number into + /// that handicap (it knows its own slice count); the host just reports what it observes. + /// + /// Optional by design: a backend that ignores it simply never arbitrates the sub-frame trade, + /// which is the safe direction. `0` = unknown / not reported yet. + fn set_send_spread_us(&mut self, _us: u32) {} /// How many frames the CAPTURER guarantees the encoder may hold in flight before it starts /// reusing an input texture (`Capturer::pipeline_depth`). Backends that encode the capturer's /// textures IN PLACE — no `CopyResource` — must not pipeline deeper than this: the capturer @@ -504,7 +518,7 @@ impl Codec { } /// Pixel rate (luma samples/s) at or above which NVENC split-frame encoding is FORCED 2-way — -/// one number shared by the direct-SDK selector (`nvenc_core::resolve_split_mode`) and the libav +/// one number shared by the direct-SDK selector ([`resolve_split_mode`]) and the libav /// `split_encode_mode` option author (`linux::NvencEncoder`), so the two paths can never disagree /// about which modes split. A single NVENC engine tops out ~1 Gpix/s on HEVC, and AUTO doesn't /// engage below ~2112 px height, so the sessions that need the second engine must be forced. Set @@ -514,6 +528,191 @@ impl Codec { /// comfortably single-engine) on AUTO. pub const SPLIT_FORCE_PIXEL_RATE: u64 = 950_000_000; +/// The `NV_ENC_SPLIT_ENCODE_MODE` values, as plain constants. +/// +/// They live HERE, not in `nvenc_core`, because the split policy below has to be shared with the +/// **libav** NVENC path — which compiles with the `nvenc` feature OFF (that is the whole +/// `PUNKTFUNK_NVENC_DIRECT=0` / featureless-package build), where the SDK enum does not exist. +/// One policy, no drift, was the point of extracting it; gating it behind the feature would have +/// left the libav copy free to diverge again, which is exactly what it had already done. +/// +/// `nvenc_split_constants_match_the_sdk` (feature-gated) pins these against the real enum, so the +/// hand-written values cannot rot. +// +// SPLIT-POLICY GATE — these constants and the three selectors below (`resolve_split_mode`, +// `max_forced_split_mode`, `clamp_to_engines`) share one cfg: the UNION of their callers'. +// - Linux, any features: the libav NVENC path (`enc/linux/mod.rs`) calls `resolve_split_mode` +// unconditionally, which is the whole reason the policy lives in this featureless file. +// - Windows: the ONLY caller is the direct-SDK backend (`enc/windows/nvenc.rs`), which needs +// `feature = "nvenc"`. Without it nothing on Windows reads any of this. +// `codec.rs` compiles everywhere, so ungated the whole cluster is dead code on a featureless +// Windows build — and `dead_code` is an ITEM lint, so reasoning about the module's own cfg does +// not catch it. That is exactly how this reached main: the CI step lints pf-encode itself WITH +// `--features nvenc,amf-qsv,qsv --all-targets`, so the items are live there; the failure came +// from the NEXT command in the same step, `clippy -p pf-vdisplay`, which pulls pf-encode in as a +// plain default-features dependency. Same trap as `forced_split_width` below, `subframe_env_forced` +// and the `nvenc_core` arbiter items — the fifth time in this crate. +#[cfg(any(target_os = "linux", all(target_os = "windows", feature = "nvenc")))] +pub(crate) const SPLIT_AUTO: u32 = 0; +#[cfg(any(target_os = "linux", all(target_os = "windows", feature = "nvenc")))] +pub(crate) const SPLIT_AUTO_FORCED: u32 = 1; +#[cfg(any(target_os = "linux", all(target_os = "windows", feature = "nvenc")))] +pub(crate) const SPLIT_TWO_FORCED: u32 = 2; +#[cfg(any(target_os = "linux", all(target_os = "windows", feature = "nvenc")))] +pub(crate) const SPLIT_THREE_FORCED: u32 = 3; +#[cfg(any(target_os = "linux", all(target_os = "windows", feature = "nvenc")))] +pub(crate) const SPLIT_DISABLE: u32 = 15; + +/// Resolved NVENC split-frame encode mode for a session — ONE selector shared by the Windows and +/// Linux direct-SDK backends (they had drifted into byte-identical duplicates, one of which +/// logged and one didn't). Precedence: +/// 1. `PUNKTFUNK_SPLIT_ENCODE` = `0`/`disable` | `1`/`auto` (AUTO_FORCED) | `2` | `3` — operator +/// override, always wins, except that `2`/`3` are clamped to the GPU's real engine count (see +/// [`clamp_to_engines`]; the driver honours an over-ask and silently encodes narrower). +/// 2. Pixel rate ≥ [`SPLIT_FORCE_PIXEL_RATE`] → force the WIDEST split the GPU can deliver +/// ([`max_forced_split_mode`]), not a hard-coded 2 (AUTO never engages below ~2112 px height, +/// so 4K120 must be forced onto the other engines; and a 3-NVENC part left at 2-way wastes a +/// third of its encode silicon). +/// 3. **HEVC** Main10 below that bar → DISABLE: 2-way split measured SLOWER on Ada for Main10 — at +/// 5120×1440@240 forced-2 took 7.6 ms/frame (~131 fps) vs 2.8 ms (~357 fps) single-engine, the +/// "broken animations in HDR" cap. ⚠ This rule used to sit ABOVE the pixel-rate arm and take no +/// codec, so it (a) vetoed 10-bit **4K120** — the very case the pixel-rate arm exists for — and +/// (b) applied an HEVC-on-Ada result to **AV1 10-bit**, which has no such measurement. Both +/// fixed; what remains is a conservative default in the regime where a second engine buys +/// nothing anyway. +/// ⚠⚠ **UNVALIDATED CONSEQUENCE:** 5120×1440@240 Main10 (1.77 Gpix/s) now clears the pixel-rate +/// bar and WILL be forced to split — i.e. the exact configuration that measurement came from +/// flips behaviour. That is deliberate (the datapoint is one sample, at low bits/frame, and the +/// bits/frame hypothesis predicts it should not generalise) but it is **the first thing to +/// re-measure on Ada**; `PUNKTFUNK_SPLIT_ENCODE=0` is the escape if it regresses. +/// 4. Else AUTO — ⚠ whose behaviour is **conditional on sub-frame**, measured on `.21` at 4K: +/// - sub-frame **ON** (the fleet default): AUTO **does not split** — 5023/5157 µs against +/// DISABLE's 4979/5000. Split and sub-frame are mutually unsupported for HEVC, so the driver +/// resolves AUTO to no-split and this arm silently means DISABLE. +/// - sub-frame **OFF**: AUTO **does split** — 2401/2352 µs against TWO_FORCED's 2319/2378. +/// +/// So AUTO is NOT dead in general and must not be retired: doing so would lose a real split on +/// every sub-frame-off session. It is dead only in the sub-frame-on combination, which +/// [`resolve_split_subframe`] logs rather than silently accepting. +/// +/// The caller still owns the rejection fallback (retry split-disabled) — a codec/config that +/// rejects the chosen mode downgrades at open, not here. +/// +/// `engines` is the GPU's `NV_ENC_CAPS_NUM_ENCODER_ENGINES`; pass `0` when it could not be probed +/// (treated as "unknown", which keeps the pre-probe behaviour of assuming a second engine exists +/// and letting the open-time rejection fallback sort it out). +// Split-policy gate — see the constants above. +#[cfg(any(target_os = "linux", all(target_os = "windows", feature = "nvenc")))] +pub(crate) fn resolve_split_mode( + codec: Codec, + bit_depth: u8, + pixel_rate: u64, + engines: u32, +) -> u32 { + let hw_max = max_forced_split_mode(engines); + let mode = match std::env::var("PUNKTFUNK_SPLIT_ENCODE").ok().as_deref() { + Some("0") | Some("disable") => SPLIT_DISABLE, + Some("1") | Some("auto") => SPLIT_AUTO_FORCED, + Some("3") => clamp_to_engines(SPLIT_THREE_FORCED, hw_max, engines), + Some("2") => clamp_to_engines(SPLIT_TWO_FORCED, hw_max, engines), + // Use every engine the card has, not a hard-coded two: on a 3-NVENC part (GB202, AD102 + // workstation) forcing 2 leaves a third of the silicon idle. + // + // ⚠ This arm now comes FIRST, ahead of the 10-bit rule. That reordering is the D1 fix: a + // 10-bit 4K120 session (995.3 Mpix/s) used to be vetoed by the depth rule before ever + // reaching the pixel-rate arm written for exactly it. + _ if pixel_rate >= SPLIT_FORCE_PIXEL_RATE => hw_max, + // Below that bar, HEVC Main10 keeps the conservative single-engine default. The one Ada + // measurement we have says split can be *slower* for Main10, and nothing under this bar + // needs a second engine anyway — so the cost of being wrong here is ~nil, unlike above it. + // + // ⚠ Now codec-scoped (the D2 fix): the measurement behind this was HEVC Main10 on Ada, and + // it used to veto **AV1 10-bit** too, which has neither the sub-frame conflict nor any + // measurement against it. + _ if codec == Codec::H265 && bit_depth >= 10 => SPLIT_DISABLE, + _ => SPLIT_AUTO, + }; + tracing::debug!( + split_mode = mode, + ?codec, + bit_depth, + pixel_rate, + engines, + "NVENC split-encode mode selected" + ); + mode +} + +/// The strongest split mode this GPU's engine count can actually deliver. +/// +/// ⚠ **The driver will NOT tell you when you over-ask.** Measured on `.21` (RTX 5070 Ti, 2 NVENC, +/// driver 610.57.04, 4K HEVC): requesting `THREE_FORCED` was **HONOURED** — session opened in mode +/// 3 — and ran at **2303 µs/frame, identical to `TWO_FORCED`'s 2308**. No rejection, no warning, +/// no third engine; just a log line claiming 3-way over a 2-way encode. So the rejection fallback +/// cannot be relied on to find the ceiling and the clamp has to happen here. +/// +/// `NV_ENC_SPLIT_ENCODE_MODE` can only *name* counts up to three (SDK 0.4.0 / NVENCAPI 12.1; +/// values 4..14 are unallocated, so a future API may extend it). Above that we fall back to +/// `AUTO_FORCED` = "split, driver picks how many", which measurably does force a split (2.01× vs +/// disabled on the same box) and is the only way to express "use everything you have". +// Split-policy gate — see the constants above. +#[cfg(any(target_os = "linux", all(target_os = "windows", feature = "nvenc")))] +pub(crate) fn max_forced_split_mode(engines: u32) -> u32 { + match engines { + // Unknown (cap unreadable / not probed): keep the historical assumption of a second + // engine and let the open-time rejection fallback correct it. + 0 => SPLIT_TWO_FORCED, + 1 => SPLIT_DISABLE, + 2 => SPLIT_TWO_FORCED, + 3 => SPLIT_THREE_FORCED, + // More engines than the enum can name — let the driver use them all. + _ => SPLIT_AUTO_FORCED, + } +} + +/// The N of an N-way FORCED split, or `None` for the modes that do not name a width +/// (`DISABLE`, plain `AUTO`, and `AUTO_FORCED` — the last forces a split but lets the driver +/// choose how wide). +/// +/// For callers that can only express "split this many ways" and have no vocabulary for our other +/// modes — the libav path, whose `split_encode_mode` AVOption is libavcodec's own enum, not the +/// NVENC one (our `DISABLE` is `15`, which would be meaningless there). +// Linux-only: its sole caller is the libav NVENC path (`enc/linux/mod.rs`). `codec.rs` compiles +// everywhere, so without this it is dead code on Windows — the same item-level `dead_code` +// trap this crate has now hit three times (see `subframe_env_forced`, and the arbiter items in +// `nvenc_core`). Caught by the `.133` check, never by reasoning about it. +#[cfg(target_os = "linux")] +pub(crate) fn forced_split_width(mode: u32) -> Option { + match mode { + m if m == SPLIT_TWO_FORCED => Some(2), + m if m == SPLIT_THREE_FORCED => Some(3), + _ => None, + } +} + +/// Hold an operator's `PUNKTFUNK_SPLIT_ENCODE=2|3` to what the hardware can deliver, loudly. +/// Without this the knob silently lies (see [`max_forced_split_mode`]); an override that asks for +/// more engines than exist is a mistake worth surfacing, not honouring. +// Split-policy gate — see the constants above. +#[cfg(any(target_os = "linux", all(target_os = "windows", feature = "nvenc")))] +pub(crate) fn clamp_to_engines(requested: u32, hw_max: u32, engines: u32) -> u32 { + // Only the named N-way modes are ordered; `hw_max` may be AUTO_FORCED (1) on a >3-engine part, + // which is not "less than" TWO_FORCED and must not clamp a legitimate request down. + let named = |m: u32| (2..=3).contains(&m); + if engines != 0 && named(requested) && named(hw_max) && requested > hw_max { + tracing::warn!( + requested, + engines, + using = hw_max, + "PUNKTFUNK_SPLIT_ENCODE asks for more NVENC engines than this GPU has — clamping. \ + (The driver would ACCEPT the over-ask and silently encode with fewer, so the log \ + would otherwise claim a split width that never happened.)" + ); + return hw_max; + } + requested +} + /// `PUNKTFUNK_VBV_FRAMES` — HRD/VBV size in frame intervals (default 1.0, the strict low-latency /// shape every backend ships: each frame must fit its rate share, keeping frame sizes uniform for /// the pacer). The AMF/VAAPI/QSV paths parse the same variable locally; this helper brings the diff --git a/crates/pf-encode/src/enc/linux/mod.rs b/crates/pf-encode/src/enc/linux/mod.rs index af9e5b65..ab783e9d 100644 --- a/crates/pf-encode/src/enc/linux/mod.rs +++ b/crates/pf-encode/src/enc/linux/mod.rs @@ -476,13 +476,22 @@ impl NvencEncoder { opts.set("profile", "main10"); } - // Split-frame encode across both NVENC engines (GB203 has 2) when the pixel rate exceeds - // a single engine's HEVC capacity; e.g. 5120x1440@240 = 1.77 Gpix/s needs it, @120 - // (0.88 Gpix/s) does not. HEVC/AV1 only (not H.264). AUTO won't engage below ~2112px - // height, so we force `2`; below the threshold we leave it AUTO (split costs ~2% BD-rate). - // Threshold shared with the direct-SDK selector ([`super::SPLIT_FORCE_PIXEL_RATE`] — set - // so 4K120 = 995.3 Mpix/s forces, which `> 1e9` famously missed by 0.47%). Output is - // standard HEVC — transparent to the client. Override with PUNKTFUNK_SPLIT_ENCODE. + // Split-frame encode across the GPU's NVENC engines. WP4: the policy is no longer + // duplicated here — it comes from the SAME [`resolve_split_mode`] the two direct-SDK + // backends use, so the pixel-rate threshold, the codec scoping and the (dropped) 10-bit + // short circuit cannot drift between the libav path and the rest. This copy had already + // diverged: it hard-coded a 2-way split regardless of engine count and carried no depth + // rule at all. + // + // ⚠ Only the FORCED outcomes are actionable here. libavcodec's `split_encode_mode` + // AVOption is its own vocabulary, and our `DISABLE` is the NVENC enum's `15` — passing + // that through would be meaningless to it (or fail the open). `DISABLE`/`AUTO` therefore + // both mean "leave the option unset", which is exactly today's behaviour: unset = the + // driver's own auto. + // + // ⚠ `engines = 0` = "not probed": the libav path has no caps probe of its own, and + // [`max_forced_split_mode`] maps unknown to 2-way, preserving what this site always did. + // A 3-NVENC part gets the wider split only on the direct-SDK path. let pix_rate = width as u64 * height as u64 * fps as u64; let split = std::env::var("PUNKTFUNK_SPLIT_ENCODE").ok(); match split.as_deref() { @@ -497,14 +506,17 @@ impl NvencEncoder { "PUNKTFUNK_SPLIT_ENCODE ignored — split encoding is not applicable to H.264 \ (nvEncodeAPI.h)" ), - None if matches!(codec, Codec::H265 | Codec::Av1) - && pix_rate >= super::SPLIT_FORCE_PIXEL_RATE => - { - opts.set("split_encode_mode", "2"); - tracing::info!( - pix_rate, - "NVENC: forcing 2-way split encode (high pixel rate)" - ); + None if matches!(codec, Codec::H265 | Codec::Av1) => { + let resolved = super::resolve_split_mode(codec, bit_depth, pix_rate, 0); + if let Some(n) = super::forced_split_width(resolved) { + opts.set("split_encode_mode", &n.to_string()); + tracing::info!( + pix_rate, + bit_depth, + split_encode_mode = n, + "NVENC (libav): forcing split encode (shared selector)" + ); + } } None => {} } diff --git a/crates/pf-encode/src/enc/linux/nvenc_cuda.rs b/crates/pf-encode/src/enc/linux/nvenc_cuda.rs index 56d15b33..f1a80847 100644 --- a/crates/pf-encode/src/enc/linux/nvenc_cuda.rs +++ b/crates/pf-encode/src/enc/linux/nvenc_cuda.rs @@ -67,11 +67,13 @@ #![deny(clippy::undocumented_unsafe_blocks)] use super::nvenc_core::{ - apply_low_latency_config, build_init_params, cached_ceiling, codec_guid, plan_range_recovery, - resolve_slices, resolve_split_mode, resolve_split_subframe, resolve_subframe, store_ceiling, - subframe_env_forced, CeilingKey, LowLatencyConfig, NvStatusExt, RangePlan, + apply_low_latency_config, build_init_params, cached_ceiling, cached_split_verdict, codec_guid, + plan_range_recovery, resolve_slices, resolve_split_subframe, resolve_subframe, store_ceiling, + store_split_verdict, subframe_env_forced, ArbAction, CeilingKey, LowLatencyConfig, NvStatusExt, + RangePlan, SplitArbiter, SplitKey, }; use super::nvenc_status; +use super::{max_forced_split_mode, resolve_split_mode}; use super::{AuChunk, ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps}; use anyhow::{anyhow, bail, Context, Result}; use pf_frame::{CapturedFrame, FramePayload}; @@ -821,6 +823,25 @@ pub struct NvencCudaEncoder { /// Sub-frame chunked poll armed for the live session (§7 LN1 Phase 1): multi-slice + /// sub-frame readback configured AND sync retrieve at init. See [`Encoder::poll_chunk`]. subframe_chunks: bool, + /// `NV_ENC_CAPS_NUM_ENCODER_ENGINES` — how many NVENC engines this GPU has, probed in + /// [`query_caps`]. `0` = not probed / unreadable. The split-encode ceiling: the driver accepts + /// a split wider than the hardware and silently encodes narrower, so this is the only honest + /// source for how wide we may go (see `codec::max_forced_split_mode`). + encoder_engines: u32, + /// Submit stamp for the split arbiter's per-frame cost (sync depth-1 path only). + last_submit_at: Option, + /// Whole-AU paced-send time (µs) the host last reported, via + /// [`Encoder::set_send_spread_us`]. `0` = never reported, which keeps the arbiter out of the + /// sub-frame trade entirely (it cannot price what it cannot see). + send_spread_us: u32, + /// Sub-frame state the session was OPENED able to run — what `resolve_subframe` decided from + /// the caps probe and the env. `subframe_on` moves as the arbiter flips arms; this does not, + /// so a return to a non-forced split can restore sub-frame without re-deriving it (and + /// without ever turning it on for a session that never had it). + subframe_opened_with: bool, + /// The live split-mode experiment, when one is running. `None` = not arbitrating (gated off, + /// already decided this process, or the config is one we refuse to arbitrate). + arbiter: Option, /// In-progress chunked readback of the front in-flight AU. See [`ChunkState`]. chunk: Option, } @@ -909,6 +930,11 @@ impl NvencCudaEncoder { subframe_on: false, subframe_forced: false, subframe_chunks: false, + encoder_engines: 0, + last_submit_at: None, + send_spread_us: 0, + subframe_opened_with: false, + arbiter: None, chunk: None, }) } @@ -1081,6 +1107,10 @@ impl NvencCudaEncoder { // consumed when slice-level readback lands. Not stored — LN1 re-probes when it configures. let subframe = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_SUBFRAME_READBACK); let dyn_slice = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_DYNAMIC_SLICE_MODE); + // How many NVENC engines this GPU has — the split-encode ceiling. Must be probed rather + // than inferred from a rejection: the driver ACCEPTS a split wider than the hardware and + // silently encodes narrower (measured on `.21`, see `max_forced_split_mode`). + let engines = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_NUM_ENCODER_ENGINES); let _ = (api().destroy_encoder)(enc); if wmax > 0 && hmax > 0 && (self.width as i32 > wmax || self.height as i32 > hmax) { @@ -1100,6 +1130,7 @@ impl NvencCudaEncoder { self.rfi_supported = rfi != 0; self.custom_vbv = custom_vbv != 0; self.subframe_cap = subframe != 0; + self.encoder_engines = engines.max(0) as u32; // Phase-3 default-on (nvenc-subframe-slice-output.md): 4 slices + sub-frame readback on // every Linux direct-NVENC session, resolved HERE (before the session opens) so the // config author, the init params and the chunked-poll latch all agree; the caps probe @@ -1334,7 +1365,25 @@ impl NvencCudaEncoder { // 2-way NVENC split-frame encoding (Ada dual-NVENC) — shared selector, see // [`resolve_split_mode`] for the precedence (env override / 10-bit / pixel rate). let pixel_rate = self.width as u64 * self.height as u64 * self.fps.max(1) as u64; - let split_mode: u32 = resolve_split_mode(self.bit_depth, pixel_rate); + let mut split_mode: u32 = + resolve_split_mode(self.codec, self.bit_depth, pixel_rate, self.encoder_engines); + // A verdict this process already measured for this exact config wins over the static + // rule — that is the whole point of arbitrating, and it lets later sessions skip the + // ~1 s experiment. An operator pin still beats both (checked inside `resolve_split_mode`, + // so only consult the cache when the knob is unset). + if std::env::var_os("PUNKTFUNK_SPLIT_ENCODE").is_none() { + if let Some(known) = cached_split_verdict(&self.split_key()) { + if known != split_mode { + tracing::info!( + from = split_mode, + to = known, + "NVENC: using the split mode a previous arbitration measured as \ + fastest for this config" + ); + } + split_mode = known; + } + } // Split × sub-frame arbitration (Phase 8) BEFORE the ladder, the ceiling key and the // chunked-poll latch — all three must see the post-arbitration truth (a drop inside // build_init_params would leave poll_chunk busy-polling its whole budget per AU). @@ -1345,6 +1394,7 @@ impl NvencCudaEncoder { self.subframe_forced, ); self.subframe_on = subframe_on; + self.subframe_opened_with = subframe_on; const CLAMP_TOL_BPS: u64 = 20_000_000; // Ceiling cache (process lifetime, `nvenc_core`): a prior clamp search already found @@ -1639,12 +1689,183 @@ impl NvencCudaEncoder { // INFO+, and "did 4K120 actually split across engines?" was undiagnosable from // a user log without it (Windows only had a debug! at selection time). split_mode = self.split_mode, + // …and how many engines the GPU HAS, so `split_mode` can be read against the + // ceiling it was chosen from. Without it a log showing split_mode=2 is ambiguous + // between "used both engines" and "left a third engine idle", and the driver + // silently honours an over-wide request, so the mode alone cannot be trusted. + engines = self.encoder_engines, + subframe = self.subframe_on, "NVENC CUDA session ready" ); + self.arm_split_arbiter(); Ok(()) } } + /// Decide whether this session may run a live split experiment, and arm it if so. + /// + /// Opt-in (`PUNKTFUNK_NVENC_SPLIT_ARBITRATE=1`) while it earns trust. Every other gate is a + /// correctness condition, not a preference: + /// + /// - **Operator pin wins.** `PUNKTFUNK_SPLIT_ENCODE` set ⇒ never arbitrate; a pinned mode is an + /// instruction, and an A/B that overrides it would make the knob useless for exactly the + /// debugging it exists for. + /// - **Already decided.** A cached verdict for this config was applied at open; re-running the + /// experiment every session would pay its cost forever. + /// - **Sync depth-1 only** (`async_rt.is_none()`), the same gate chunked poll uses: the + /// per-frame cost is measured as submit → AU, which is only the encode on this path. Under + /// pipelined retrieve that span includes queue depth and the comparison would be noise. + /// - **Needs a second engine**, and split must be applicable at all (never H.264). + /// - ⚠ **No sub-frame trade.** For HEVC, forcing split gives up sub-frame readback, which costs + /// send/encode overlap the ENCODER CANNOT SEE — it measures encode time only, so it would + /// reliably prefer split and silently make end-to-end latency worse. So we arbitrate only + /// where nothing is traded: sub-frame already off, or AV1 (where both features are legal). + /// Pricing that trade needs the host's send cost and is the next work package. + fn arm_split_arbiter(&mut self) { + if !matches!( + std::env::var("PUNKTFUNK_NVENC_SPLIT_ARBITRATE").as_deref(), + Ok("1") + ) { + return; + } + if std::env::var_os("PUNKTFUNK_SPLIT_ENCODE").is_some() + || cached_split_verdict(&self.split_key()).is_some() + || self.async_rt.is_some() + || self.encoder_engines < 2 + || self.codec == Codec::H264 + { + return; + } + // Losing sub-frame costs the send/encode overlap: without it the AU's last byte waits for + // the WHOLE send instead of just the final slice, so the challenger owes roughly + // `spread × (slices−1)/slices`. Priced here because only the encoder knows `slices`; the + // host reports the raw spread. + let handicap_us = if self.subframe_on && self.codec != Codec::Av1 { + if self.send_spread_us == 0 || self.slices < 2 { + tracing::debug!( + "NVENC split arbitration skipped: engaging split would cost sub-frame readback \ + and no send-spread has been reported, so the trade cannot be priced — an \ + encode-only comparison would take the arm that looks fastest and lose \ + end-to-end" + ); + return; + } + let slices = self.slices as u64; + self.send_spread_us as u64 * (slices - 1) / slices + } else { + 0 + }; + // Pick the challenger that tests the question worth asking: "are we leaving engines idle?" + // So anything that is not already the widest forced split is challenged BY the widest, and + // only a session already there is challenged by single-engine ("is splitting even helping + // here?"). + // + // ⚠ Not "whatever we are not": with the fallthrough `AUTO` incumbent that a 4K60 session + // gets, the naive version challenged with DISABLE and spent the experiment re-proving that + // splitting beats not-splitting — while parking the session on the slow arm to do it. + let disable = nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_DISABLE_MODE as u32; + let widest = max_forced_split_mode(self.encoder_engines); + let challenger = if self.split_mode == widest { + disable + } else { + widest + }; + if challenger == self.split_mode { + return; + } + tracing::info!( + incumbent = self.split_mode, + challenger, + handicap_us, + send_spread_us = self.send_spread_us, + "NVENC split arbitration armed — measuring both arms on the live session (no IDR)" + ); + self.arbiter = Some(SplitArbiter::with_handicap( + self.split_mode, + challenger, + handicap_us, + )); + } + + /// The config identity this session's split verdict is cached under. + fn split_key(&self) -> SplitKey { + SplitKey { + gpu: self.cu_ctx as u64, + codec: self.codec, + width: self.width, + height: self.height, + fps: self.fps, + bit_depth: self.bit_depth, + chroma_444: self.chroma_444, + } + } + + /// Move the LIVE session to `mode` without an IDR — spike S1 proved `nvEncReconfigureEncoder` + /// takes a changed `splitEncodeMode` with `resetEncoder=0`, emits no keyframe, and actually + /// applies it. Reuses the bitrate reconfigure path at the CURRENT rate, so only the split mode + /// moves. Returns whether the driver accepted it; on refusal the field is restored so the + /// encoder's idea of its own session stays truthful. + fn apply_split_mode(&mut self, mode: u32) -> bool { + let (prev_mode, prev_sub, prev_chunks) = + (self.split_mode, self.subframe_on, self.subframe_chunks); + // Sub-frame rides along: HEVC cannot hold both, so a forced split must drop it and a + // return to non-forced may take it back (only up to what the session was opened able to + // do — `subframe_cap`/`resolve_subframe` decided that once, at open). + let (mode, subframe) = resolve_split_subframe( + self.codec, + mode, + self.subframe_opened_with, + self.subframe_forced, + ); + self.split_mode = mode; + self.subframe_on = subframe; + // ⚠ The latch `reconfigure_bitrate` does NOT recompute (spike S1c): leave it stale and + // `supports_chunked_poll` keeps saying yes while `numSlices` never advances, so + // `poll_chunk` busy-polls its entire budget every AU. + self.subframe_chunks = self.slices >= 2 && subframe && self.async_rt.is_none(); + if self.reconfigure_bitrate(self.bitrate_bps) { + true + } else { + tracing::warn!( + from = prev_mode, + to = mode, + "NVENC split arbitration: driver refused the in-place split change — staying put" + ); + self.split_mode = prev_mode; + self.subframe_on = prev_sub; + self.subframe_chunks = prev_chunks; + false + } + } + + /// Feed one frame's encode cost to the split arbiter and act on its verdict. + fn feed_split_arbiter(&mut self, encode_us: u64) { + let Some(arb) = self.arbiter.as_mut() else { + return; + }; + let action = arb.on_frame(encode_us); + let done = arb.is_done(); + match action { + Some(ArbAction::SwitchTo(mode)) => { + if !self.apply_split_mode(mode) { + // The experiment cannot proceed if the session will not move — abandon it + // rather than compare two measurements of the same arm. + self.arbiter = None; + return; + } + } + Some(ArbAction::Settled(mode)) => { + store_split_verdict(self.split_key(), mode); + } + None => {} + } + if done { + // A "switch back to the incumbent" verdict settles on the mode now live. + store_split_verdict(self.split_key(), self.split_mode); + self.arbiter = None; + } + } + /// Copy the captured `DeviceBuffer` into the ring slot's registered input surface (device→device /// on the shared context). `sync` blocks until the copy completes (the pre-existing behavior); /// `!sync` enqueues on the encode thread's copy stream and leaves ordering to the session's @@ -2019,6 +2240,10 @@ impl Encoder for NvencCudaEncoder { // never emits an IDR on its own, so this matches the eventual pictureType. is_idr, )); + // Stamp for the split arbiter's per-frame cost. Deliberately a single field rather + // than a sixth `pending` element: the arbiter only runs on the sync depth-1 path + // (`async_rt.is_none()`), where at most one encode is outstanding. + self.last_submit_at = Some(std::time::Instant::now()); } if sample { tracing::info!( @@ -2199,6 +2424,16 @@ impl Encoder for NvencCudaEncoder { if !map.is_null() { let _ = (api().unmap_input_resource)(self.encoder, map); } + // One frame's encode cost, submit → AU complete. Only meaningful on this sync, + // depth-1 path (the arbiter is gated to it), where `lock_bitstream` above blocked + // until the ASIC finished, so the span is the encode rather than a queue wait. + let encode_us = self + .last_submit_at + .take() + .map(|t| t.elapsed().as_micros() as u64); + if let Some(us) = encode_us { + self.feed_split_arbiter(us); + } Ok(Some(EncodedFrame { data, pts_ns, @@ -2363,6 +2598,17 @@ impl Encoder for NvencCudaEncoder { "NVENC chunked poll: picture type diverged from the submit-time prediction" ); } + // The AU is complete here too — the chunked path is how a sub-frame session finishes, + // so the arbiter has to be fed from BOTH completion points or it would never see a + // frame on the incumbent arm of an HEVC sub-frame experiment (that arm is chunked; + // only the challenger, with sub-frame dropped, comes through `poll`). + let encode_us = self + .last_submit_at + .take() + .map(|t| t.elapsed().as_micros() as u64); + if let Some(us) = encode_us { + self.feed_split_arbiter(us); + } Ok(Some(AuChunk { data, pts_ns, @@ -2446,6 +2692,10 @@ impl Encoder for NvencCudaEncoder { } } + fn set_send_spread_us(&mut self, us: u32) { + self.send_spread_us = us; + } + fn applied_bitrate_bps(&self) -> Option { // `bitrate_bps` is the post-clamp truth: the open path's ceiling search and the // reconfigure path's cache clamp both write what the session ACTUALLY targets. @@ -2498,6 +2748,66 @@ mod tests { assert_eq!(slot_fmt_of(F::NV_ENC_BUFFER_FORMAT_ARGB), SlotFormat::Argb); } + /// The `encoder_engines` field `query_caps` latched — read through a helper so the intent + /// ("what the resolver will actually see") is explicit at the call site. + fn self_engines(enc: &NvencCudaEncoder) -> u32 { + enc.encoder_engines + } + + /// An NV12 frame filled with **real high-entropy content**, not the zeroed VRAM every other + /// helper here hands the encoder. + /// + /// This matters more than it looks. Under CBR the rate controller spends its quota only if + /// there is something to code; against uninitialised (driver-zeroed) buffers it emits ~300 B/AU + /// where the configured rate wants ~833 KB, so every timing taken that way measures the + /// PIXEL-proportional cost and is blind to the bits/frame regime — the regime the 4K60 HDR + /// field report actually came from. A cheap xorshift per pixel plus a per-frame seed gives both + /// spatial detail (so intra costs real bits) and inter-frame change (so P-frames cannot + /// skip-code), which is what drives the entropy coder. + /// `block` sets the spatial detail: 1 = per-pixel noise (incompressible — rate control + /// OVERSHOOTS any low target), larger = blockier and cheaper to code. Sweeping it is how the + /// bench reaches the LOW bits/frame end at all; pure noise cannot get there. + fn noise_nv12_frame(w: u32, h: u32, i: u32, block: usize) -> CapturedFrame { + let buf = DeviceBuffer::alloc_nv12(w, h).expect("alloc NV12 device buffer"); + let (uv_ptr, uv_pitch) = buf.uv.expect("NV12 buffer has a UV plane"); + let mut st = 0x2545_F491_4F6C_DD1Du64 ^ ((i as u64 + 1) << 32); + let mut next = move || { + st ^= st << 13; + st ^= st >> 7; + st ^= st << 17; + st + }; + let b = block.max(1); + let mut plane = |pw: usize, ph: usize| -> Vec { + let bw = pw.div_ceil(b); + let cells: Vec = (0..(bw * ph.div_ceil(b))) + .map(|_| (next() >> 24) as u8) + .collect(); + let mut out = Vec::with_capacity(pw * ph); + for y in 0..ph { + let row = y / b * bw; + for x in 0..pw { + out.push(cells[row + x / b]); + } + } + out + }; + let y = plane(w as usize, h as usize); + let uv = plane(w as usize, h as usize / 2); + pf_zerocopy::cuda::write_plane_from_host(buf.ptr, buf.pitch, &y, w as usize, h as usize) + .expect("upload Y plane"); + pf_zerocopy::cuda::write_plane_from_host(uv_ptr, uv_pitch, &uv, w as usize, h as usize / 2) + .expect("upload UV plane"); + CapturedFrame { + width: w, + height: h, + pts_ns: i as u64 * 16_666_667, + format: PixelFormat::Nv12, + payload: FramePayload::Cuda(buf), + cursor: None, + } + } + fn nv12_frame(w: u32, h: u32, i: u32) -> CapturedFrame { // Content is uninitialized device memory — NVENC encodes it fine; this smoke test asserts the // session/registration/encode/RFI machinery, not picture fidelity (that's the on-glass A/B). @@ -2911,6 +3221,982 @@ mod tests { println!("nvenc_cuda reconfigure smoke: 20→60→10 Mbps in place, zero IDRs"); } + /// ON-HARDWARE — **spike S1** (`design/nvenc-split-encode-engagement-implementation-plan.md`): + /// can `splitEncodeMode` change via `nvEncReconfigureEncoder` with `resetEncoder=0`, WITHOUT + /// emitting an IDR? + /// + /// This is the gate on the whole split-engagement program. `splitEncodeMode` lives in + /// `NV_ENC_INITIALIZE_PARAMS`, and our own invariant says a reconfigure "must present the SAME + /// init params as the open" (`windows/nvenc.rs:620`) — but that is OUR rule, never tested + /// against the driver. A forced mid-stream IDR is not acceptable (user), so: + /// - **driver rejects the change** → the constraint is real; the split decision is + /// once-per-session and must be predicted at open. + /// - **accepts it AND the next AU is not a keyframe** → mid-stream adaptation is free, and the + /// engagement rule can simply be re-resolved whenever ABR moves. + /// - **accepts it but emits an IDR anyway** → same as a rejection for our purposes. This is the + /// case a naive "did it return Ok?" check would get wrong, which is why the keyframe count + /// below is the real assertion. + /// + /// Sub-frame is pinned OFF for the whole test: HEVC forced-split and sub-frame readback are + /// mutually unsupported (`resolve_split_subframe`), so leaving it on would have the driver + /// reject the reconfigure for the WRONG reason and read as a false negative. + /// + /// Reports rather than asserts the verdict — S1 is a measurement, and BOTH outcomes are + /// legitimate findings. It only asserts the things that would invalidate the measurement + /// itself (session came up, engines ≥ 2, the arms actually differ). Run ALONE (it sets env): + /// cargo test -p pf-encode --features nvenc -- --ignored --test-threads=1 \ + /// nvenc_cuda_split_reconfigure_in_place --nocapture + #[test] + #[ignore = "requires an NVIDIA GPU + driver — run manually on the RTX box (.21)"] + fn nvenc_cuda_split_reconfigure_in_place() { + use nv::NV_ENC_SPLIT_ENCODE_MODE as M; + const W: u32 = 1920; + const H: u32 = 1080; + const BPS: u64 = 40_000_000; + let disable = M::NV_ENC_SPLIT_DISABLE_MODE as u32; + let two = M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32; + + // Isolate the split variable: sub-frame off, and open explicitly split-DISABLED so the + // switch below is a real change rather than a no-op. + std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0"); + std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", "0"); + + pf_zerocopy::cuda::make_current().expect("shared CUDA context current"); + let mut enc = NvencCudaEncoder::open( + Codec::H265, + PixelFormat::Nv12, + W, + H, + 60, + BPS, + true, + 8, + ChromaFormat::Yuv420, + false, + 4, + ) + .expect("open NVENC CUDA session"); + + let submit_and_poll = |enc: &mut NvencCudaEncoder, range: std::ops::Range| { + let (mut aus, mut keyframes) = (0usize, 0usize); + for i in range { + let frame = nv12_frame(W, H, i); + enc.submit_indexed(&frame, i).expect("submit"); + while let Some(au) = enc.poll().expect("poll") { + aus += 1; + keyframes += au.keyframe as usize; + } + } + (aus, keyframes) + }; + + // Frames first: the session is lazily created on the first submit, and + // `reconfigure_bitrate` short-circuits to `true` while `!inited` (no session to reconfigure + // yet), which would make the whole spike vacuous. + let (aus, kfs) = submit_and_poll(&mut enc, 0..4); + assert!(aus > 0, "no AUs before the reconfigure"); + assert_eq!(kfs, 1, "exactly the opening IDR before the reconfigure"); + assert!( + enc.inited, + "session must be live for the spike to mean anything" + ); + assert_eq!( + enc.split_mode, disable, + "the spike needs to OPEN split-disabled so the switch is a real change" + ); + + // Engine count (WP1.1's probe, borrowed): forced-2 on a 1-NVENC GPU would be rejected for a + // reason that has nothing to do with reconfigure, so the verdict is only interpretable + // when the card actually has a second engine. + // SAFETY: `enc.encoder` is the live session (`inited` asserted above); `get_cap` only reads + // a cap through it and returns 0 on any driver error. + let engines = unsafe { + enc.get_cap( + enc.encoder, + nv::NV_ENC_CAPS::NV_ENC_CAPS_NUM_ENCODER_ENGINES, + ) + }; + println!( + "S1: NV_ENC_CAPS_NUM_ENCODER_ENGINES = {engines} (query_caps latched \ + encoder_engines={})", + self_engines(&enc) + ); + // The cap is only useful if `query_caps` actually stored it — that latched field is what + // `resolve_split_mode` reads to pick the split width, so a silent 0 there would quietly + // fall back to "assume two engines" on every GPU. + assert_eq!( + self_engines(&enc), + engines.max(0) as u32, + "query_caps must latch NUM_ENCODER_ENGINES — resolve_split_mode reads that field, \ + not the live cap" + ); + assert!( + engines >= 2, + "this GPU reports {engines} NVENC engine(s) — S1 is not interpretable here, run it on \ + a 2-engine card" + ); + + // THE SPIKE: change ONLY splitEncodeMode (same bitrate, same everything else) and ask the + // driver to take it in place. + enc.split_mode = two; + let accepted = enc.reconfigure_bitrate(BPS); + println!("S1: reconfigure DISABLE→TWO_FORCED accepted = {accepted}"); + + let verdict = if !accepted { + // Restore the field so the encoder's idea of its own session stays truthful for the + // rest of the test (the live session is still split-disabled). + enc.split_mode = disable; + "FAIL — driver REJECTED the in-place splitEncodeMode change" + } else { + let (aus, kfs) = submit_and_poll(&mut enc, 4..8); + assert!(aus > 0, "no AUs after the accepted reconfigure"); + if kfs == 0 { + "PASS — accepted with NO IDR: mid-stream split adaptation is free" + } else { + "FAIL — accepted but forced an IDR (silently), which is the same as a rejection" + } + }; + println!("S1 VERDICT: {verdict}"); + + // The reverse direction only means something if the forward one worked. + if accepted { + enc.split_mode = disable; + let back = enc.reconfigure_bitrate(BPS); + let kfs = if back { + submit_and_poll(&mut enc, 8..12).1 + } else { + usize::MAX + }; + println!("S1: reverse TWO_FORCED→DISABLE accepted = {back}, keyframes after = {kfs}"); + } + + enc.flush().ok(); + std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE"); + std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME"); + } + + /// ON-HARDWARE — **spike S1b**, the other half of S1: an in-place `splitEncodeMode` change that + /// the driver ACCEPTS without an IDR is worthless if the driver then quietly ignores it, and + /// "accepted, no IDR" looks identical in both cases. So measure whether it took effect. + /// + /// Three legs at 4K (where split has something to bite on), same bitrate throughout: + /// A. fresh session, split DISABLED + /// B. fresh session, split TWO_FORCED + /// C. session opened DISABLED, then reconfigured in place to TWO_FORCED + /// If C ≈ B and both differ from A, the reconfigure is real. If C ≈ A, the driver accepted the + /// parameter and dropped it on the floor. + /// + /// ⚠ **Reads out bytes/AU as well as timing, and that column is load-bearing**: these frames + /// are uninitialised device memory, so under CBR rate control can run out of things to code + /// and every leg collapses to the same trivially-cheap encode — which would make the A/B/C + /// comparison meaningless rather than negative. Tiny or identical byte counts ⇒ the run says + /// nothing, and the real answer needs the content path WP0 route (b) uses. Run ALONE: + /// cargo test -p pf-encode --features nvenc -- --ignored --test-threads=1 \ + /// nvenc_cuda_split_reconfigure_takes_effect --nocapture + #[test] + #[ignore = "requires an NVIDIA GPU + driver — run manually on the RTX box (.21)"] + fn nvenc_cuda_split_reconfigure_takes_effect() { + use nv::NV_ENC_SPLIT_ENCODE_MODE as M; + use std::time::Instant; + const W: u32 = 3840; + const H: u32 = 2160; + const BPS: u64 = 400_000_000; + const WARMUP: u32 = 8; + const MEASURED: u32 = 24; + /// Frames to discard AFTER an in-place switch before measuring. Split-encode does not + /// reach steady state on the first frame — even a FRESH `TWO_FORCED` session shows it + /// (early-half 3280 µs vs late-half 1996 in one run) — and without this the switched leg + /// lands midway between the two arms and the verdict flips run to run. Measured: at 16 + /// the switched leg reaches the fresh-split steady state; at 0 it did so only sometimes. + const SETTLE: u32 = 16; + let two = M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32; + + std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0"); + + // Separate buffers rotated per frame, so identical content can't let the encoder + // skip-code everything and erase the difference we are trying to measure. + // ⚠ MEASURED 2026-08-06: this does NOT work — the driver hands back zeroed VRAM, so all + // four are identical anyway and the legs come out at ~427 B/AU against an 833 KB CBR + // quota. What survives is the PIXEL-proportional half of the cost (motion estimation over + // 8.29 Mpix); the bits/frame half is untested by this harness. Read the printout's + // INCONCLUSIVE-on-content line before drawing any bitrate conclusion from it. + pf_zerocopy::cuda::make_current().expect("shared CUDA context current"); + let frames: Vec = (0..4).map(|i| nv12_frame(W, H, i)).collect(); + + // Returns (early-half p50 µs, late-half p50 µs, median bytes/AU). + let run_leg = |open_split: &str, switch_to: Option| -> (u128, u128, usize) { + std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", open_split); + let mut enc = NvencCudaEncoder::open( + Codec::H265, + PixelFormat::Nv12, + W, + H, + 60, + BPS, + true, + 8, + ChromaFormat::Yuv420, + false, + 4, + ) + .expect("open NVENC CUDA session"); + + // Every leg is measured over the SAME number of frames; a switched leg just starts its + // window `SETTLE` frames later, so the arms stay comparable. + let measure_from = if switch_to.is_some() { + WARMUP + SETTLE + } else { + WARMUP + }; + let (mut times, mut sizes) = (Vec::new(), Vec::new()); + for i in 0..(measure_from + MEASURED) { + // Flip to the target mode exactly once, after warmup, in place. + if i == WARMUP { + if let Some(target) = switch_to { + enc.split_mode = target; + assert!( + enc.reconfigure_bitrate(BPS), + "in-place split switch must be accepted (S1a proved it is)" + ); + continue; + } + } + let t0 = Instant::now(); + enc.submit_indexed(&frames[(i % 4) as usize], i) + .expect("submit"); + let mut got = 0usize; + while let Some(au) = enc.poll().expect("poll") { + got = au.data.len(); + } + let dt = t0.elapsed().as_micros(); + if i >= measure_from { + times.push(dt); + sizes.push(got); + } + } + enc.flush().ok(); + // Split the window in half. A single median over the whole post-switch run is + // ACTIVELY MISLEADING here: leg C's median landed midway between the two arms and the + // nearest-neighbour verdict flipped run to run. Early-vs-late says whether the switch + // SETTLES — which a median cannot. + let half = times.len() / 2; + let med = |s: &[u128]| { + let mut v = s.to_vec(); + v.sort_unstable(); + v[v.len() / 2] + }; + let (early, late) = (med(×[..half]), med(×[half..])); + sizes.sort_unstable(); + (early, late, sizes[sizes.len() / 2]) + }; + + let (a_early, a_late, a_bytes) = run_leg("0", None); + let (b_early, b_late, b_bytes) = run_leg("2", None); + let (c_early, c_late, c_bytes) = run_leg("0", Some(two)); + let (a_us, b_us, c_us) = (a_late, b_late, c_late); + + println!("S1b @ {W}x{H}@60 HEVC 8-bit, {} Mbps CBR:", BPS / 1_000_000); + println!(" (early = first half of the measured window, late = second half)"); + println!(" A fresh DISABLE : early {a_early:>6} late {a_late:>6} us/frame, {a_bytes:>8} B/AU"); + println!(" B fresh TWO_FORCED : early {b_early:>6} late {b_late:>6} us/frame, {b_bytes:>8} B/AU"); + println!(" C DISABLE→TWO in situ: early {c_early:>6} late {c_late:>6} us/frame, {c_bytes:>8} B/AU"); + if c_early > c_late + c_late / 8 { + println!( + " ⇒ leg C SETTLES ({c_early} → {c_late} us): the in-place switch is not \ + instantaneous, so a whole-window median understates it." + ); + } + + let want_bytes = (BPS / 60 / 8) as usize; + if a_bytes * 4 < want_bytes { + println!( + " ⚠ INCONCLUSIVE on content: {a_bytes} B/AU is far below the {want_bytes} B/AU \ + CBR quota — rate control ran out of things to code, so these legs are not the \ + high-bits/frame regime the field case is in." + ); + } + let (near_b, near_a) = (c_us.abs_diff(b_us), c_us.abs_diff(a_us)); + println!( + " ⇒ C is nearer {} (|C-B|={near_b} vs |C-A|={near_a}) — {}", + if near_b < near_a { "B" } else { "A" }, + if near_b < near_a { + "the in-place split switch TOOK EFFECT" + } else { + "the driver appears to have IGNORED the in-place split change" + } + ); + + std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE"); + std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME"); + let _ = (a_bytes, b_bytes, c_bytes); + } + + /// ON-HARDWARE — **spike S1c**, the leg S1a/S1b deliberately excluded. Both pinned sub-frame + /// OFF to isolate the split variable, but a real HEVC arbitration cannot: split and sub-frame + /// readback are mutually unsupported there (`resolve_split_subframe`), so engaging split means + /// flipping `enableSubFrameWrite` in the same breath — a SECOND init param, and the one the + /// reconfigure path deliberately pins today (`windows/nvenc.rs:624-628`). + /// + /// So: can the PAIR move in place? `(DISABLE, sub-frame on)` → `(TWO_FORCED, sub-frame off)`, + /// `resetEncoder=0`, and back. Accepted? IDR-free? + /// + /// ⚠ Also pins the invariant that makes this safe to build on: `subframe_chunks` is latched + /// ONLY in the init path (line ~1625) and is NOT recomputed by `reconfigure_bitrate`, so a + /// caller flipping sub-frame in place MUST clear it too — otherwise `supports_chunked_poll` + /// keeps reporting true and `poll_chunk` busy-polls its whole budget every AU against a + /// `numSlices` that never advances. That is the exact failure the Phase 8 comment warns about + /// for an in-params drop; here the test performs the correct sequence and asserts the state + /// stays coherent, so WP3 has a worked example to copy. Run ALONE: + /// cargo test -p pf-encode --features nvenc -- --ignored --test-threads=1 \ + /// nvenc_cuda_split_subframe_pair_reconfigure --nocapture + #[test] + #[ignore = "requires an NVIDIA GPU + driver — run manually on the RTX box (.21)"] + fn nvenc_cuda_split_subframe_pair_reconfigure() { + use nv::NV_ENC_SPLIT_ENCODE_MODE as M; + const W: u32 = 1920; + const H: u32 = 1080; + const BPS: u64 = 40_000_000; + let disable = M::NV_ENC_SPLIT_DISABLE_MODE as u32; + let two = M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32; + + // Open split-DISABLED, and leave sub-frame at its Linux default (ON where the GPU + // advertises SUBFRAME_READBACK) — that is the fleet shape the arbitration starts from. + std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", "0"); + std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME"); + + pf_zerocopy::cuda::make_current().expect("shared CUDA context current"); + let mut enc = NvencCudaEncoder::open( + Codec::H265, + PixelFormat::Nv12, + W, + H, + 60, + BPS, + true, + 8, + ChromaFormat::Yuv420, + false, + 4, + ) + .expect("open NVENC CUDA session"); + + let submit_and_poll = |enc: &mut NvencCudaEncoder, range: std::ops::Range| { + let (mut aus, mut keyframes) = (0usize, 0usize); + for i in range { + let frame = nv12_frame(W, H, i); + enc.submit_indexed(&frame, i).expect("submit"); + while let Some(au) = enc.poll().expect("poll") { + aus += 1; + keyframes += au.keyframe as usize; + } + } + (aus, keyframes) + }; + + let (aus, kfs) = submit_and_poll(&mut enc, 0..4); + assert!(aus > 0 && kfs == 1, "opening IDR then steady P-frames"); + println!( + "S1c: opened split={} subframe_on={} subframe_chunks={} chunked_poll={}", + enc.split_mode, + enc.subframe_on, + enc.subframe_chunks, + enc.supports_chunked_poll() + ); + if !enc.subframe_on { + println!( + "S1c SKIPPED: sub-frame is off at open on this GPU/driver, so there is no pair to \ + flip — the arbitration reduces to S1a's plain split switch here." + ); + std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE"); + return; + } + + // THE PAIR FLIP, in the order WP3 must use: clear the chunked-poll latch alongside the + // sub-frame flag, or `poll_chunk` outlives the feature it depends on. + enc.split_mode = two; + enc.subframe_on = false; + enc.subframe_chunks = false; + let accepted = enc.reconfigure_bitrate(BPS); + println!("S1c: (DISABLE,sub-frame on) → (TWO_FORCED,sub-frame off) accepted = {accepted}"); + + if accepted { + let (aus, kfs) = submit_and_poll(&mut enc, 4..8); + assert!(aus > 0, "no AUs after the pair flip"); + assert!( + !enc.supports_chunked_poll(), + "chunked poll must be disarmed once sub-frame is off — a stale latch makes \ + poll_chunk busy-poll its whole budget every AU" + ); + println!( + "S1c VERDICT: {}", + if kfs == 0 { + "PASS — the split×sub-frame PAIR moves in place with NO IDR" + } else { + "FAIL — pair flip forced an IDR" + } + ); + + // …and back, which is what a de-escalation would do. + enc.split_mode = disable; + enc.subframe_on = true; + enc.subframe_chunks = enc.slices >= 2 && enc.async_rt.is_none(); + let back = enc.reconfigure_bitrate(BPS); + let kfs_back = if back { + submit_and_poll(&mut enc, 8..12).1 + } else { + usize::MAX + }; + println!("S1c: reverse pair flip accepted = {back}, keyframes after = {kfs_back}"); + } else { + println!( + "S1c VERDICT: FAIL — driver REJECTED the pair flip. Split can still move alone \ + (S1a), so a WP3 arbitration would have to keep sub-frame fixed for the session \ + and only arbitrate split within that." + ); + enc.split_mode = disable; + enc.subframe_on = true; + } + + enc.flush().ok(); + std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE"); + } + + /// ON-HARDWARE — **the D5 confirm** (design §2 defect D5), the one claim in that list that was + /// only ever *inferred*: plain `AUTO` + default-on sub-frame is believed to resolve to + /// no-split, because HEVC split is unsupported *with* sub-frame — which would make the + /// resolver's `AUTO` fallthrough read as "let the driver decide" while actually meaning "never + /// split", on both platforms. + /// + /// The driver reports no "mode I actually chose", so this settles it the same way S1b settled + /// its question: by timing. At 4K the split/no-split gap is unmissable (~2×), so + /// AUTO+sub-frame ≈ DISABLE ⇒ the driver did NOT split ⇒ D5 CONFIRMED + /// AUTO+sub-frame ≈ TWO_FORCED ⇒ it did ⇒ D5 REFUTED and the `AUTO` arm is fine as-is + /// + /// Content is trivial here for the reason `nvenc_cuda_split_reconfigure_takes_effect` + /// documents (zeroed VRAM), so this compares the PIXEL-proportional cost — which is exactly + /// the term split halves, so the discriminator holds. Run ALONE: + /// cargo test -p pf-encode --features nvenc -- --ignored --test-threads=1 \ + /// nvenc_cuda_auto_split_with_subframe --nocapture + #[test] + #[ignore = "requires an NVIDIA GPU + driver — run manually on the RTX box (.21)"] + fn nvenc_cuda_auto_split_with_subframe() { + use std::time::Instant; + const W: u32 = 3840; + const H: u32 = 2160; + const BPS: u64 = 400_000_000; + const WARMUP: u32 = 8; + const MEASURED: u32 = 24; + + pf_zerocopy::cuda::make_current().expect("shared CUDA context current"); + let frames: Vec = (0..4).map(|i| nv12_frame(W, H, i)).collect(); + + // (split env, sub-frame env) → p50 µs, plus the resolved sub-frame state for the printout. + // `split: None` means UNSET, which is the only way to reach the resolver's plain-`AUTO` + // fallthrough: the env knob cannot express it (`0` is DISABLE, `1` is AUTO_**FORCED**), + // and AUTO_FORCED counts as forced in `resolve_split_subframe`, so passing `1` here would + // silently disarm sub-frame and test a completely different configuration. That mistake + // produced a spurious "D5 REFUTED" on the first run of this test. + let run = |split: Option<&str>, subframe: Option<&str>| -> (u128, bool) { + match split { + Some(v) => std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", v), + None => std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE"), + } + match subframe { + Some(v) => std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", v), + None => std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME"), + } + let mut enc = NvencCudaEncoder::open( + Codec::H265, + PixelFormat::Nv12, + W, + H, + 60, + BPS, + true, + 8, + ChromaFormat::Yuv420, + false, + 4, + ) + .expect("open NVENC CUDA session"); + let mut times = Vec::new(); + for i in 0..(WARMUP + MEASURED) { + let t0 = Instant::now(); + enc.submit_indexed(&frames[(i % 4) as usize], i) + .expect("submit"); + while enc.poll().expect("poll").is_some() {} + if i >= WARMUP { + times.push(t0.elapsed().as_micros()); + } + } + let sub = enc.subframe_on; + enc.flush().ok(); + times.sort_unstable(); + (times[times.len() / 2], sub) + }; + + // THE FLEET CASE: env unset ⇒ 4K60 8-bit is below SPLIT_FORCE_PIXEL_RATE (497.7 vs 950 + // Mpix/s) and not 10-bit, so the resolver falls through to plain AUTO, and sub-frame + // stays at its caps-gated default. This leg must report sub-frame TRUE or it is not + // testing D5. + let (auto_us, auto_sub) = run(None, None); + let (dis_us, dis_sub) = run(Some("0"), None); + let (two_us, two_sub) = run(Some("2"), Some("0")); + // The leg that decides whether the `AUTO` arm can simply be RETIRED: D5 proves AUTO does + // not split while sub-frame is on, but retiring it would also change sub-frame-OFF + // sessions, where AUTO is free to split and might. Measure before removing. + let (auto_nosub_us, auto_nosub_sub) = run(None, Some("0")); + + println!("D5 confirm @ {W}x{H}@60 HEVC 8-bit:"); + println!(" AUTO (unset) + sub-frame({auto_sub}) : {auto_us:>6} us/frame"); + println!(" DISABLE + sub-frame({dis_sub}) : {dis_us:>6} us/frame"); + println!(" TWO_FORCED, no sub-frame({two_sub}): {two_us:>6} us/frame"); + println!(" AUTO (unset), no sub-frame({auto_nosub_sub}): {auto_nosub_us:>6} us/frame"); + println!( + " ⇒ with sub-frame OFF, AUTO is nearer {} — retiring the AUTO arm {}", + if auto_nosub_us.abs_diff(two_us) < auto_nosub_us.abs_diff(dis_us) { + "TWO_FORCED (it DOES split)" + } else { + "DISABLE (it does not split either way)" + }, + if auto_nosub_us.abs_diff(two_us) < auto_nosub_us.abs_diff(dis_us) { + "would LOSE a real split on sub-frame-off sessions" + } else { + "is behaviour-neutral" + } + ); + assert!( + auto_sub, + "the AUTO leg resolved sub-frame OFF — it is not testing D5's fleet shape" + ); + let (near_dis, near_two) = (auto_us.abs_diff(dis_us), auto_us.abs_diff(two_us)); + println!( + " ⇒ AUTO sits nearer {} (|A-D|={near_dis} vs |A-T|={near_two}) — D5 {}", + if near_dis < near_two { + "DISABLE" + } else { + "TWO" + }, + if near_dis < near_two { + "CONFIRMED: AUTO + sub-frame does NOT split; the resolver's AUTO arm is dead" + } else { + "REFUTED: AUTO does engage the second engine even with sub-frame on" + } + ); + + std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE"); + std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME"); + } + + /// ON-HARDWARE — **what is the real split ceiling on this GPU?** Feeds WP1.1: we want to use + /// every engine the card has, not a hard-coded 2. + /// + /// `NV_ENC_SPLIT_ENCODE_MODE` tops out at `THREE_FORCED` in SDK 0.4.0 / NVENCAPI 12.1 (values + /// 4..14 are unallocated, so a future API could add more), and `AUTO_FORCED` means "split, you + /// pick how many" — the only way to name a count we have no enum for. + /// + /// For each candidate this reports what the session ACTUALLY opened with, which is the honest + /// signal: the backend's rejection fallback silently retries split-disabled, so a mode the + /// driver refuses shows up as `split_mode == DISABLE` afterwards rather than as an error. And + /// the timing says whether an ACCEPTED mode did anything — a card that takes `THREE_FORCED` + /// but only has two engines would otherwise look like a win. Run ALONE: + /// cargo test -p pf-encode --features nvenc -- --ignored --test-threads=1 \ + /// nvenc_cuda_split_hardware_max --nocapture + #[test] + #[ignore = "requires an NVIDIA GPU + driver — run manually on the RTX box (.21)"] + fn nvenc_cuda_split_hardware_max() { + use nv::NV_ENC_SPLIT_ENCODE_MODE as M; + use std::time::Instant; + const W: u32 = 3840; + const H: u32 = 2160; + const BPS: u64 = 400_000_000; + const WARMUP: u32 = 8; + const MEASURED: u32 = 24; + + std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0"); + pf_zerocopy::cuda::make_current().expect("shared CUDA context current"); + let frames: Vec = (0..4).map(|i| nv12_frame(W, H, i)).collect(); + + // → (requested mode, mode actually opened, p50 µs, engines the driver reports) + let run = |split: &str| -> (u32, u128, i32) { + std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", split); + let mut enc = NvencCudaEncoder::open( + Codec::H265, + PixelFormat::Nv12, + W, + H, + 60, + BPS, + true, + 8, + ChromaFormat::Yuv420, + false, + 4, + ) + .expect("open NVENC CUDA session"); + let mut times = Vec::new(); + for i in 0..(WARMUP + MEASURED) { + let t0 = Instant::now(); + enc.submit_indexed(&frames[(i % 4) as usize], i) + .expect("submit"); + while enc.poll().expect("poll").is_some() {} + if i >= WARMUP { + times.push(t0.elapsed().as_micros()); + } + } + // SAFETY: the session is live (frames encoded above); `get_cap` only reads a cap and + // returns 0 on any driver error. + let engines = unsafe { + enc.get_cap( + enc.encoder, + nv::NV_ENC_CAPS::NV_ENC_CAPS_NUM_ENCODER_ENGINES, + ) + }; + let opened = enc.split_mode; + enc.flush().ok(); + times.sort_unstable(); + (opened, times[times.len() / 2], engines) + }; + + println!("split ceiling probe @ {W}x{H}@60 HEVC 8-bit:"); + let mut baseline = None; + // The env value is NOT the enum value for DISABLE (`0` selects `NV_ENC_SPLIT_DISABLE_MODE`, + // which is 15), so compare against the enum each arm actually asks for. + for (label, env, want) in [ + ("DISABLE ", "0", M::NV_ENC_SPLIT_DISABLE_MODE as u32), + ("AUTO_FORCED ", "1", M::NV_ENC_SPLIT_AUTO_FORCED_MODE as u32), + ("TWO_FORCED ", "2", M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32), + ( + "THREE_FORCED", + "3", + M::NV_ENC_SPLIT_THREE_FORCED_MODE as u32, + ), + ] { + let (opened, us, engines) = run(env); + let honoured = opened == want; + let vs = match baseline { + None => { + baseline = Some(us); + String::new() + } + Some(b) => format!(" ({:.2}× vs DISABLE)", b as f64 / us as f64), + }; + println!( + " req {label} → opened_mode={opened:<2} {} {us:>6} us/frame{vs} [engines={engines}]", + if honoured { + "HONOURED" + } else { + "FELL BACK" + } + ); + } + println!( + " note: opened_mode 15 = DISABLE (the backend's rejection fallback); a mode that is \ + HONOURED but no faster than DISABLE was accepted and did nothing." + ); + + std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE"); + std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME"); + } + + /// ON-HARDWARE — the live split arbitration end to end (WP3). Opens a 4K session that the + /// static rule leaves single-engine, lets the arbiter run, and asserts it converges to the + /// faster arm **without emitting a single IDR** and records a verdict other sessions can reuse. + /// + /// Sub-frame is pinned off so the arbiter's own no-trade gate lets it arm (see + /// `arm_split_arbiter`); this is the shape the first increment supports. + /// + /// Asserts behaviour, not timing: that it settles, that it lands on the arm the ~2× split + /// advantage implies, and — the load-bearing one — **zero keyframes after the opening IDR**, + /// which is the whole reason this design is allowed to exist. Run ALONE: + /// cargo test -p pf-encode --features nvenc -- --ignored --test-threads=1 \ + /// nvenc_cuda_split_arbitration_converges --nocapture + #[test] + #[ignore = "requires an NVIDIA GPU + driver — run manually on the RTX box (.21)"] + fn nvenc_cuda_split_arbitration_converges() { + const W: u32 = 3840; + const H: u32 = 2160; + let disable = nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_DISABLE_MODE as u32; + + std::env::set_var("PUNKTFUNK_NVENC_SPLIT_ARBITRATE", "1"); + std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0"); + std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE"); + + pf_zerocopy::cuda::make_current().expect("shared CUDA context current"); + let frames: Vec = (0..4).map(|i| nv12_frame(W, H, i)).collect(); + let mut enc = NvencCudaEncoder::open( + Codec::H265, + PixelFormat::Nv12, + W, + H, + 60, + 400_000_000, + true, + 8, + ChromaFormat::Yuv420, + false, + 4, + ) + .expect("open NVENC CUDA session"); + + let mut keyframes = 0usize; + let mut aus = 0usize; + // Enough frames for measure + settle + measure with room to spare. + for i in 0..140u32 { + enc.submit_indexed(&frames[(i % 4) as usize], i) + .expect("submit"); + while let Some(au) = enc.poll().expect("poll") { + aus += 1; + keyframes += au.keyframe as usize; + } + } + let final_mode = enc.split_mode; + let still_arbitrating = enc.arbiter.is_some(); + let verdict = cached_split_verdict(&enc.split_key()); + let enc_engines = enc.encoder_engines; + enc.flush().ok(); + + println!( + "arbitration: {aus} AUs, {keyframes} keyframes, final split_mode={final_mode}, \ + cached verdict={verdict:?}, still running={still_arbitrating}" + ); + assert!(aus > 100, "not enough AUs to complete an arbitration"); + assert!( + !still_arbitrating, + "arbitration did not finish in 140 frames" + ); + assert_eq!( + keyframes, 1, + "THE POINT OF THIS DESIGN: arbitration must cost ZERO extra IDRs — only the session's \ + opening one" + ); + assert_eq!( + verdict, + Some(final_mode), + "the winning arm must be cached so later sessions skip the experiment" + ); + assert_ne!( + final_mode, disable, + "at 4K with two engines a splitting arm is ~2x faster, so single-engine must not win" + ); + // The static rule leaves 4K60 on the fallthrough AUTO (497.7 Mpix/s is under + // SPLIT_FORCE_PIXEL_RATE), so the experiment is AUTO vs the widest forced split — the + // "are we leaving engines idle?" question. Either outcome is legitimate; what must NOT + // happen is landing on single-engine. + println!( + " (incumbent was the static rule's choice; challenger was mode {})", + max_forced_split_mode(enc_engines) + ); + + std::env::remove_var("PUNKTFUNK_NVENC_SPLIT_ARBITRATE"); + std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME"); + // The verdict cache is process-global: leaving this session's result in it would steer + // every later test that opens the same config with the split env unset (the D5 legs do + // exactly that). + super::super::nvenc_core::clear_split_verdicts(); + } + + /// ON-HARDWARE — **THE ADA MAIN10 QUESTION**, the one this whole programme has been deferring. + /// + /// The 10-bit split veto rests on a single datapoint: at 5120×1440@240 Main10 on Ada, forced-2 + /// took 7.6 ms/frame against 2.8 ms single-engine — split was **2.7× SLOWER**. That number + /// vetoed splitting for every HDR session on every GPU, and `resolve_split_mode` has now + /// stopped short-circuiting on it, which means a Main10 session above the pixel-rate bar WILL + /// split. If the datapoint generalises, that is a regression and the veto has to come back + /// (scoped properly this time). + /// + /// So: 4K **Main10** (10-bit, via the packed-RGB10 input path), forced-2 against + /// single-engine, same bitrate, sub-frame pinned off so only the split variable moves. + /// Reports rather than asserts — both outcomes are legitimate findings and the point is the + /// number. Run on the **Ada** box (`.181`) and compare against Blackwell (`.21`): + /// cargo test -p pf-encode --features nvenc -- --ignored --test-threads=1 \ + /// nvenc_cuda_main10_split_ab --nocapture + #[test] + #[ignore = "requires an NVIDIA GPU + driver — run manually (Ada .181 vs Blackwell .21)"] + fn nvenc_cuda_main10_split_ab() { + use std::time::Instant; + const BPS: u64 = 400_000_000; + const WARMUP: u32 = 12; + const MEASURED: u32 = 32; + // Mode is overridable so the SAME test can be pointed at the configuration the veto was + // originally measured on — `PF_AB_MODE=5120x1440x240` reproduces the 2.7×-slower datapoint's + // operating point, which is the one config this change flips behaviour for. + let (w, h, fps) = std::env::var("PF_AB_MODE") + .ok() + .and_then(|s| { + let p: Vec = s.split('x').filter_map(|v| v.parse().ok()).collect(); + (p.len() == 3).then(|| (p[0], p[1], p[2])) + }) + .unwrap_or((3840, 2160, 60)); + + std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0"); + pf_zerocopy::cuda::make_current().expect("shared CUDA context current"); + // 10-bit input: the packed 2:10:10:10 PQ path is how a Main10 session is actually fed here + // (`bit_depth`/`hdr` are DERIVED from the input format, never trusted from the args). + let frames: Vec = (0..4).map(|i| rgb10_frame(w, h, i)).collect(); + + let run = |split: &str| -> (u128, u8, usize) { + std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", split); + let mut enc = NvencCudaEncoder::open( + Codec::H265, + PixelFormat::X2Rgb10, + w, + h, + fps, + BPS, + true, + 10, + ChromaFormat::Yuv420, + false, + 4, + ) + .expect("open NVENC CUDA session"); + let (mut times, mut bytes) = (Vec::new(), Vec::new()); + for i in 0..(WARMUP + MEASURED) { + let t0 = Instant::now(); + enc.submit_indexed(&frames[(i % 4) as usize], i) + .expect("submit"); + let mut got = 0usize; + while let Some(au) = enc.poll().expect("poll") { + got = au.data.len(); + } + if i >= WARMUP { + times.push(t0.elapsed().as_micros()); + bytes.push(got); + } + } + let depth = enc.bit_depth; + let opened = enc.split_mode; + enc.flush().ok(); + times.sort_unstable(); + bytes.sort_unstable(); + println!( + " (opened split_mode={opened}, derived bit_depth={depth}, \ + {} B/AU)", + bytes[bytes.len() / 2] + ); + (times[times.len() / 2], depth, bytes[bytes.len() / 2]) + }; + + println!( + "Main10 split A/B @ {w}x{h}@{fps} HEVC 10-bit, {} Mbps:", + BPS / 1_000_000 + ); + let (single_us, d1, _) = run("0"); + println!(" single-engine : {single_us:>6} us/frame"); + let (split_us, d2, _) = run("2"); + println!(" forced 2-way : {split_us:>6} us/frame"); + assert_eq!(d1, 10, "leg 1 did not derive a 10-bit session"); + assert_eq!(d2, 10, "leg 2 did not derive a 10-bit session"); + let ratio = single_us as f64 / split_us.max(1) as f64; + println!( + " ⇒ split is {ratio:.2}× the single-engine rate — {}", + if ratio > 1.15 { + "split WINS for Main10 here; the 2.7x-slower datapoint does NOT generalise" + } else if ratio < 0.87 { + "split LOSES for Main10 — the veto was right and must come back, scoped" + } else { + "a wash; neither arm is clearly better for Main10 here" + } + ); + + std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE"); + std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME"); + } + + /// ON-HARDWARE — **THE BITS/FRAME CURVE**, the measurement this whole programme has been blind + /// to (WP0's real deliverable). + /// + /// Every other timing here was taken against driver-zeroed buffers, so rate control had + /// nothing to code (~300 B/AU against an 833 KB quota) and only the PIXEL-proportional half of + /// the encode cost was ever exercised. But the 4K60 HDR field report was a *bits/frame* + /// problem — 6.8 Mbit/frame — and the central hypothesis is that split's benefit and the + /// 10-bit veto's origin both live on that axis. [`noise_nv12_frame`] finally puts real entropy + /// in front of the encoder. + /// + /// Sweeps bitrate at a fixed mode, single-engine vs forced-2, and prints **bytes/AU alongside + /// every timing** — without that column a run that silently undershoots its quota looks like a + /// result instead of a non-measurement. Run on both boxes: + /// cargo test -p pf-encode --features nvenc -- --ignored --test-threads=1 \ + /// nvenc_cuda_bits_per_frame_curve --nocapture + #[test] + #[ignore = "requires an NVIDIA GPU + driver — run manually (Ada .181 / Blackwell .21)"] + fn nvenc_cuda_bits_per_frame_curve() { + use std::time::Instant; + const WARMUP: u32 = 10; + const MEASURED: u32 = 24; + let (w, h, fps) = std::env::var("PF_AB_MODE") + .ok() + .and_then(|s| { + let p: Vec = s.split('x').filter_map(|v| v.parse().ok()).collect(); + (p.len() == 3).then(|| (p[0], p[1], p[2])) + }) + .unwrap_or((3840, 2160, 60)); + + std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0"); + pf_zerocopy::cuda::make_current().expect("shared CUDA context current"); + // Sweep CONTENT DETAIL, not nominal bitrate. Pure noise is incompressible, so a low + // bitrate target simply overshoots (measured: 719 KB/AU against a 104 KB quota) and every + // low row lands at the same high bits/frame — the exact blindness this test exists to fix. + // Blockier content codes cheaper, so detail is what actually moves along the axis, and the + // x-axis below is the bits/frame the encoder ACTUALLY produced, never the one requested. + let bps: u64 = 600_000_000; + println!( + "bits/frame curve @ {w}x{h}@{fps} HEVC 8-bit, REAL content, {} Mbps cap:", + bps / 1_000_000 + ); + println!(" detail | ACTUAL bits/frame | single | split-2 | ratio"); + for block in [64usize, 32, 16, 8, 4, 1] { + let frames: Vec = + (0..4).map(|i| noise_nv12_frame(w, h, i, block)).collect(); + let run = |split: &str| -> (u128, usize) { + std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", split); + let mut enc = NvencCudaEncoder::open( + Codec::H265, + PixelFormat::Nv12, + w, + h, + fps, + bps, + true, + 8, + ChromaFormat::Yuv420, + false, + 4, + ) + .expect("open NVENC CUDA session"); + let (mut times, mut bytes) = (Vec::new(), Vec::new()); + for i in 0..(WARMUP + MEASURED) { + let t0 = Instant::now(); + enc.submit_indexed(&frames[(i % 4) as usize], i) + .expect("submit"); + let mut got = 0usize; + while let Some(au) = enc.poll().expect("poll") { + got = au.data.len(); + } + if i >= WARMUP { + times.push(t0.elapsed().as_micros()); + bytes.push(got); + } + } + enc.flush().ok(); + times.sort_unstable(); + bytes.sort_unstable(); + (times[times.len() / 2], bytes[bytes.len() / 2]) + }; + let (s_us, s_bytes) = run("0"); + let (p_us, _) = run("2"); + println!( + " {block:>5}px | {:>10.2} Mbit | {s_us:>6}us | {p_us:>6}us | {:>4.2}×", + s_bytes as f64 * 8.0 / 1e6, + s_us as f64 / p_us.max(1) as f64 + ); + } + + std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE"); + std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME"); + } + /// A pre-session RFI request and nonsense ranges all correctly decline (→ caller forces IDR). /// Needs no GPU session (it short-circuits on the null encoder / range checks), so it runs in the /// normal suite — but `open` gates on the NVENC `.so`, so it skips gracefully where the NVIDIA diff --git a/crates/pf-encode/src/enc/nvenc_core.rs b/crates/pf-encode/src/enc/nvenc_core.rs index b591f754..d78420f1 100644 --- a/crates/pf-encode/src/enc/nvenc_core.rs +++ b/crates/pf-encode/src/enc/nvenc_core.rs @@ -67,8 +67,11 @@ pub(super) fn resolve_slices(codec: Codec, default_slices: u32) -> u32 { /// Resolved sub-frame readback (`enableSubFrameWrite` + `reportSliceOffsets`; sync sessions /// only, see [`build_init_params`]): `PUNKTFUNK_NVENC_SUBFRAME` tri-state — `0` = never (the /// default-on escape), `1` = force (even where the caps probe says unsupported — an operator -/// explicitly testing), unset = the backend's `default_on` (Linux direct-NVENC passes its -/// SUBFRAME_READBACK caps-probe result since Phase 3; Windows passes `false`). +/// explicitly testing), unset = the backend's `default_on` — which is the GPU's +/// `SUBFRAME_READBACK` caps-probe result on **both** backends now (Linux since Phase 3, Windows +/// since the 2026-07-31 `.173` A/B). This comment used to say "Windows passes `false`"; it had +/// been stale since that flip, which mattered because it made the AUTO-plus-sub-frame dead +/// combination look Linux-only when it is fleet-wide. pub(super) fn resolve_subframe(default_on: bool) -> bool { match std::env::var("PUNKTFUNK_NVENC_SUBFRAME").as_deref() { Ok("0") => false, @@ -77,41 +80,6 @@ pub(super) fn resolve_subframe(default_on: bool) -> bool { } } -/// Resolved NVENC split-frame encode mode for a session — ONE selector shared by the Windows and -/// Linux direct-SDK backends (they had drifted into byte-identical duplicates, one of which -/// logged and one didn't). Precedence: -/// 1. `PUNKTFUNK_SPLIT_ENCODE` = `0`/`disable` | `1`/`auto` (AUTO_FORCED) | `2` | `3` — operator -/// override, always wins. -/// 2. 10-bit → DISABLE: 2-way split is measurably SLOWER on Ada for Main10 — at 5120×1440@240 -/// forced-2 took 7.6 ms/frame (~131 fps) vs 2.8 ms (~357 fps) single-engine (the split/merge -/// overhead dominates), and a single engine handles 5K@240 Main10 well under budget. This was -/// the "broken animations in HDR" cap at ~131 fps. -/// 3. Pixel rate ≥ [`super::SPLIT_FORCE_PIXEL_RATE`] → force 2-way (AUTO never engages below -/// ~2112 px height, so 4K120 must be forced onto the second engine). -/// 4. Else AUTO (the ~2% BD-rate split cost isn't worth it at low pixel rates). -/// -/// The caller still owns the rejection fallback (retry split-disabled) — a codec/config that -/// rejects the chosen mode downgrades at open, not here. -pub(super) fn resolve_split_mode(bit_depth: u8, pixel_rate: u64) -> u32 { - use nv::NV_ENC_SPLIT_ENCODE_MODE as M; - let mode = match std::env::var("PUNKTFUNK_SPLIT_ENCODE").ok().as_deref() { - Some("0") | Some("disable") => M::NV_ENC_SPLIT_DISABLE_MODE as u32, - Some("1") | Some("auto") => M::NV_ENC_SPLIT_AUTO_FORCED_MODE as u32, - Some("3") => M::NV_ENC_SPLIT_THREE_FORCED_MODE as u32, - Some("2") => M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32, - _ if bit_depth >= 10 => M::NV_ENC_SPLIT_DISABLE_MODE as u32, - _ if pixel_rate >= super::SPLIT_FORCE_PIXEL_RATE => M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32, - _ => M::NV_ENC_SPLIT_AUTO_MODE as u32, - }; - tracing::debug!( - split_mode = mode, - bit_depth, - pixel_rate, - "NVENC split-encode mode selected" - ); - mode -} - /// Whether the operator EXPLICITLY forced sub-frame readback on (`PUNKTFUNK_NVENC_SUBFRAME=1`) /// — the log-severity input to [`resolve_split_subframe`]: a forced knob being overridden /// deserves a `warn`, a default being tuned an `info`. Callers LATCH this once next to their @@ -177,6 +145,20 @@ pub(super) fn resolve_split_subframe( } return (split_mode, false); } + // The silently-inert combination, made visible. HEVC + plain AUTO + sub-frame: the driver + // cannot split (mutually unsupported) so it resolves AUTO to no-split — MEASURED on `.21` at + // 4K, AUTO+sub-frame 5023/5157 µs vs DISABLE's 4979/5000, while the same AUTO with sub-frame + // OFF splits at 2401/2352 vs TWO_FORCED's 2319/2378. This is the fleet's default shape, so + // "split_mode=AUTO" in a log has meant "no split" for every default session and nothing said + // so. Deliberately NOT rewritten to DISABLE: the mode we pass is what the driver was actually + // given, and the ceiling-cache key must keep describing that. + if codec == Codec::H265 && subframe && split_mode == M::NV_ENC_SPLIT_AUTO_MODE as u32 { + tracing::debug!( + "NVENC: split-encode AUTO with sub-frame readback on — the driver cannot split HEVC \ + in this combination, so this session runs SINGLE-ENGINE (measured). Set \ + PUNKTFUNK_NVENC_SUBFRAME=0 to trade sub-frame for a real split." + ); + } (split_mode, subframe) } @@ -237,6 +219,28 @@ mod split_subframe_tests { ); } + /// ⚠ DO NOT "SIMPLIFY" THE `AUTO` ARM AWAY. Measured on `.21` at 4K, plain `AUTO` is + /// conditional, not dead: + /// sub-frame ON → 5023/5157 µs ≈ DISABLE 4979/5000 (cannot split — mutually unsupported) + /// sub-frame OFF → 2401/2352 µs ≈ TWO_FORCED 2319/2378 (DOES split) + /// An earlier read of the sub-frame-ON measurement alone concluded "AUTO never splits, retire + /// it" — that would have silently cost every sub-frame-off session its second engine. This + /// test pins the arbitration's half of the contract: AUTO must survive both ways. + #[test] + fn auto_survives_the_arbitration_in_both_subframe_states() { + // Sub-frame on: kept as AUTO (inert, but that is the driver's call, and rewriting it to + // DISABLE would lie to the ceiling-cache key about what the session was given). + assert_eq!( + resolve_split_subframe(Codec::H265, AUTO, true, false), + (AUTO, true) + ); + // Sub-frame off: still AUTO, and here it is a REAL split — the arm must not be demoted. + assert_eq!( + resolve_split_subframe(Codec::H265, AUTO, false, false), + (AUTO, false) + ); + } + /// AV1: both features are legal together (per-tile sub-frame; split constrained only by /// output-into-vidmem) — the arbitration must not touch it. #[test] @@ -248,6 +252,165 @@ mod split_subframe_tests { } } +// Split arbitration now runs on BOTH direct-SDK backends, so these are gated to the union of +// the two rather than to Linux. Kept gated at all because `nvenc_core` is also reachable from +// builds where neither backend is compiled, and an ungated item there is the item-level +// dead_code trap this file already carries three scars from (see `subframe_env_forced`). +#[cfg(any(target_os = "linux", windows))] +/// What the split arbiter wants the backend to do next. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ArbAction { + /// Reconfigure the live session to this split mode (in place — S1 proved this is IDR-free). + SwitchTo(u32), + /// Arbitration finished; this mode won and the arbiter will ask for nothing further. + Settled(u32), +} + +#[cfg(any(target_os = "linux", windows))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ArbState { + MeasuringIncumbent, + Settling, + MeasuringChallenger, + Done, +} + +#[cfg(any(target_os = "linux", windows))] +/// Picks the faster of two NVENC split modes **on the live session**, by measuring both. +/// +/// This exists because the alternative — predicting the right mode at open — cannot work: the +/// decision depends on bits/frame, and for an Automatic client the host does not know the +/// steady-state bitrate at open (ABR climbs in place afterwards). Spike S1 showed +/// `nvEncReconfigureEncoder` accepts a changed `splitEncodeMode` with `resetEncoder=0`, emits **no +/// IDR**, and genuinely takes effect — so the encoder can simply try both and keep the winner, +/// with nothing visible on the wire. +/// +/// Deliberately measures rather than models: hard-coded per-architecture constants are exactly how +/// the rule this replaces went wrong (one 5120×1440@240 Ada datapoint generalised into a fleet-wide +/// 10-bit veto). A measurement tracks driver updates for free. +/// +/// ⚠ **`SETTLE_FRAMES` is load-bearing, not padding.** Split-encode does not reach steady state on +/// the first frame — a *fresh* `TWO_FORCED` session measured early-half 3280 µs against late-half +/// 1996 on `.21`. Judging an arm immediately after switching to it reads the transient, and does so +/// **intermittently**, which is the worst failure mode: the verdict would be wrong only sometimes, +/// and then be cached. +pub(super) struct SplitArbiter { + state: ArbState, + incumbent: u32, + challenger: u32, + samples: Vec, + incumbent_us: u64, + settle_left: u32, + /// Latency the challenger COSTS beyond its encode time, added to its measured result before + /// the comparison. Non-zero only when winning the split means giving up sub-frame readback: + /// sub-frame lets the send overlap the encode, so losing it pushes the AU's last byte out by + /// roughly `send_spread × (slices−1)/slices`. Without this term the arbiter compares encode + /// against encode, always prefers split on HEVC, and makes end-to-end latency worse while + /// reporting a win. + challenger_handicap_us: u64, +} + +/// Frames discarded after a switch before the challenger is judged (measured — see the struct doc). +#[cfg(any(target_os = "linux", windows))] +const SETTLE_FRAMES: u32 = 16; +/// Frames measured per arm. Long enough to median out content variation, short enough that the +/// whole arbitration is over in well under a second at 60 fps. +#[cfg(any(target_os = "linux", windows))] +const SAMPLE_FRAMES: usize = 24; +/// The challenger must beat the incumbent by this much to win. Switching is not free (a +/// reconfigure, and for HEVC it costs sub-frame readback), so a coin-flip difference should leave +/// the session where it already is. +#[cfg(any(target_os = "linux", windows))] +const WIN_MARGIN_PCT: u64 = 10; + +#[cfg(any(target_os = "linux", windows))] +impl SplitArbiter { + /// `handicap_us` is what the challenger costs OUTSIDE the encode it is measured on — pass `0` + /// when it gives up nothing. See [`Self::challenger_handicap_us`]. + pub(super) fn with_handicap(incumbent: u32, challenger: u32, handicap_us: u64) -> Self { + Self { + state: ArbState::MeasuringIncumbent, + incumbent, + challenger, + samples: Vec::with_capacity(SAMPLE_FRAMES), + incumbent_us: 0, + settle_left: 0, + challenger_handicap_us: handicap_us, + } + } + + /// Feed one frame's encode time. Returns an action when the arbiter wants the session changed. + pub(super) fn on_frame(&mut self, us: u64) -> Option { + match self.state { + ArbState::Done => None, + ArbState::Settling => { + self.settle_left = self.settle_left.saturating_sub(1); + if self.settle_left == 0 { + self.state = ArbState::MeasuringChallenger; + self.samples.clear(); + } + None + } + ArbState::MeasuringIncumbent => { + self.samples.push(us); + if self.samples.len() < SAMPLE_FRAMES { + return None; + } + self.incumbent_us = median(&mut self.samples); + self.state = ArbState::Settling; + self.settle_left = SETTLE_FRAMES; + Some(ArbAction::SwitchTo(self.challenger)) + } + ArbState::MeasuringChallenger => { + self.samples.push(us); + if self.samples.len() < SAMPLE_FRAMES { + return None; + } + // Compare TOTAL cost, not encode cost: whatever the challenger gives up outside + // the encode (on HEVC, the sub-frame send overlap) is charged to it here. + let challenger_us = median(&mut self.samples) + self.challenger_handicap_us; + self.state = ArbState::Done; + // Strictly better by the margin, or the incumbent keeps the session. Equal-ish is + // deliberately a win for the incumbent: we are already there. + let threshold = self + .incumbent_us + .saturating_sub(self.incumbent_us.saturating_mul(WIN_MARGIN_PCT) / 100); + if challenger_us < threshold { + tracing::info!( + winner = self.challenger, + winner_us = challenger_us, + loser = self.incumbent, + loser_us = self.incumbent_us, + "NVENC split arbitration: challenger wins — keeping it" + ); + Some(ArbAction::Settled(self.challenger)) + } else { + tracing::info!( + winner = self.incumbent, + winner_us = self.incumbent_us, + loser = self.challenger, + loser_us = challenger_us, + "NVENC split arbitration: incumbent held — switching back" + ); + // The session is currently running the challenger, so returning to the + // incumbent is an actual reconfigure, not a no-op. + Some(ArbAction::SwitchTo(self.incumbent)) + } + } + } + } + + pub(super) fn is_done(&self) -> bool { + self.state == ArbState::Done + } +} + +#[cfg(any(target_os = "linux", windows))] +fn median(v: &mut [u64]) -> u64 { + v.sort_unstable(); + v[v.len() / 2] +} + /// One session config's identity for the process-lifetime bitrate-ceiling cache /// ([`cached_ceiling`]/[`store_ceiling`]). Everything the driver's codec-level validation keys /// off: the GPU (different NVENC generations have different level ceilings), dims/fps (the luma @@ -292,9 +455,61 @@ pub(super) fn store_ceiling(key: CeilingKey, bps: u64) { ceilings().lock().unwrap().insert(key, bps); } +#[cfg(any(target_os = "linux", windows))] +/// A config's identity for the split-arbitration verdict cache — [`CeilingKey`] **minus +/// `split_mode`**, because the split mode is the thing being decided. Including it would key each +/// verdict under the arm that produced it and the cache could never answer "which arm should this +/// config use?". +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub(super) struct SplitKey { + pub gpu: u64, + pub codec: Codec, + pub width: u32, + pub height: u32, + pub fps: u32, + pub bit_depth: u8, + pub chroma_444: bool, +} + +#[cfg(any(target_os = "linux", windows))] +fn split_verdicts() -> &'static std::sync::Mutex> { + static V: std::sync::OnceLock>> = + std::sync::OnceLock::new(); + V.get_or_init(Default::default) +} + +#[cfg(any(target_os = "linux", windows))] +/// The split mode a previous arbitration found fastest for `key` this process lifetime. +/// +/// Process-lifetime and advisory, exactly like [`cached_ceiling`]: a session that reads a verdict +/// opens straight into the winning arm and skips the ~1 s exploration. It is NOT persisted — a +/// driver update can change the answer, and a stale verdict on disk would outlive its evidence +/// (persisting it needs the driver version in the key; see the plan's WP3). +pub(super) fn cached_split_verdict(key: &SplitKey) -> Option { + split_verdicts().lock().unwrap().get(key).copied() +} + +#[cfg(any(target_os = "linux", windows))] +/// Record an arbitration result for `key`. +pub(super) fn store_split_verdict(key: SplitKey, mode: u32) { + split_verdicts().lock().unwrap().insert(key, mode); +} + +#[cfg(any(target_os = "linux", windows))] +/// Drop every cached verdict. Test-only: the cache is process-global, so an on-hardware test that +/// runs an arbitration would otherwise leak its verdict into every later test that opens the same +/// config with `PUNKTFUNK_SPLIT_ENCODE` unset — which is exactly the shape the D5 legs use. +// Linux-only: its sole caller is `nvenc_cuda`'s arbitration on-hw test. Ungated it is dead +// code on Windows — the same item-level trap, now four times over. +#[cfg(all(test, target_os = "linux"))] +pub(super) fn clear_split_verdicts() { + split_verdicts().lock().unwrap().clear(); +} + #[cfg(test)] mod tests { use super::*; + use crate::{clamp_to_engines, max_forced_split_mode, resolve_split_mode}; use nv::NV_ENC_SPLIT_ENCODE_MODE as M; // These assume PUNKTFUNK_SPLIT_ENCODE is unset (CI); an operator override deliberately wins. @@ -382,7 +597,7 @@ mod tests { // 4090 because AUTO never engages at 2160 px height. let four_k_120 = 3840u64 * 2160 * 120; assert_eq!( - resolve_split_mode(8, four_k_120), + resolve_split_mode(Codec::H265, 8, four_k_120, 2), M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32 ); } @@ -392,22 +607,141 @@ mod tests { // 884.7 Mpix/s is comfortably single-engine — the threshold move must not drag it in. let qhd_240 = 2560u64 * 1440 * 240; assert_eq!( - resolve_split_mode(8, qhd_240), + resolve_split_mode(Codec::H265, 8, qhd_240, 2), M::NV_ENC_SPLIT_AUTO_MODE as u32 ); } #[test] - fn split_disabled_for_10bit_even_at_high_pixel_rate() { - // The measured Main10 rule: split/merge overhead dominates 10-bit on Ada (7.6 ms forced-2 - // vs 2.8 ms single-engine at 5K240) — 10-bit precedes the pixel-rate arm. - let five_k_240 = 5120u64 * 1440 * 240; + fn split_rules_for_10bit_after_dropping_the_short_circuit() { + let five_k_240 = 5120u64 * 1440 * 240; // 1.77 Gpix/s — over the bar + let four_k_120 = 3840u64 * 2160 * 120; // 995.3 Mpix/s — over the bar + let hd_60 = 1920u64 * 1080 * 60; // 124 Mpix/s — well under + + // ⚠ BEHAVIOUR FLIP, deliberate: the config the Main10 veto was measured on (7.6 ms + // forced-2 vs 2.8 ms single-engine on Ada) now clears the pixel-rate bar and SPLITS. The + // datapoint is one sample at low bits/frame; re-measuring it on Ada is the first on-glass + // item, and PUNKTFUNK_SPLIT_ENCODE=0 is the escape if it regresses. assert_eq!( - resolve_split_mode(10, five_k_240), + resolve_split_mode(Codec::H265, 10, five_k_240, 2), + M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32 + ); + // D1: 10-bit 4K120 used to be vetoed by the depth rule BEFORE reaching the pixel-rate arm + // written for exactly it. It splits now. + assert_eq!( + resolve_split_mode(Codec::H265, 10, four_k_120, 2), + M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32 + ); + // Under the bar, HEVC Main10 keeps the conservative single-engine default — a second + // engine buys nothing there, so being wrong costs ~nil. + assert_eq!( + resolve_split_mode(Codec::H265, 10, hd_60, 2), M::NV_ENC_SPLIT_DISABLE_MODE as u32 ); } + /// D2: the Main10 rule was measured on HEVC and used to be codec-blind, so it vetoed **AV1 + /// 10-bit** — which has neither the sub-frame conflict nor any measurement against it. + #[test] + fn av1_10bit_is_no_longer_vetoed_by_an_hevc_measurement() { + let hd_60 = 1920u64 * 1080 * 60; + let four_k_120 = 3840u64 * 2160 * 120; + assert_eq!( + resolve_split_mode(Codec::Av1, 10, hd_60, 2), + M::NV_ENC_SPLIT_AUTO_MODE as u32, + "AV1 10-bit must follow the ordinary path, not inherit an HEVC veto" + ); + assert_eq!( + resolve_split_mode(Codec::Av1, 10, four_k_120, 2), + M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32 + ); + } + + /// THE ENGINE-COUNT FIX: a high-pixel-rate session must use every engine the GPU has, not a + /// hard-coded two. A 3-NVENC part (GB202 / AD102 workstation) left at 2-way wastes a third of + /// its encode silicon, and the driver never complains because it accepts an over- OR + /// under-wide request without comment. + #[test] + fn split_uses_every_engine_the_gpu_has() { + let four_k_120 = 3840u64 * 2160 * 120; + assert_eq!( + resolve_split_mode(Codec::H265, 8, four_k_120, 3), + M::NV_ENC_SPLIT_THREE_FORCED_MODE as u32, + "a 3-engine GPU must split three ways" + ); + assert_eq!( + resolve_split_mode(Codec::H265, 8, four_k_120, 1), + M::NV_ENC_SPLIT_DISABLE_MODE as u32, + "a 1-engine GPU must not pretend to split — today this costs a wasted session open" + ); + assert_eq!( + resolve_split_mode(Codec::H265, 8, four_k_120, 0), + M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32, + "unprobed engine count keeps the historical assumption; the rejection fallback corrects" + ); + } + + /// `NV_ENC_SPLIT_ENCODE_MODE` cannot NAME more than three (SDK 0.4.0 / NVENCAPI 12.1), so a + /// hypothetical wider part falls back to AUTO_FORCED = "split, driver picks how many" — which + /// is measurably a real split (2.01× vs disabled on `.21`), not a no-op. + #[test] + fn split_beyond_three_engines_delegates_to_the_driver() { + assert_eq!( + max_forced_split_mode(4), + M::NV_ENC_SPLIT_AUTO_FORCED_MODE as u32 + ); + assert_eq!( + max_forced_split_mode(8), + M::NV_ENC_SPLIT_AUTO_FORCED_MODE as u32 + ); + } + + /// An operator over-ask must be clamped, because the DRIVER WON'T: measured on `.21` (2 NVENC), + /// `THREE_FORCED` was honoured and ran identically to `TWO_FORCED` (2303 vs 2308 µs/frame) — + /// a log claiming a 3-way split over a 2-way encode. Clamping keeps the log honest. + #[test] + fn operator_override_is_clamped_to_real_engine_count() { + assert_eq!( + clamp_to_engines( + M::NV_ENC_SPLIT_THREE_FORCED_MODE as u32, + max_forced_split_mode(2), + 2 + ), + M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32, + "asking for 3 on a 2-engine card must clamp to 2" + ); + // Within budget → untouched. + assert_eq!( + clamp_to_engines( + M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32, + max_forced_split_mode(3), + 3 + ), + M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32 + ); + // Unknown engine count must not clamp — we have nothing to clamp against. + assert_eq!( + clamp_to_engines( + M::NV_ENC_SPLIT_THREE_FORCED_MODE as u32, + max_forced_split_mode(0), + 0 + ), + M::NV_ENC_SPLIT_THREE_FORCED_MODE as u32 + ); + // ⚠ The ordering trap: on a >3-engine part `hw_max` is AUTO_FORCED (1), which is NOT + // "narrower than" TWO_FORCED (2) despite comparing smaller. A naive `min` would clamp a + // legitimate 3-way request down to AUTO on the widest hardware we support. + assert_eq!( + clamp_to_engines( + M::NV_ENC_SPLIT_THREE_FORCED_MODE as u32, + max_forced_split_mode(4), + 4 + ), + M::NV_ENC_SPLIT_THREE_FORCED_MODE as u32, + "a 4-engine GPU must honour an explicit 3-way request, not collapse it to AUTO" + ); + } + #[test] fn ceiling_cache_round_trips_and_keys_precisely() { let key = CeilingKey { @@ -851,3 +1185,184 @@ pub(super) unsafe fn apply_low_latency_config(cfg: &mut nv::NV_ENC_CONFIG, c: Lo } } } + +#[cfg(all(test, any(target_os = "linux", windows)))] +mod arbiter_tests { + use super::{ArbAction, SplitArbiter, SETTLE_FRAMES}; + + use nvidia_video_codec_sdk::sys::nvEncodeAPI::NV_ENC_SPLIT_ENCODE_MODE as M; + + /// THE SUB-FRAME TRADE, which is the whole reason `set_send_spread_us` exists. Same encode + /// numbers both times; only the handicap differs. + /// + /// A 4K HEVC session where split halves the encode (5000 → 2400 µs) but costs sub-frame + /// readback. With a cheap send there is headroom and split wins. With an expensive send the + /// lost overlap outweighs the encode saving, and the arbiter must REFUSE the arm that looks + /// twice as fast — which is exactly the mistake an encode-only comparison makes. + #[test] + fn handicap_can_reverse_the_verdict() { + let (inc, chal) = ( + M::NV_ENC_SPLIT_DISABLE_MODE as u32, + M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32, + ); + let run = |handicap: u64| { + let mut arb = SplitArbiter::with_handicap(inc, chal, handicap); + let mut live = inc; + for _ in 0..500 { + if arb.is_done() { + break; + } + let us = if live == inc { 5000 } else { 2400 }; + if let Some(a) = arb.on_frame(us) { + match a { + ArbAction::SwitchTo(m) | ArbAction::Settled(m) => live = m, + } + } + } + live + }; + // Cheap send: the 2600 µs encode saving is real, split wins. + assert_eq!(run(500), chal, "with a cheap send, split should win"); + // Expensive send: 2400 + 3000 = 5400 against 5000 — the "twice as fast" arm is a LOSS + // end to end, and an encode-only comparison would have taken it. + assert_eq!( + run(3000), + inc, + "when losing sub-frame costs more than split saves, the incumbent must hold — this is \ + the regression an encode-only arbiter would ship" + ); + } + + /// Drive an arbiter with a fixed cost per arm and return every action it emitted. + fn drive(incumbent_us: u64, challenger_us: u64) -> (Vec, u32) { + let (inc, chal) = ( + M::NV_ENC_SPLIT_DISABLE_MODE as u32, + M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32, + ); + let mut arb = SplitArbiter::with_handicap(inc, chal, 0); + let mut actions = Vec::new(); + // Whatever the session is currently running; the harness follows the arbiter's switches + // so the cost it reports matches the arm actually in effect. + let mut live = inc; + for _ in 0..500 { + if arb.is_done() { + break; + } + let us = if live == inc { + incumbent_us + } else { + challenger_us + }; + if let Some(a) = arb.on_frame(us) { + actions.push(a); + match a { + ArbAction::SwitchTo(m) => live = m, + ArbAction::Settled(m) => live = m, + } + } + } + (actions, live) + } + + /// A clearly faster challenger is adopted, and the session ends up running it. + #[test] + fn arbiter_adopts_a_clearly_faster_challenger() { + let (actions, live) = drive(5000, 2400); + assert_eq!( + actions[0], + ArbAction::SwitchTo(M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32), + "must try the challenger before judging it" + ); + assert_eq!( + actions.last(), + Some(&ArbAction::Settled(M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32)) + ); + assert_eq!(live, M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32); + } + + /// A slower challenger is rejected and the session is put BACK — the arbiter is mid-experiment + /// when it decides, so "keep the incumbent" is a real reconfigure, not a no-op. Getting this + /// wrong would strand every losing arbitration on the losing arm. + #[test] + fn arbiter_restores_the_incumbent_when_the_challenger_loses() { + let (actions, live) = drive(2400, 5000); + assert_eq!( + actions.last(), + Some(&ArbAction::SwitchTo(M::NV_ENC_SPLIT_DISABLE_MODE as u32)), + "a losing experiment must be undone" + ); + assert_eq!(live, M::NV_ENC_SPLIT_DISABLE_MODE as u32); + } + + /// Within the margin the incumbent holds: switching costs a reconfigure and, on HEVC, sub-frame + /// readback, so a coin-flip difference must not move the session. + #[test] + fn arbiter_keeps_the_incumbent_inside_the_margin() { + // 5 % better — under WIN_MARGIN_PCT. + let (_, live) = drive(2400, 2280); + assert_eq!(live, M::NV_ENC_SPLIT_DISABLE_MODE as u32); + } + + /// THE SETTLE CONTRACT: the challenger must not be judged on frames taken immediately after the + /// switch. Feed it a transient — slow for the whole settle window, fast afterwards — and it + /// must still see the fast steady state. Without the settle window this arbiter would read the + /// transient, reject a genuinely better arm, and cache that verdict. + #[test] + fn arbiter_ignores_the_post_switch_transient() { + let (inc, chal) = ( + M::NV_ENC_SPLIT_DISABLE_MODE as u32, + M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32, + ); + let mut arb = SplitArbiter::with_handicap(inc, chal, 0); + let mut switched_at = None; + let mut frame = 0usize; + let mut outcome = None; + while outcome.is_none() && frame < 500 { + let us = match switched_at { + None => 5000, + // The transient: as slow as the incumbent for exactly the settle window. + Some(s) if frame - s <= SETTLE_FRAMES as usize => 5000, + Some(_) => 2000, + }; + match arb.on_frame(us) { + Some(ArbAction::SwitchTo(m)) if m == chal => switched_at = Some(frame), + Some(a) => outcome = Some(a), + None => {} + } + frame += 1; + } + assert_eq!( + outcome, + Some(ArbAction::Settled(chal)), + "the settle window must hide the post-switch transient — otherwise a better arm is \ + rejected on its own warmup" + ); + } +} + +/// The hand-written split constants in `codec.rs` MUST equal the SDK enum they mirror. They are +/// duplicated there so the libav path — which builds without the `nvenc` feature, where the enum +/// does not exist — can share one policy instead of keeping the copy that had already drifted. +/// This is the only place both are visible at once. +#[cfg(test)] +mod split_constant_parity { + use nvidia_video_codec_sdk::sys::nvEncodeAPI::NV_ENC_SPLIT_ENCODE_MODE as M; + + #[test] + fn nvenc_split_constants_match_the_sdk() { + assert_eq!(crate::SPLIT_AUTO, M::NV_ENC_SPLIT_AUTO_MODE as u32); + assert_eq!( + crate::SPLIT_AUTO_FORCED, + M::NV_ENC_SPLIT_AUTO_FORCED_MODE as u32 + ); + assert_eq!( + crate::SPLIT_TWO_FORCED, + M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32 + ); + assert_eq!( + crate::SPLIT_THREE_FORCED, + M::NV_ENC_SPLIT_THREE_FORCED_MODE as u32 + ); + assert_eq!(crate::SPLIT_DISABLE, M::NV_ENC_SPLIT_DISABLE_MODE as u32); + } +} diff --git a/crates/pf-encode/src/enc/windows/nvenc.rs b/crates/pf-encode/src/enc/windows/nvenc.rs index c960080b..67c0f7ce 100644 --- a/crates/pf-encode/src/enc/windows/nvenc.rs +++ b/crates/pf-encode/src/enc/windows/nvenc.rs @@ -44,10 +44,16 @@ use super::nvenc_core::{ apply_low_latency_config, build_init_params, cached_ceiling, codec_guid, plan_range_recovery, - resolve_slices, resolve_split_mode, resolve_split_subframe, resolve_subframe, store_ceiling, - subframe_env_forced, CeilingKey, LowLatencyConfig, NvStatusExt, RangePlan, + resolve_slices, resolve_split_subframe, resolve_subframe, store_ceiling, subframe_env_forced, + CeilingKey, LowLatencyConfig, NvStatusExt, RangePlan, +}; +// Moved to `codec.rs` (WP4) so the libav path, which builds without the `nvenc` feature, can share +// one split policy instead of keeping the copy that had already drifted. +use super::nvenc_core::{ + cached_split_verdict, store_split_verdict, ArbAction, SplitArbiter, SplitKey, }; use super::nvenc_status; +use super::{max_forced_split_mode, resolve_split_mode}; use super::{AuChunk, ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps}; use anyhow::{anyhow, bail, Context, Result}; use pf_frame::{CapturedFrame, FramePayload, PixelFormat}; @@ -592,6 +598,21 @@ pub struct NvencD3d11Encoder { /// sub-frame readback (the Linux backend's rule since its Phase 3; Windows joined after the /// 2026-07-31 on-glass A/B), so a GPU without it never has sub-frame forced by default. subframe_cap: bool, + /// `NV_ENC_CAPS_NUM_ENCODER_ENGINES` — how many NVENC engines this GPU has, probed in + /// [`query_caps`](Self::query_caps). `0` = not probed / unreadable. The split-encode ceiling: + /// the driver accepts a split wider than the hardware and silently encodes narrower, so this + /// is the only honest source for how wide we may go (see `codec::max_forced_split_mode`). + encoder_engines: u32, + /// Submit stamp for the split arbiter's per-frame cost (sync depth-1 path only). + last_submit_at: Option, + /// Whole-AU paced-send time (µs) the host last reported. `0` = never reported, which keeps + /// the arbiter out of the sub-frame trade it cannot otherwise price. + send_spread_us: u32, + /// Sub-frame state the session was OPENED able to run, so a return to a non-forced split can + /// restore it without ever turning it on for a session that never had it. + subframe_opened_with: bool, + /// The live split-mode experiment, when one is running. + arbiter: Option, /// (bitstream, mapped input resource to unmap after retrieval, pts_ns, recovery-anchor) per /// in-flight encode. The fourth field tags the first frame encoded after a successful /// [`invalidate_ref_frames`](Encoder::invalidate_ref_frames) — the clean re-anchor P-frame the @@ -753,6 +774,11 @@ impl NvencD3d11Encoder { input_ring_depth: None, async_supported: false, subframe_cap: false, + encoder_engines: 0, + last_submit_at: None, + send_spread_us: 0, + subframe_opened_with: false, + arbiter: None, pending: VecDeque::new(), frame_idx: 0, force_kf: false, @@ -928,6 +954,10 @@ impl NvencD3d11Encoder { ); let async_enc = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_ASYNC_ENCODE_SUPPORT); let subframe = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_SUBFRAME_READBACK); + // How many NVENC engines this GPU has — the split-encode ceiling. Must be probed rather + // than inferred from a rejection: the driver ACCEPTS a split wider than the hardware and + // silently encodes narrower (measured on `.21`, see `max_forced_split_mode`). + let engines = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_NUM_ENCODER_ENGINES); let _ = (api().destroy_encoder)(enc); // Reject an over-range mode with a clear message instead of an opaque InvalidParam. @@ -962,6 +992,7 @@ impl NvencD3d11Encoder { self.custom_vbv = custom_vbv != 0; self.async_supported = async_enc != 0; self.subframe_cap = subframe != 0; + self.encoder_engines = engines.max(0) as u32; tracing::info!( rfi = self.rfi_supported, custom_vbv = self.custom_vbv, @@ -1034,6 +1065,126 @@ impl NvencD3d11Encoder { Ok(cfg) } + /// The config identity this session's split verdict is cached under. + fn split_key(&self) -> SplitKey { + // Same GPU identity as `ceiling_key`: the selected render adapter's LUID, `0` when + // unresolved. Advisory either way. + let gpu = pf_gpu::resolve_render_adapter_luid() + .map(|l| ((l.HighPart as u32 as u64) << 32) | l.LowPart as u64) + .unwrap_or(0); + SplitKey { + gpu, + codec: self.codec, + width: self.width, + height: self.height, + fps: self.fps, + bit_depth: self.bit_depth, + chroma_444: self.chroma_444, + } + } + + /// Move the LIVE session to `mode` without an IDR. Windows twin of the Linux method; S1 on + /// D3D11 proved `nvEncReconfigureEncoder` takes a changed `splitEncodeMode` with + /// `resetEncoder=0` and emits no keyframe on this device type too. + fn apply_split_mode(&mut self, mode: u32) -> bool { + let (prev_mode, prev_sub) = (self.split_mode, self.subframe_on); + let (mode, subframe) = resolve_split_subframe( + self.codec, + mode, + self.subframe_opened_with, + subframe_env_forced(), + ); + self.split_mode = mode; + self.subframe_on = subframe; + if self.reconfigure_bitrate(self.bitrate_bps) { + true + } else { + tracing::warn!( + from = prev_mode, + to = mode, + "NVENC split arbitration: driver refused the in-place split change — staying put" + ); + self.split_mode = prev_mode; + self.subframe_on = prev_sub; + false + } + } + + /// Feed one frame's encode cost to the split arbiter and act on its verdict. + fn feed_split_arbiter(&mut self, encode_us: u64) { + let Some(arb) = self.arbiter.as_mut() else { + return; + }; + let action = arb.on_frame(encode_us); + let done = arb.is_done(); + match action { + Some(ArbAction::SwitchTo(mode)) => { + if !self.apply_split_mode(mode) { + self.arbiter = None; + return; + } + } + Some(ArbAction::Settled(mode)) => store_split_verdict(self.split_key(), mode), + None => {} + } + if done { + store_split_verdict(self.split_key(), self.split_mode); + self.arbiter = None; + } + } + + /// Decide whether this session may run a live split experiment. Same gates as the Linux + /// backend — see its `arm_split_arbiter` for why each one is a correctness condition rather + /// than a preference; the only Windows difference is that `async_rt` is a real possibility + /// here (opt-in two-thread retrieve), and under it the submit→AU span includes queue depth, + /// so the comparison would be noise. + fn arm_split_arbiter(&mut self) { + if !matches!( + std::env::var("PUNKTFUNK_NVENC_SPLIT_ARBITRATE").as_deref(), + Ok("1") + ) { + return; + } + if std::env::var_os("PUNKTFUNK_SPLIT_ENCODE").is_some() + || cached_split_verdict(&self.split_key()).is_some() + || self.async_rt.is_some() + || self.encoder_engines < 2 + || self.codec == Codec::H264 + { + return; + } + let handicap_us = if self.subframe_on && self.codec != Codec::Av1 { + if self.send_spread_us == 0 || self.slices < 2 { + return; + } + let slices = self.slices as u64; + self.send_spread_us as u64 * (slices - 1) / slices + } else { + 0 + }; + let disable = nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_DISABLE_MODE as u32; + let widest = max_forced_split_mode(self.encoder_engines); + let challenger = if self.split_mode == widest { + disable + } else { + widest + }; + if challenger == self.split_mode { + return; + } + tracing::info!( + incumbent = self.split_mode, + challenger, + handicap_us, + "NVENC split arbitration armed (Windows) — measuring both arms live (no IDR)" + ); + self.arbiter = Some(SplitArbiter::with_handicap( + self.split_mode, + challenger, + handicap_us, + )); + } + /// This session config's identity in the process-lifetime bitrate-ceiling cache /// (`nvenc_core::{cached_ceiling, store_ceiling}`). GPU identity is the selected render /// adapter's LUID — the adapter the capturer's device (and so this session) lives on; `0` @@ -1154,7 +1305,8 @@ impl NvencD3d11Encoder { // precedence (env override / the measured Main10 don't-split rule / pixel rate). // The init-failure fallback below disables it if a codec/config rejects it. let pixel_rate = self.width as u64 * self.height as u64 * self.fps.max(1) as u64; - let split_mode: u32 = resolve_split_mode(self.bit_depth, pixel_rate); + let split_mode: u32 = + resolve_split_mode(self.codec, self.bit_depth, pixel_rate, self.encoder_engines); // Negotiated multi-slice (P2f): the direct-NVENC default of 4, clamped by the // client's ceiling — a single-slice client keeps today's shape, a // VIDEO_CAP_MULTI_SLICE / Moonlight slices-per-frame client gets real slices. @@ -1400,6 +1552,15 @@ impl NvencD3d11Encoder { } self.inited = true; tracing::info!( + // Parity with the Linux session-ready line. `split_mode` is the FINAL mode (post + // any rejection fallback) and `engines` the ceiling it was chosen from — the mode + // alone is ambiguous between "used every engine" and "left one idle", and the + // driver honours an over-wide request without complaint, so neither number means + // much without the other. `subframe` because AUTO + sub-frame is a measurably + // single-engine combination that reads like a split in a log. + split_mode = self.split_mode, + engines = self.encoder_engines, + subframe = self.subframe_on, "NVENC D3D11 session: {}x{}@{} {}-bit{} {} Mbps {:?}", self.width, self.height, @@ -1409,6 +1570,8 @@ impl NvencD3d11Encoder { self.bitrate_bps / 1_000_000, self.codec_guid ); + self.subframe_opened_with = self.subframe_on; + self.arm_split_arbiter(); Ok(()) } } @@ -1752,6 +1915,9 @@ impl Encoder for NvencD3d11Encoder { anchor, idr_hint, )); + // Split-arbiter cost stamp; only meaningful on the sync depth-1 path, which is the + // only path `arm_split_arbiter` allows an experiment on. + self.last_submit_at = Some(std::time::Instant::now()); // Async: hand the in-flight encode to the retrieve thread (channel capacity = POOL ≥ // in-flight, so this send never blocks). The pending entry above pairs with its // completion FIFO in `absorb_done`. @@ -1935,6 +2101,13 @@ impl Encoder for NvencD3d11Encoder { if !map.is_null() { let _ = (api().unmap_input_resource)(self.encoder, map); } + let encode_us = self + .last_submit_at + .take() + .map(|t| t.elapsed().as_micros() as u64); + if let Some(us) = encode_us { + self.feed_split_arbiter(us); + } Ok(Some(EncodedFrame { data, pts_ns, @@ -2194,6 +2367,10 @@ impl Encoder for NvencD3d11Encoder { } } + fn set_send_spread_us(&mut self, us: u32) { + self.send_spread_us = us; + } + fn applied_bitrate_bps(&self) -> Option { // `bitrate_bps` is the post-clamp truth: the open path's ceiling search and the // reconfigure path's cache clamp both write what the session ACTUALLY targets. @@ -2680,6 +2857,162 @@ mod tests { } } + /// ON-HARDWARE — **S1 on WINDOWS/D3D11**, the question that gates Windows split arbitration. + /// + /// Everything the split-encode programme rests on was proven on **Linux/CUDA**: that + /// `nvEncReconfigureEncoder` accepts a changed `splitEncodeMode` with `resetEncoder=0`, emits + /// **no IDR**, and actually takes effect. The Windows backend drives a different device type + /// (`NV_ENC_DEVICE_TYPE_DIRECTX`), so none of that transfers by assumption — and if the driver + /// refuses it here, Windows arbitration is simply not buildable and should not be attempted. + /// + /// Also checks the two things WP1.1 added, on real Windows hardware rather than by inference + /// from Linux: that `query_caps` latches `NUM_ENCODER_ENGINES`, and that the driver **honours + /// an over-ask** (asking for a 3-way split on a 2-engine card) — the behaviour that makes the + /// clamp necessary rather than defensive. + /// + /// Reports rather than asserts the verdict: both outcomes are legitimate findings. Run: + /// cargo test -p pf-encode --features nvenc -- --ignored --test-threads=1 \ + /// nvenc_split_reconfigure_in_place --nocapture + #[test] + #[ignore = "requires an NVIDIA GPU + driver — run manually on the RTX Windows box"] + fn nvenc_split_reconfigure_in_place() { + let _ = tracing_subscriber::fmt().with_test_writer().try_init(); + const W: u32 = 1920; + const H: u32 = 1080; + const BPS: u64 = 40_000_000; + let disable = nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_DISABLE_MODE as u32; + let two = nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_TWO_FORCED_MODE as u32; + + // Isolate the split variable exactly as the Linux spike does. + std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0"); + std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", "0"); + + // SAFETY: (test-only) the same straight-line D3D11/DXGI setup as `nvenc_reconfigure_no_idr`. + unsafe { + let factory: IDXGIFactory1 = CreateDXGIFactory1().expect("DXGI factory"); + let mut adapter = None; + for i in 0.. { + let Ok(a) = factory.EnumAdapters1(i) else { + break; + }; + if a.GetDesc1().expect("adapter desc").Flags & DXGI_ADAPTER_FLAG_SOFTWARE.0 as u32 + == 0 + { + adapter = Some(a); + break; + } + } + let adapter = adapter.expect("no hardware DXGI adapter"); + let (device, _ctx) = pf_frame::dxgi::make_device(&adapter).expect("make_device"); + let bytes = probe_pattern(W as usize, H as usize); + let init = D3D11_SUBRESOURCE_DATA { + pSysMem: bytes.as_ptr() as *const _, + SysMemPitch: W * 4, + SysMemSlicePitch: 0, + }; + let desc = D3D11_TEXTURE2D_DESC { + Width: W, + Height: H, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_B8G8R8A8_UNORM, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: D3D11_BIND_RENDER_TARGET.0 as u32, + CPUAccessFlags: 0, + MiscFlags: 0, + }; + let mut tex = None; + device + .CreateTexture2D(&desc, Some(&init), Some(&mut tex)) + .expect("pattern texture"); + let tex = tex.expect("null pattern texture"); + + let mut enc = NvencD3d11Encoder::open( + Codec::H265, + PixelFormat::Bgra, + W, + H, + 60, + BPS, + 8, + ChromaFormat::Yuv420, + 1, + ) + .expect("NVENC open"); + + let submit_and_poll = |enc: &mut NvencD3d11Encoder, range: std::ops::Range| { + let (mut aus, mut keyframes) = (0usize, 0usize); + for i in range { + let frame = CapturedFrame { + width: W, + height: H, + pts_ns: i * 16_666_667, + format: PixelFormat::Bgra, + payload: FramePayload::D3d11(D3d11Frame { + texture: tex.clone(), + device: device.clone(), + pyro: None, + }), + cursor: None, + }; + enc.submit_indexed(&frame, i as u32).expect("submit"); + while let Some(au) = enc.poll().expect("poll") { + aus += 1; + keyframes += au.keyframe as usize; + } + } + (aus, keyframes) + }; + + let (aus, kfs) = submit_and_poll(&mut enc, 0..6); + assert!(aus > 0 && kfs == 1, "opening IDR then steady P-frames"); + println!( + "S1(win): engines={} (latched by query_caps), opened split_mode={}", + enc.encoder_engines, enc.split_mode + ); + assert!( + enc.encoder_engines >= 2, + "this GPU reports {} NVENC engine(s) — S1 is not interpretable here", + enc.encoder_engines + ); + assert_eq!(enc.split_mode, disable, "must open split-disabled"); + + // THE SPIKE: change ONLY splitEncodeMode, in place, same bitrate. + enc.split_mode = two; + let accepted = enc.reconfigure_bitrate(BPS); + println!("S1(win): reconfigure DISABLE→TWO_FORCED accepted = {accepted}"); + if accepted { + let (aus, kfs) = submit_and_poll(&mut enc, 6..12); + assert!(aus > 0, "no AUs after the accepted reconfigure"); + println!( + "S1(win) VERDICT: {}", + if kfs == 0 { + "PASS — accepted with NO IDR on D3D11: Windows arbitration is buildable" + } else { + "FAIL — accepted but forced an IDR, which is the same as a rejection" + } + ); + enc.split_mode = disable; + let back = enc.reconfigure_bitrate(BPS); + println!("S1(win): reverse accepted = {back}"); + } else { + enc.split_mode = disable; + println!( + "S1(win) VERDICT: FAIL — the D3D11 path REFUSES an in-place split change. \ + Windows arbitration is not buildable; the Linux result does not transfer." + ); + } + enc.flush().ok(); + } + + std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE"); + std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME"); + } + /// ON-GLASS (RTX box): the measurement gating the AYUV 4:4:4 work — encodes the probe /// pattern through the REAL ARGB-input NVENC session once with `chromaFormatIDC=3`/FREXT /// and once as plain 4:2:0, so offline analysis of the two bitstreams answers (1) whether diff --git a/crates/pf-encode/src/lib.rs b/crates/pf-encode/src/lib.rs index 66c71ab3..984bddfd 100644 --- a/crates/pf-encode/src/lib.rs +++ b/crates/pf-encode/src/lib.rs @@ -287,6 +287,12 @@ impl Encoder for TrackedEncoder { fn set_wire_chunking(&mut self, shard_payload: usize) { self.inner.set_wire_chunking(shard_payload) } + // Same trap class again: unforwarded, the default no-op would leave the split arbitration + // permanently blind to send cost and it would never arbitrate the sub-frame trade — failing + // silently in the safe direction, which is the hardest kind to notice. + fn set_send_spread_us(&mut self, us: u32) { + self.inner.set_send_spread_us(us) + } // Forwarded for the same reason as `set_wire_chunking` above — an unforwarded default here // would silently leave the in-place backends pipelining past the capturer's ring. fn set_input_ring_depth(&mut self, depth: usize) { diff --git a/crates/pf-ffvk/Cargo.toml b/crates/pf-ffvk/Cargo.toml deleted file mode 100644 index d6e53dde..00000000 --- a/crates/pf-ffvk/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "pf-ffvk" -description = "Bindgen shim for FFmpeg's Vulkan hwcontext (libavutil/hwcontext_vulkan.h) — the AVVulkanDeviceContext/AVVkFrame surface ffmpeg-sys-next doesn't bind; enables Vulkan Video decode straight onto the presenter's device" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true - -# The bindings are generated at build time from the SYSTEM headers (libavutil + -# vulkan-headers), so they are ABI-exact for the installed FFmpeg — including the -# FF_API_VULKAN_* deprecation gates that change AVVulkanDeviceContext's layout between -# FFmpeg builds. This is deliberately not hand-transcribed. - -[target.'cfg(any(target_os = "linux", windows))'.dependencies] -ash = { version = "0.38", features = ["loaded"] } - -[build-dependencies] -# Same bindgen configuration as ffmpeg-sys-next (runtime = dlopen libclang). -bindgen = { version = "0.72", features = ["runtime"], default-features = false } -pkg-config = "0.3" - -[lints] -workspace = true diff --git a/crates/pf-ffvk/build.rs b/crates/pf-ffvk/build.rs deleted file mode 100644 index 10f0d75b..00000000 --- a/crates/pf-ffvk/build.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Generate bindings for `libavutil/hwcontext_vulkan.h` against the SYSTEM headers. -//! -//! ffmpeg-sys-next binds a curated header list that omits every hwcontext_*.h; the -//! Vulkan hwcontext structs (`AVVulkanDeviceContext`, `AVVkFrame`) are what let us run -//! FFmpeg's Vulkan Video decoder on the presenter's own VkDevice and read the decoded -//! VkImages back. Their layout depends on compile-time FF_API_* deprecation gates in -//! libavutil/version.h, so bindgen over the installed header is the only ABI-safe -//! source of truth — hand transcription would silently skew on the next FFmpeg bump. -//! -//! Header discovery is per-OS: Linux asks pkg-config; Windows reuses the FFMPEG_DIR -//! tree ffmpeg-sys-next links against (BtbN trees ship no .pc files) plus an explicit -//! Vulkan-Headers include dir, since Windows has no system . Other -//! targets get an empty file: the workspace builds on macOS (clients/apple is the -//! client there). - -use std::env; -use std::path::PathBuf; - -fn main() { - println!("cargo:rerun-if-changed=wrapper.h"); - println!("cargo:rerun-if-env-changed=PF_FFVK_VULKAN_INCLUDE"); - let out = PathBuf::from(env::var("OUT_DIR").unwrap()).join("bindings.rs"); - - let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); - let includes = match target_os.as_str() { - "linux" => linux_includes(), - "windows" => windows_includes(), - _ => { - std::fs::write( - &out, - "// pf-ffvk: Linux/Windows-only, empty on this target\n", - ) - .unwrap(); - return; - } - }; - - let mut builder = bindgen::Builder::default() - .header("wrapper.h") - // The whole point of this crate: the Vulkan hwcontext surface… - .allowlist_type("AVVulkan.*") - .allowlist_type("AVVkFrame.*") - .allowlist_function("av_vk_frame_alloc") - .allowlist_function("av_vkfmt_from_pixfmt") - // The feature structs chained into AVVulkanDeviceContext.device_features (plain - // vulkan.h types; generating them here keeps the chain in one type system). - .allowlist_type("VkPhysicalDeviceVulkan11Features") - .allowlist_type("VkPhysicalDeviceVulkan12Features") - .allowlist_type("VkPhysicalDeviceVulkan13Features") - // AVVulkanFramesContext.img_flags values (plane views need MUTABLE_FORMAT). - .allowlist_type("VkImageCreateFlagBits") - // Timeline-semaphore wait — the pump measures true GPU decode completion. - .allowlist_type("VkSemaphoreWaitInfo") - .allowlist_type("PFN_vkWaitSemaphores") - .allowlist_type("PFN_vkGetDeviceProcAddr") - // …plus nothing else of FFmpeg: the core types these structs reference only - // ever appear behind pointers here, so keep them opaque instead of duplicating - // ffmpeg-sys-next's definitions (callers cast pointers between the crates). - .opaque_type("AVHWDeviceContext") - .opaque_type("AVHWFramesContext") - .opaque_type("AVBufferRef") - .opaque_type("AVFrame") - .derive_debug(false) - .layout_tests(true); - for dir in &includes { - builder = builder.clang_arg(format!("-I{}", dir.display())); - } - let bindings = builder.generate().expect( - "bindgen over libavutil/hwcontext_vulkan.h failed — is `vulkan-headers` installed? \ - (the header includes )", - ); - bindings.write_to_file(&out).unwrap(); - - // The av_vk_* symbols live in libavutil, which ffmpeg-sys-next already links into - // every consumer of this crate; no extra link flags needed. Emitting the lib anyway - // keeps `cargo test -p pf-ffvk` linking standalone — which on Windows also needs the - // import-lib search path (there is no system linker path for FFmpeg there). - if target_os == "windows" { - // windows_includes() already required FFMPEG_DIR. - let ff = PathBuf::from(env::var("FFMPEG_DIR").unwrap()); - println!( - "cargo:rustc-link-search=native={}", - ff.join("lib").display() - ); - } - println!("cargo:rustc-link-lib=avutil"); -} - -/// Include paths from pkg-config (libavutil for the hwcontext header; the Vulkan -/// headers usually live in /usr/include, but honor a registered vulkan.pc too). -/// PF_FFVK_VULKAN_INCLUDE prepends an explicit Vulkan-Headers include dir — for -/// cross builds and boxes without the system package. -fn linux_includes() -> Vec { - let mut includes: Vec = Vec::new(); - if let Ok(dir) = env::var("PF_FFVK_VULKAN_INCLUDE") { - includes.push(PathBuf::from(dir)); - } - let avutil = pkg_config::Config::new() - .cargo_metadata(false) - .probe("libavutil") - .expect("pkg-config: libavutil not found — install the FFmpeg dev package"); - includes.extend(avutil.include_paths); - if let Ok(vk) = pkg_config::Config::new() - .cargo_metadata(false) - .probe("vulkan") - { - includes.extend(vk.include_paths); - } - includes -} - -/// No pkg-config on Windows: headers come from the FFMPEG_DIR tree (the same BtbN -/// lgpl-shared tree ffmpeg-sys-next links against) plus an explicit Vulkan-Headers -/// dir — PF_FFVK_VULKAN_INCLUDE (provision-windows-punktfunk-extras.ps1 stages -/// C:\Users\Public\vulkan-headers) or an installed Vulkan SDK. Only headers are -/// needed at build time; the loader (vulkan-1.dll) is a GPU-driver component and is -/// never linked here. -fn windows_includes() -> Vec { - println!("cargo:rerun-if-env-changed=FFMPEG_DIR"); - println!("cargo:rerun-if-env-changed=VULKAN_SDK"); - let mut includes: Vec = Vec::new(); - if let Ok(dir) = env::var("PF_FFVK_VULKAN_INCLUDE") { - includes.push(PathBuf::from(dir)); - } else if let Ok(sdk) = env::var("VULKAN_SDK") { - includes.push(PathBuf::from(sdk).join("Include")); - } else { - panic!( - "pf-ffvk: no Vulkan headers — set PF_FFVK_VULKAN_INCLUDE to a Vulkan-Headers \ - include dir (scripts/ci/provision-windows-punktfunk-extras.ps1 stages \ - C:\\Users\\Public\\vulkan-headers\\include) or install the Vulkan SDK (VULKAN_SDK)" - ); - } - let ff = env::var("FFMPEG_DIR").expect( - "pf-ffvk: FFMPEG_DIR not set — point it at the FFmpeg tree \ - (scripts/ci/provision-windows-punktfunk-extras.ps1 stages C:\\Users\\Public\\ffmpeg)", - ); - includes.push(PathBuf::from(ff).join("include")); - includes -} diff --git a/crates/pf-ffvk/src/lib.rs b/crates/pf-ffvk/src/lib.rs deleted file mode 100644 index 6ced96e1..00000000 --- a/crates/pf-ffvk/src/lib.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! FFmpeg's Vulkan hwcontext surface (`AVVulkanDeviceContext`, `AVVulkanFramesContext`, -//! `AVVkFrame`), bindgen-generated from the system headers at build time — see build.rs -//! for why this must not be hand-transcribed. -//! -//! The raw bindings use vulkan.h's own handle types (pointers on 64-bit). The [`ash`] -//! conversion helpers below cross between them and ash's u64-newtype handles; both sides -//! are the same underlying Vulkan object handles, so the casts are value-preserving. - -// Unsafe-proof program: every `unsafe {}` here carries a `// SAFETY:` proof. -#![deny(clippy::undocumented_unsafe_blocks)] -#![allow(non_upper_case_globals)] -#![allow(non_camel_case_types)] -#![allow(non_snake_case)] -#![allow(clippy::missing_safety_doc)] -// bindgen's layout tests deref-null-pointer by design; silence the lints they trip. -#![allow(deref_nullptr)] -#![allow(unnecessary_transmutes)] - -#[cfg(any(target_os = "linux", windows))] -include!(concat!(env!("OUT_DIR"), "/bindings.rs")); - -/// Conversions between the generated vulkan.h handle types and ash's. -#[cfg(any(target_os = "linux", windows))] -pub mod ashx { - use super::*; - use ash::vk::Handle as _; - - /// vulkan.h non-dispatchable handles are `*mut T` on 64-bit; ash's are `u64` - /// newtypes. Same bits either way. - pub fn image(h: VkImage) -> ash::vk::Image { - ash::vk::Image::from_raw(h as u64) - } - - pub fn semaphore(h: VkSemaphore) -> ash::vk::Semaphore { - ash::vk::Semaphore::from_raw(h as u64) - } - - // bindgen's enum repr is target-dependent: u32 on Linux (clang default), i32 on - // MSVC — so the cast is required on one target and a same-type no-op on the other. - #[allow(clippy::unnecessary_cast)] - pub fn image_layout(l: VkImageLayout) -> ash::vk::ImageLayout { - ash::vk::ImageLayout::from_raw(l as i32) - } - - // --- ash → vulkan.h (filling AVVulkanDeviceContext) --------------------------------- - - pub fn to_instance(h: ash::vk::Instance) -> VkInstance { - h.as_raw() as VkInstance - } - - pub fn to_physical_device(h: ash::vk::PhysicalDevice) -> VkPhysicalDevice { - h.as_raw() as VkPhysicalDevice - } - - pub fn to_device(h: ash::vk::Device) -> VkDevice { - h.as_raw() as VkDevice - } - - /// ash's loader-level `vkGetInstanceProcAddr` as the header's PFN type. Both are the - /// same C ABI function pointer (`extern "system"` == `extern "C"` on the platforms - /// this crate builds for). - pub fn to_get_proc_addr( - f: unsafe extern "system" fn( - ash::vk::Instance, - *const std::ffi::c_char, - ) -> ash::vk::PFN_vkVoidFunction, - ) -> PFN_vkGetInstanceProcAddr { - // SAFETY: both sides are `extern "system"` fn pointers with the identical signature — - // `(VkInstance, *const c_char) -> PFN_vkVoidFunction`. The transmute only reinterprets the - // ash-side type alias as our bindgen-side one, which are the same ABI type. - unsafe { std::mem::transmute(f) } - } -} - -#[cfg(all(test, any(target_os = "linux", windows)))] -mod tests { - use super::*; - - /// The allocator runs (links against the system libavutil) and the struct is - /// readable at the offsets bindgen computed — sem_value zero-initialized. - #[test] - fn vk_frame_alloc_links_and_zeroes() { - // SAFETY: `av_vk_frame_alloc` is libavutil's own allocator and returns either null — - // asserted against before any field is read — or a zero-initialized `AVVkFrame` valid for - // the reads below. The frame is deliberately leaked, so nothing frees it twice. - unsafe { - let f = av_vk_frame_alloc(); - assert!(!f.is_null(), "av_vk_frame_alloc returned NULL"); - assert_eq!((*f).sem_value[0], 0); - assert_eq!((*f).queue_family[0], 0); - // Leak the one test frame rather than binding av_free here. - } - } - - /// AV_NUM_DATA_POINTERS-sized arrays came through with the right length. - #[test] - fn frame_arrays_are_av_num_data_pointers() { - // SAFETY: `AVVkFrame` is a `repr(C)` POD of scalars, handles and fixed-size arrays, so - // all-zeroes is a valid bit pattern for it; the test only reads array lengths. - let f: AVVkFrame = unsafe { std::mem::zeroed() }; - assert_eq!(f.img.len(), 8); - assert_eq!(f.sem_value.len(), 8); - } -} diff --git a/crates/pf-ffvk/wrapper.h b/crates/pf-ffvk/wrapper.h deleted file mode 100644 index 9a8a286f..00000000 --- a/crates/pf-ffvk/wrapper.h +++ /dev/null @@ -1,3 +0,0 @@ -/* The one header ffmpeg-sys-next's bindgen list omits: FFmpeg's Vulkan hwcontext. - * Pulls (the vulkan-headers package) transitively. */ -#include diff --git a/crates/pf-presenter/Cargo.toml b/crates/pf-presenter/Cargo.toml index d0e175c3..40c789e3 100644 --- a/crates/pf-presenter/Cargo.toml +++ b/crates/pf-presenter/Cargo.toml @@ -16,9 +16,13 @@ repository.workspace = true # Otherwise a consumer that deliberately builds us without `pyrowave` still drags the vendored # C++ in — fatal on Windows ARM64, where Granite has no SIMD path. pf-client-core = { path = "../pf-client-core", default-features = false } -# AVVkFrame access (Vulkan Video frames: live sync state under the frames lock). -pf-ffvk = { path = "../pf-ffvk" } punktfunk-core = { path = "../punktfunk-core", features = ["quic"] } +# `--probe-decode` reports the driver's own video-format answers through pf-vkdecode's +# query rather than a second copy of it — a probe that keeps its own copy is a probe +# that eventually disagrees with the code it exists to explain (VIDEO_BASE's doc says +# the same thing about the extension list). Already in the tree via pf-client-core; +# named here because setup.rs calls it directly. +pf-vkdecode = { path = "../pf-vkdecode" } # `loaded` dlopens libvulkan at runtime (no link-time dependency — GPU-less boxes still # start and fail into a clean error; on Windows vulkan-1.dll is a GPU-driver component). diff --git a/crates/pf-presenter/src/csc.rs b/crates/pf-presenter/src/csc.rs index 5e5bb48f..dfcccabe 100644 --- a/crates/pf-presenter/src/csc.rs +++ b/crates/pf-presenter/src/csc.rs @@ -40,9 +40,15 @@ impl CscPass { ) } - /// The planar 3-plane variant (separate Cb/Cr R8 planes — the PyroWave decode - /// output, design/pyrowave-codec-plan.md §4.5). Same push-constant contract. - #[cfg(feature = "pyrowave")] + /// The planar 3-plane variant (separate Cb/Cr R8 planes). Same push-constant + /// contract. + /// + /// Two producers now: the PyroWave decode output + /// (design/pyrowave-codec-plan.md §4.5) and — since M8 — the SOFTWARE rung, whose + /// I420 planes the presenter uploads and converts here instead of receiving swscale's + /// RGBA. That is why this is no longer feature-gated or probe-gated: the CPU rung is + /// the ladder's last one, so it must exist on every device, including the ones that + /// failed the pyrowave probe. pub fn new_planar(device: &ash::Device, attachment_format: vk::Format) -> Result { Self::build( device, @@ -222,14 +228,19 @@ impl CscPass { } /// Planar variant of [`bind_planes`](Self::bind_planes): three single-component - /// plane views in GENERAL layout (the pyrowave decode leaves them there; same - /// fence-wait safety contract). - #[cfg(feature = "pyrowave")] - pub fn bind_planes_planar(&self, device: &ash::Device, planes: [vk::ImageView; 3]) { + /// plane views, in the layout their producer left them in — GENERAL for the pyrowave + /// decode, `SHADER_READ_ONLY_OPTIMAL` for the software rung's uploaded planes. Same + /// fence-wait safety contract. + pub fn bind_planes_planar( + &self, + device: &ash::Device, + planes: [vk::ImageView; 3], + layout: vk::ImageLayout, + ) { let infos = planes.map(|view| { [vk::DescriptorImageInfo::default() .image_view(view) - .image_layout(vk::ImageLayout::GENERAL)] + .image_layout(layout)] }); let writes = [0u32, 1, 2].map(|b| { vk::WriteDescriptorSet::default() diff --git a/crates/pf-presenter/src/lib.rs b/crates/pf-presenter/src/lib.rs index db34f32f..c868e158 100644 --- a/crates/pf-presenter/src/lib.rs +++ b/crates/pf-presenter/src/lib.rs @@ -3,7 +3,9 @@ //! decoded frames, captures input on the `ui_stream` state-machine contract, and reports //! the unified stats window on stdout. No UI toolkit anywhere in the dependency tree. //! -//! Three frame paths: software (`CpuFrame` RGBA staging upload), Vulkan Video (the +//! Three frame paths: software (`CpuPlanarFrame` — I420 planes staged into three R8 +//! images and converted by the same CICP-driven CSC pass as the hardware lanes; before M8 +//! this lane arrived as swscale RGBA and skipped the pass entirely), Vulkan Video (the //! decoder's VkImage on THIS device — plane views + the CICP-driven CSC pass), and on //! Linux additionally VAAPI hardware (NV12 dmabuf imported per-plane — `dmabuf.rs`), //! all composited by a letterboxed blit. Devices without the import extensions, and any diff --git a/crates/pf-presenter/src/overlay.rs b/crates/pf-presenter/src/overlay.rs index 9bca9afa..13748e52 100644 --- a/crates/pf-presenter/src/overlay.rs +++ b/crates/pf-presenter/src/overlay.rs @@ -20,9 +20,11 @@ pub struct SharedDevice { pub device: ash::Device, pub queue: vk::Queue, pub queue_family_index: u32, - /// External-sync lock for `queue` — FFmpeg's decode prep submits to the same queue - /// from the pump thread, so every overlay flush/submit must hold it (the presenter - /// and FFmpeg's `lock_queue` callbacks serialize on this same lock). + /// External-sync lock for `queue` — the decode lane submits to the same queue + /// from the pump thread, so every overlay flush/submit must hold it. The presenter, + /// this overlay and the native decode lane all serialize on this one lock; take it + /// with [`pf_client_core::video::QueueLock::guard`], whose RAII form is what every + /// Rust caller wants. pub queue_lock: std::sync::Arc, } @@ -100,6 +102,53 @@ pub enum OverlayAction { CancelConnect, /// Quit the launcher (B at the root) — ends the process, Gaming Mode returns. Quit, + /// Put this text on the system clipboard (the host menu's "Copy link"). An action + /// rather than a console command because the clipboard belongs to SDL, which lives on + /// the run loop's thread and nowhere else. + CopyText(String), +} + +/// Which button a [`PointerInput`] press/release carries. A touchscreen contact always +/// arrives as `Primary` — there is no second finger-button, and the console's back +/// affordance is on glass. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PointerButton { + Primary, + /// The right button — the console reads it as Back, the pointer's B. + Secondary, +} + +/// Pointer or touch input offered to the overlay, in SWAPCHAIN PIXELS. +/// +/// Pixels, not window coordinates, because that is the space the overlay renders in: a +/// screen hit-tests the very rects it drew last frame instead of re-deriving a layout +/// through the display scale. The run loop owns the conversion — it is the side that +/// holds the window. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PointerInput { + Move { + x: f32, + y: f32, + }, + Down { + x: f32, + y: f32, + button: PointerButton, + }, + Up { + x: f32, + y: f32, + button: PointerButton, + }, + /// One wheel/trackpad scroll step at `x`/`y`; `dy` > 0 scrolls away from the user. + Wheel { + x: f32, + y: f32, + dy: f32, + }, + /// The gesture was abandoned (the pointer left the window, the touch was canceled) — + /// any armed press is dropped without acting. + Cancel, } /// Session lifecycle notifications into the overlay (browse mode drives its scenes off @@ -113,6 +162,15 @@ pub enum SessionPhase<'a> { Failed(&'a str), /// The session ran and ended (`Some` = abnormal reason for the status strip). Ended(Option<&'a str>), + /// The session ended and the client is DIALING AGAIN by itself — today only because + /// the negotiated codec ran out of decode rungs (M8's software-HEVC drop) and the + /// retry advertises a codec this device can actually finish. + /// + /// Distinct from [`Self::Ended`] and [`Self::Failed`] because the user's next action + /// is different: nothing. "Session ended — HEVC decoding failed" invites a manual + /// reconnect that is already in flight, and "Couldn't connect" is simply false — the + /// connect worked, the decode did not. + Reconnecting(&'a str), } /// The console-UI side. Object-safe; the session binary passes @@ -131,6 +189,17 @@ pub trait Overlay { None } + /// Mouse/touch input, in swapchain pixels, before capture sees it. `true` = consumed + /// (the console is up and something under the pointer took it) — the event must not + /// reach capture/forwarding. + /// + /// Separate from [`Self::handle_event`] because the window→pixel conversion belongs to + /// the run loop, which is the side that holds the window: the overlay renders in + /// pixels and would otherwise have to re-derive the display scale it never sees. + fn handle_pointer(&mut self, _input: PointerInput) -> bool { + false + } + /// Drain one pending action raised by handled input. Called once per loop /// iteration; return `None` when idle. fn take_action(&mut self) -> Option { diff --git a/crates/pf-presenter/src/run.rs b/crates/pf-presenter/src/run.rs index 9eb6e7a2..93a76348 100644 --- a/crates/pf-presenter/src/run.rs +++ b/crates/pf-presenter/src/run.rs @@ -17,7 +17,9 @@ //! D disconnect, S stats tier, V microphone mute. use crate::input::{Capture, FingerPhase}; -use crate::overlay::{FrameCtx, Overlay, OverlayAction, OverlayFrame, SessionPhase}; +use crate::overlay::{ + FrameCtx, Overlay, OverlayAction, OverlayFrame, PointerButton, PointerInput, SessionPhase, +}; use crate::present_pace::{ Cadence, CadenceProbe, FrameStore, LatchClock, PresentGate, MARGIN_MAX_NS, MARGIN_STEP_NS, }; @@ -221,10 +223,14 @@ struct StreamState { /// mid-stream re-syncs keep the end-to-end number honest after an NTP step / drift. clock_offset: Option>, hdr: bool, - /// The presented lane was the CPU/software one, where a PQ stream is shown RAW — the - /// software path has no tone-map pass at all (the presenter uploads swscale RGBA - /// as-is; the CSC mode-1 tonemap is hardware-lane only) — so the OSD badge reads - /// `HDR→SDR (raw)` there instead of claiming a tone-map that never ran. + /// The presented lane shows a PQ stream RAW — no tone-map pass ran — so the OSD badge + /// reads `HDR→SDR (raw)` instead of claiming one that never did. + /// + /// Nothing sets it since M8. It used to mark the software lane, which arrived as + /// swscale RGBA and skipped the CSC pass entirely; that lane now uploads planes into + /// the same planar CSC pass as the hardware lanes and tone-maps in mode 1 like them. + /// Kept — not deleted — because the badge's distinction is real and the next lane that + /// bypasses the pass must be able to say so rather than quietly claim a tone-map. hdr_untonemapped: bool, // Presenter-side 1 s window (design/stats-unification.md): end-to-end // capture→displayed (host-clock corrected) p50+p95, display = decoded→displayed p50. @@ -279,6 +285,10 @@ struct StreamState { /// warn on the first failure of a streak, then stay quiet until a present succeeds. #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] pyro_present_warned: bool, + /// The same latch for the SOFTWARE lane, which since M8 has real failure modes (three + /// plane images + their allocations and views, rebuilt on every size change, plus a + /// render pass) and — being the ladder's LAST rung — nothing left to demote to. + cpu_present_warned: bool, hw_fails: u32, /// The OSD's text (multi-line; rebuilt each Stats window and on a live tier cycle). osd_text: String, @@ -322,6 +332,15 @@ struct StreamState { /// `None` = nothing sent yet. Edge-detected each iteration from the live mouse model, so /// the chord, the M3 auto-flip, and engage/release all reconcile through one path. sent_client_draws: Option, + /// The params this session was started with, kept so a codec fallback can re-dial + /// with `exclude_codecs` widened — see [`SessionEvent::CodecFallback`]. Cloned once + /// per session start, so anything the SESSION changed after launch (an accepted mode + /// switch) is not in here and the retry re-reads it from the connector. + /// + /// The latch grid rides along by `Arc` on purpose — it is the presenter's, not the + /// session's. `force_software` does NOT: it is a per-session demote latch, and the + /// retry replaces it (a fallback would otherwise open on software). + params: SessionParams, } impl StreamState { @@ -343,6 +362,8 @@ impl StreamState { // pump reads (see `LatchGrid`), so keep the Arc before the params move. `None` // when the session didn't advertise the cap — the 1 Hz fold then skips the work. let latch_grid = params.phase_lock.then(|| params.latch_grid.clone()); + // Kept for a codec-fallback re-dial (`SessionEvent::CodecFallback`). + let retry_params = params.clone(); let handle = session::start(params); let (wake_tx, wake_rx) = async_channel::bounded(2); let pump_rx = handle.frames.clone(); @@ -392,6 +413,7 @@ impl StreamState { dmabuf_demoted: false, #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] pyro_present_warned: false, + cpu_present_warned: false, hw_fails: 0, osd_text: String::new(), last_stats: None, @@ -401,6 +423,7 @@ impl StreamState { shown_mode: None, resize_overlay: ResizeIndicator::default(), last_video: None, + params: retry_params, } } @@ -451,7 +474,7 @@ impl StreamState { /// Whether a present error is `VK_ERROR_DEVICE_LOST` anywhere in its chain. A lost /// device is unrecoverable by spec — every object on it (decoder frames, swapchain, /// the Skia context) is dead, and the demote-to-software path would rebuild the -/// decoder against that same dead device (observed live 2026-07-09: FFmpeg wedges +/// decoder against that same dead device (observed live 2026-07-09: the decode lane wedges /// inside the rebuild, the decode thread never returns, and the client zombies with /// the pump flushing a never-draining backlog every 2 s). The only correct response /// is to fail the session loudly and let the shell relaunch. @@ -697,6 +720,14 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result if o.handle_event(&event) { continue; } + // …and the same for mouse/touch, which the console hit-tests in its own + // pixel space. Consumed while the console is up; ignored while streaming, + // where these belong to `Capture` below. + if let Some(input) = overlay_pointer(&event, &window) { + if o.handle_pointer(input) { + continue; + } + } } match event { Event::Quit { .. } => { @@ -1177,6 +1208,13 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result } } } + // The console already toasted "Link copied"; a clipboard SDL refuses is + // worth a log line but not worth contradicting the toast over. + OverlayAction::CopyText(text) => { + if let Err(e) = video.clipboard().set_clipboard_text(&text) { + tracing::warn!(error = %e, "copying to the clipboard"); + } + } action => { let force_software = Arc::new(AtomicBool::new(false)); match on_action( @@ -1196,6 +1234,22 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result opts.render_scale_max_dim, ); } + // A live pump here would be DETACHED by the assignment + // below — `StreamState` has no `Drop`, so its thread + // would keep decoding onto the shared Vulkan device that + // gets destroyed at exit. The console normally gates the + // launch behind `in_stream`/`connecting`, but M8's + // Reconnecting phase is the first state that is neither + // while the stream is still alive. Every other + // replacement site takes-and-shuts-down; so does this + // one. + if let Some(prev) = stream.take() { + tracing::warn!( + "launch while a session was still attached — \ + stopping it first" + ); + prev.shutdown(); + } stream = Some(StreamState::new( *params, force_software, @@ -1353,6 +1407,103 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result } } } + // M8's HEVC path, as a first-class flow rather than a dead session: the + // negotiated codec ran out of decode rungs, so re-dial the SAME host with + // that codec removed from the advertised caps and let the host pick + // again. The pump computed the retry (never re-offering the failed codec, + // and — when the CODEC is what has no CPU rung — never offering one + // without a CPU rung either) and left nothing of its own running before + // sending this: the mid-stream site joins the audio/pad/clipboard threads + // and drops the connector, the construction-time site never spawned them. + // So starting the new session here is a clean start, not an overlap. + // + // Applies in BOTH modes. In single (`--connect`) mode there is no console + // to fall back to, which is exactly where limping-on-software used to be + // the only option; browse mode gets the same retry plus a toast. + SessionEvent::CodecFallback { + exclude_codecs, + retry_caps, + msg, + } => { + tracing::warn!( + %msg, + exclude_codecs, + retry_caps, + "decode ladder exhausted — reconnecting with reduced codec caps" + ); + gamepad.detach(); + if let Some(cap) = &mut st.capture { + cap.release(true); + } + apply_capture(&mut window, &mouse, false, false, inhibit_shortcuts); + // Widen the exclusion rather than replace it: a second fallback in the + // same run must not re-offer what the first one already ruled out. + let mut params = st.params.clone(); + params.exclude_codecs |= exclude_codecs; + // The mode this session ENDED on, not the one it dialled with: a + // mid-session `Reconfigure` the host accepted lives only in the + // connector, and `st.params` is a clone taken at launch — re-sending + // it would silently undo the switch on the retry. + if let Some(c) = &st.connector { + params.mode = c.mode(); + } + // ...and then the window follower on top, exactly as + // `ActionOutcome::Start` does, so a retry lands on the size the + // window is NOW rather than the size it was at launch. + if opts.match_window.is_some() { + apply_match_window( + &mut params, + &window, + opts.render_scale, + opts.render_scale_max_dim, + ); + } + // A FRESH demote flag, like `ActionOutcome::Start` builds — never the + // old session's. `force_software` is a latch the presenter sets when + // the hardware PRESENT path fails three times; inheriting it made an + // HEVC→H.264 retry open a SOFTWARE H.264 decoder on a box with + // perfectly good hardware H.264. It is shared with `params` because + // both ends of it belong to this presenter. + let force_software = Arc::new(AtomicBool::new(false)); + params.force_software = force_software.clone(); + // ⚠ `params.launch` rides along VERBATIM, and that is a deliberate + // choice between two wrong answers, not an assumption of idempotence. + // The host has no "already running → attach" branch on the launch + // path (`punktfunk-host`'s `native/stream.rs` launches + // unconditionally; its "launched ONCE" guarantee is scoped to + // mid-stream rebuilds WITHIN a session), and the game survives the + // session end under the default `GameOnSessionEnd::Keep`. So the + // re-send is idempotent only where the LAUNCHER dedupes it — + // `steam://rungameid` focuses the running copy, an Epic/AUMID URI + // likewise — while a `gog:`/`custom:` target really does start a + // second copy. Dropping the field instead is worse where it matters + // most: on Linux the per-session gamescope is re-adopted through + // `pf-vdisplay`'s display registry, whose reuse key INCLUDES the + // launch command, so a retry without it would miss the lingering + // display and orphan the running game inside it. Keeping it is the + // only option that preserves that attach; the real fix is host-side + // (an idempotency key on `Hello::launch`, or a running-title check + // before the spawn) and is not M8's to make. + // + // Known and unfixed here for the same reason: the RETRY's game lease + // cannot adopt a game that predates its own launch stamp (`procscan` + // rejects anything started more than 2 s before it), so a reconnected + // session has no game-exit detection for the rest of its life. + if let Some(st) = stream.take() { + st.shutdown(); + } + if let Some(o) = overlay.as_mut() { + o.session_phase(SessionPhase::Reconnecting(&msg)); + } + stream = Some(StreamState::new( + params, + force_software, + events.event_sender(), + present_priority, + native.refresh_hz, + )); + break; + } } } @@ -1639,13 +1790,48 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result } DecodedImage::Cpu(c) => { st.hdr = c.color.is_pq(); - // The software lane shows PQ raw (no tone-map pass exists there) - // — the OSD badge must not claim `HDR→SDR` for it. - st.hdr_untonemapped = true; - presenter.present(&window, FrameInput::Cpu(&c), overlay_frame.as_ref())? + // Since M8 the software lane uploads planes into the SAME planar + // CSC pass as the hardware lanes, so a PQ stream is tone-mapped + // there exactly like theirs — the badge no longer has to warn + // that this lane shows PQ raw, because it does not. + st.hdr_untonemapped = false; + // Same treatment as the pyrowave arm below, and for the same + // reason: since M8 this arm allocates three plane images (plus + // memory and views) on every size change and runs a render pass, + // so it has failure modes a staging upload never had — and it is + // the LAST rung, so a present failure has nothing left to demote + // to. Drop the frame and keep the session; only a lost device + // ends it. + match presenter.present( + &window, + FrameInput::Cpu(&c), + overlay_frame.as_ref(), + ) { + Ok(p) => { + st.cpu_present_warned = false; + p + } + Err(e) => { + if device_lost(&e) { + return Err(e) + .context("GPU device lost — the session cannot continue"); + } + if !st.cpu_present_warned { + st.cpu_present_warned = true; + tracing::warn!( + error = %format!("{e:#}"), + "software present failed — suppressing repeats until it recovers" + ); + } + false + } + } } + // The VAAPI rung's output: dmabuf fds plus a plane layout, guard + // opaque. (This arm took libavcodec's VAAPI rung too until M10; the + // import and its failure-streak demotion were identical for both.) #[cfg(target_os = "linux")] - DecodedImage::Dmabuf(d) + DecodedImage::NativeDmabuf(d) if presenter.supports_dmabuf() && !st.dmabuf_demoted => { st.hdr = d.color.is_pq(); @@ -1682,7 +1868,7 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result } } #[cfg(target_os = "linux")] - DecodedImage::Dmabuf(_) => { + DecodedImage::NativeDmabuf(_) => { // No import extensions on this device (or already demoted) — the // pump rebuilds the decoder as software; frames flow again soon. if !st.dmabuf_demoted { @@ -1742,15 +1928,17 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result } false } - // Vulkan-Video: decoded on the presenter's own device — present is - // views + CSC, no import step to gate on. Same failure-streak - // demotion contract as the dmabuf path. - DecodedImage::VkFrame(v) if !st.dmabuf_demoted => { + // Native Vulkan Video (pf-vkdecode): decoded on the presenter's own + // device — present is views + CSC, no import step to gate on. Same + // failure-streak demotion contract as the dmabuf path. A + // drained/demoted frame drops through the arm below — its guard still + // returns the decoder's slot. + DecodedImage::NativeVk(v) if !st.dmabuf_demoted => { st.hdr = v.color.is_pq(); st.hdr_untonemapped = false; match presenter.present( &window, - FrameInput::VkFrame(v), + FrameInput::NativeVk(v), overlay_frame.as_ref(), ) { Ok(p) => { @@ -1765,7 +1953,7 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result } st.hw_fails += 1; tracing::warn!(error = %format!("{e:#}"), fails = st.hw_fails, - "vulkan-video present failed"); + "native vulkan present failed"); if st.hw_fails >= 3 { st.dmabuf_demoted = true; tracing::warn!("demoting the decoder to software"); @@ -1775,7 +1963,7 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result } } } - DecodedImage::VkFrame(_) => false, // demoted — drain until rebuild + DecodedImage::NativeVk(_) => false, // demoted — drain until rebuild }; if did_present { presented_video = true; @@ -2189,6 +2377,82 @@ fn apply_capture( } } +/// One SDL mouse/touch event as the overlay wants it: swapchain PIXELS, which is the +/// space the console renders and hit-tests in. `None` for events the console can't use. +/// +/// Two different conversions, and mixing them up puts every click off by the display +/// scale: SDL reports mouse positions in WINDOW coordinates (logical units — 1× on a +/// HiDPI panel at 200 % is half a pixel), while fingers arrive window-NORMALIZED (0..1). +/// Only DIRECT touch devices are offered; an indirect trackpad already drives the mouse, +/// and forwarding both would double every tap. +fn overlay_pointer(event: &Event, window: &sdl3::video::Window) -> Option { + let (pw, ph) = window.size_in_pixels(); + let (lw, lh) = window.size(); + // Logical → physical. A zero-sized window (minimized) would divide by zero. + let sx = pw as f32 / lw.max(1) as f32; + let sy = ph as f32 / lh.max(1) as f32; + let button = |b: sdl3::mouse::MouseButton| match b { + sdl3::mouse::MouseButton::Left => Some(PointerButton::Primary), + sdl3::mouse::MouseButton::Right => Some(PointerButton::Secondary), + _ => None, + }; + Some(match event { + Event::MouseMotion { x, y, .. } => PointerInput::Move { + x: x * sx, + y: y * sy, + }, + Event::MouseButtonDown { + mouse_btn, x, y, .. + } => PointerInput::Down { + x: x * sx, + y: y * sy, + button: button(*mouse_btn)?, + }, + Event::MouseButtonUp { + mouse_btn, x, y, .. + } => PointerInput::Up { + x: x * sx, + y: y * sy, + button: button(*mouse_btn)?, + }, + Event::MouseWheel { + y, + mouse_x, + mouse_y, + .. + } => PointerInput::Wheel { + x: mouse_x * sx, + y: mouse_y * sy, + dy: *y, + }, + Event::FingerDown { touch_id, x, y, .. } if is_direct_touch(*touch_id) => { + PointerInput::Down { + x: x * pw as f32, + y: y * ph as f32, + button: PointerButton::Primary, + } + } + Event::FingerMotion { touch_id, x, y, .. } if is_direct_touch(*touch_id) => { + PointerInput::Move { + x: x * pw as f32, + y: y * ph as f32, + } + } + Event::FingerUp { touch_id, x, y, .. } if is_direct_touch(*touch_id) => PointerInput::Up { + x: x * pw as f32, + y: y * ph as f32, + button: PointerButton::Primary, + }, + // The pointer left the window mid-press: drop the press rather than let a release + // that never comes leave a widget armed forever. + Event::Window { + win_event: WindowEvent::MouseLeave, + .. + } => PointerInput::Cancel, + _ => return None, + }) +} + /// Is this SDL touch device a real touchscreen (DIRECT, window-relative coordinates)? /// Trackpads report INDIRECT and drive the mouse — their finger events must not be /// forwarded as touch passthrough. An unknown/invalid id (INVALID) reads as not-direct. @@ -2393,10 +2657,12 @@ const HINT_WITH_PAD: &str = "Click the stream to capture input · Ctrl+Alt+Shift /// /// The HDR tag is honest about the display path: `HDR` only when the swapchain actually /// runs HDR10 (`hdr_display`); a PQ stream tone-mapped onto an SDR surface (no HDR10 -/// format offered, HDR off in the compositor) shows `HDR→SDR`; and a PQ stream on the -/// software-decode lane (`hdr_untonemapped`) shows `HDR→SDR (raw)` — that lane has no -/// tone-map pass at all, so the washed-out picture is named for what it is rather than -/// passed off as a tone-map. +/// format offered, HDR off in the compositor) shows `HDR→SDR`; and a lane that shows PQ +/// with no tone-map pass at all (`hdr_untonemapped`) shows `HDR→SDR (raw)`, so a +/// washed-out picture is named for what it is rather than passed off as a tone-map. +/// ⚠ Since M8 no lane sets that flag — the software lane, which used to, now goes through +/// the same planar CSC pass as the hardware ones. The arm is kept for the next one that +/// does not; see `StreamState::hdr_untonemapped`. /// /// `profile` (the session's settings profile, `None` for the global defaults) closes the /// first line at every tier — the cheapest possible answer to "which profile am I on?" @@ -2539,6 +2805,70 @@ fn stats_text( text.push_str(&format!(" · dropped {}", s.mic_dropped)); } } + // Decode integrity (M4) — the native lane's answer to "was that stream actually + // clean?". Appended LAST and only when it has something to say, which keeps it + // additive for the stdout `stats:` line's parsers (a machine interface: every + // existing segment stays where it was) and keeps a healthy session's OSD exactly + // as quiet as it is today. + // + // "Something to say" deliberately includes a device with no `RESULT_STATUS` + // support (RADV), even with zero damage: there the counters cover the parser's + // half only, and a silent integrity line would read as a clean bill of health on + // the one configuration that cannot give one. Saying "no driver status" once a + // second is the whole lesson of `nb_queries = 0` — an unmeasured session must + // never look like a measured one. A lane that cannot report integrity at all (the + // CPU rung, PyroWave) prints nothing rather than zeros, for the same reason. + if detailed && s.decode_integrity { + let mut parts: Vec = Vec::new(); + if s.decode_damaged > 0 { + parts.push(format!("damaged {}", s.decode_damaged)); + } + if s.decode_refused > 0 { + // The decoder could not run at all — a different diagnosis from + // `damaged`, and the one that means the screen is frozen rather than + // occasionally glitching. Without it a rung refusing every AU printed + // no integrity line whatsoever. + parts.push(format!("refused {}", s.decode_refused)); + } + if s.decode_failed > 0 { + parts.push(format!("driver-failed {}", s.decode_failed)); + } + if s.concealed_run > 0 { + // The figure that says "and it has not recovered" — a run still climbing + // at the end of the window is a different problem from the same count of + // isolated damaged AUs. + parts.push(format!("run {}", s.concealed_run)); + } + if s.worst_concealed_run > s.concealed_run { + // Only when it says something the instantaneous run does not: this is + // sampled once a second and the worst moment lasts a handful of frames, + // so a window that reads `damaged 40` with no run at all is either forty + // isolated glitches or one 40-AU freeze that recovered — and until this + // figure was surfaced, nothing on the OSD could tell those apart. + // Session-cumulative, unlike everything before it on this line, which is + // why it is labelled rather than folded into `run`. + parts.push(format!("worst run {}", s.worst_concealed_run)); + } + if !s.decode_status_queries { + parts.push("no driver status".into()); + } + if !parts.is_empty() { + text.push_str(&format!("\nintegrity: {}", parts.join(" · "))); + } + } + // M8's software-HEVC-drop telemetry ("telemetry on frequency", §7 risk register): + // how many times in THIS process a session's codec ran out of decode rungs and had + // to reconnect as another codec. Process-cumulative, appended LAST and only when + // nonzero — additive for the stdout `stats:` line's parsers, and invisible on the + // overwhelming majority of runs where it never happens. + // + // On the line rather than only in the log because the question it answers is a rate + // across a session history ("did dropping software HEVC cost anyone a stream?"), and + // a warn nobody greps for cannot answer it. + let fallbacks = pf_client_core::session::codec_fallbacks(); + if detailed && fallbacks > 0 { + text.push_str(&format!("\ncodec_fallbacks {fallbacks}")); + } text } @@ -2786,13 +3116,27 @@ mod tests { lost_pct: 0.4, mic_sent: 0, mic_dropped: 0, - decoder: "vulkan", + // The decode-path tag as the session actually spells it since M10 — the + // ladder's rung names (`NativeRung::name`), not the deleted libavcodec + // ones. A fixture carrying a tag no client emits would let this test go on + // asserting the shape of a string that no longer exists. + decoder: "native-vulkan", // Old-host baseline (no reported target, 4:2:0 never asked): the tier // texts stay exactly what they were before the target/chroma elements. target_kbps: 0, auto_rate: false, chroma_444: false, asked_444: false, + // A lane with NO detectors (the CPU rung / PyroWave — and, before M10, + // any libavcodec rung): it cannot answer integrity questions at all, so + // every existing tier text below must be unchanged by M4's line. + decode_integrity: false, + decode_damaged: 0, + decode_failed: 0, + decode_refused: 0, + concealed_run: 0, + worst_concealed_run: 0, + decode_status_queries: false, }, PresentedWindow { e2e_p50_ms: 6.4, @@ -2820,7 +3164,10 @@ mod tests { assert!(normal.starts_with("1920×1080@120 · 120 fps · 24.3 Mb/s\n")); assert!(normal.contains("e2e 6.4/9.1 ms (p50/p95)")); assert!(normal.contains("lost 3 (0.4%)")); - assert!(!normal.contains("vulkan"), "decoder tag is Detailed-only"); + assert!( + !normal.contains("native-vulkan"), + "decoder tag is Detailed-only" + ); assert!(!normal.contains("decode"), "stage terms are Detailed-only"); let detailed = text(StatsVerbosity::Detailed); @@ -2902,10 +3249,147 @@ mod tests { assert!(!normal.contains("present:") && !normal.contains("pace")); } - /// The honest HDR badges: a PQ stream on the software-decode lane is shown WITHOUT - /// tone-mapping (that lane has no PQ→sRGB pass), so its badge must not read as the - /// hardware lane's `HDR→SDR` tone-map — and an HDR10 swapchain shows plain `HDR` - /// whatever the lane claims (a CPU frame forces the swapchain to SDR anyway). + /// The decode-integrity line (M4) — the whole point of which is that it can tell + /// three states apart that all look identical as "no complaints today": + /// + /// * a lane that CANNOT see corruption (the CPU rung, PyroWave — and every + /// libavcodec rung this program used to have: `nb_queries = 0`, no + /// `AV_FRAME_FLAG_CORRUPT`): silent, never zeros, because printing zeros would + /// assert a cleanliness nothing checked; + /// * a lane that looked and saw nothing: also silent, but it earned it; + /// * a lane that looked with only half its detectors — a device without + /// `queryResultStatusSupport` — which says so EVERY window, damage or not. + /// + /// Plus the shape a support engineer actually needs when there IS damage: how + /// much, whose fault (stream vs driver), and whether it ever recovered. + #[test] + fn the_integrity_line_distinguishes_clean_from_unmeasurable() { + let (base, p) = sample(); + let line = |s: &Stats| { + stats_text( + StatsVerbosity::Detailed, + "m", + s, + &p, + false, + false, + false, + None, + ) + .lines() + .find(|l| l.starts_with("integrity:")) + .map(str::to_string) + }; + + // A lane with no detectors cannot answer at all — nothing is printed. (The + // fixture is one, so this also pins that every other tier text is untouched by M4.) + assert_eq!(line(&base), None, "a lane with no detectors says nothing"); + + // The native rung on a device with full status support, decoding clean: + // also nothing — a healthy session's OSD stays exactly as quiet as it was. + let clean = Stats { + decode_integrity: true, + decode_status_queries: true, + ..base + }; + assert_eq!(line(&clean), None); + + // The same rung on RADV, where a RESULT_STATUS query would hang the VCN ring: + // clean counters, but only the parser's half was ever measured, and the line + // says so rather than implying a full bill of health. + let unmeasured = Stats { + decode_status_queries: false, + ..clean + }; + assert_eq!( + line(&unmeasured).as_deref(), + Some("integrity: no driver status") + ); + + // Damage, attributed: concealment is the stream's, `driver-failed` is the + // hardware's, and `run` answers "did it come back?". + let damaged = Stats { + decode_damaged: 4, + decode_failed: 2, + concealed_run: 3, + worst_concealed_run: 3, + ..clean + }; + assert_eq!( + line(&damaged).as_deref(), + Some("integrity: damaged 4 · driver-failed 2 · run 3") + ); + + // A lossy window the stream recovered from: the run is 0 and simply drops out. + let recovered = Stats { + decode_damaged: 4, + concealed_run: 0, + ..clean + }; + assert_eq!(line(&recovered).as_deref(), Some("integrity: damaged 4")); + + // …and the reason that window is not the whole story. `concealed_run` is an + // INSTANT sampled once a second; the freeze it missed lasted 40 AUs. Forty + // isolated glitches and one 40-AU freeze that recovered render identically + // without the session's worst run, and they are completely different bugs. + let recovered_hard = Stats { + worst_concealed_run: 40, + ..recovered + }; + assert_eq!( + line(&recovered_hard).as_deref(), + Some("integrity: damaged 4 · worst run 40") + ); + // It stays quiet whenever it adds nothing — a run still climbing at the end + // of the window already IS the worst one. + let still_broken = Stats { + concealed_run: 40, + worst_concealed_run: 40, + ..recovered + }; + assert_eq!( + line(&still_broken).as_deref(), + Some("integrity: damaged 4 · run 40") + ); + + // A rung that REFUSED every AU — a host renegotiating outside the decode + // envelope. The screen is frozen, nothing was concealed, no driver verdict + // exists, and before M4's review this printed no integrity line at all: a + // decoder that decoded nothing, reported as a clean session. + let refusing = Stats { + decode_refused: 60, + concealed_run: 60, + worst_concealed_run: 60, + ..clean + }; + assert_eq!( + line(&refusing).as_deref(), + Some("integrity: refused 60 · run 60") + ); + + // Never below Detailed — the tier ladder is a strict superset chain and this + // is diagnostic detail, not a glanceable number. + for tier in [ + StatsVerbosity::Compact, + StatsVerbosity::Normal, + StatsVerbosity::Off, + ] { + assert!( + !stats_text(tier, "m", &damaged, &p, false, false, false, None) + .contains("integrity:"), + "{tier:?}" + ); + } + } + + /// The honest HDR badges. ⚠ **The `(raw)` arm is currently unreachable in + /// production**: `hdr_untonemapped` is written `false` on every present arm since M8 + /// took the software lane through the same planar CSC pass (and therefore the same + /// tone-map) as the hardware lanes — see `StreamState::hdr_untonemapped`. This tests + /// the FORMATTER, not a state the client can be in, and it is kept for the same + /// reason the field is: the next lane that bypasses the pass must be able to say so + /// rather than quietly claim a tone-map, and this is the assertion that will still be + /// here when it does. #[test] fn hdr_badge_names_the_untonemapped_cpu_lane() { let (s, p) = sample(); diff --git a/crates/pf-presenter/src/vk/gpu.rs b/crates/pf-presenter/src/vk/gpu.rs index d371eb2e..393c9af4 100644 --- a/crates/pf-presenter/src/vk/gpu.rs +++ b/crates/pf-presenter/src/vk/gpu.rs @@ -1,4 +1,4 @@ -//! Low-level GPU helpers: memory allocation, image barriers, AVVkFrame sync, geometry. +//! Low-level GPU helpers: memory allocation, decode-image barriers, geometry. use super::Presenter; use anyhow::{Context as _, Result}; @@ -7,7 +7,7 @@ use ash::vk; impl Presenter { /// Wait the in-flight fence: OUR command buffers are done (staging, video image, /// old-swapchain images). Deliberately NOT `vkDeviceWaitIdle` — the pump thread - /// submits FFmpeg's Vulkan decode work concurrently, and wait-idle's external-sync + /// submits Vulkan decode work concurrently, and wait-idle's external-sync /// rule over every device queue would race it (observed as a resize crash). pub(super) fn quiesce_own(&mut self) -> Result<()> { // SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this @@ -79,43 +79,46 @@ pub(super) fn subresource_range() -> vk::ImageSubresourceRange { .layer_count(1) } -/// Acquire a Vulkan-Video frame's image from the decode queue family (EXCLUSIVE -/// sharing) and transition it for sampling. `src_qf == dst_qf` (or IGNORED/CONCURRENT) -/// degrades to a plain layout transition. The matching decode-side acquire happens in -/// FFmpeg, keyed off the queue_family we write back after submission. +/// Layout round-trip for one LAYER of a native (pf-vkdecode) decode image: decode +/// layout → SHADER_READ_ONLY before the CSC pass, and back after it. Layer-scoped — +/// the pool is an image array and the other layers are live DPB state that must not +/// be touched. No queue-family transfer: the pool is created CONCURRENT across the +/// graphics+decode families. /// -/// `srcStage` is FRAGMENT_SHADER — NOT TOP_OF_PIPE — deliberately: the submit waits the -/// frame's decode-complete timeline semaphore with `wait_dst_stage_mask = +/// Both scopes are FRAGMENT_SHADER, and that is load-bearing, not tidiness: the submit +/// waits the frame's decode-complete timeline with `wait_dst_stage_mask = /// FRAGMENT_SHADER`, and a semaphore wait only orders operations whose first sync scope -/// INTERSECTS that mask (the dependency-chain rule). With TOP_OF_PIPE the barrier's -/// layout transition (VIDEO_DECODE_DST/DPB → SHADER_READ_ONLY) formed no chain with the -/// wait and could execute while the decode queue was still writing the image. On RADV -/// that transition physically touches the image (metadata/decompression), so the race -/// showed as green/yellow block corruption exactly at freshly-decoded (damaged) regions -/// — the Steam Deck cursor-trail artifact. NVIDIA treats the transition as a no-op, -/// which is why the same code looked clean there. -pub(super) fn vkframe_acquire_barrier( +/// INTERSECTS that mask (the dependency-chain rule). With TOP_OF_PIPE the transition +/// formed no chain with the wait and could execute while the decode queue was still +/// writing the image. On RADV that transition physically touches the image +/// (metadata/decompression), so the race showed as green/yellow block corruption exactly +/// at freshly-decoded (damaged) regions — the Steam Deck cursor-trail artifact. NVIDIA +/// treats the transition as a no-op, which is why the same code looked clean there. +/// (Diagnosed on the FFmpeg-Vulkan rung's own acquire barrier, which is where the +/// TOP_OF_PIPE was; that rung is gone, the rule is not.) +pub(super) fn native_layer_barrier( device: &ash::Device, cmd: vk::CommandBuffer, image: vk::Image, - old_layout: vk::ImageLayout, - src_qf: u32, - dst_qf: u32, + layer: u32, + from: vk::ImageLayout, + to: vk::ImageLayout, ) { - let (src, dst) = if src_qf == dst_qf || src_qf == vk::QUEUE_FAMILY_IGNORED { - (vk::QUEUE_FAMILY_IGNORED, vk::QUEUE_FAMILY_IGNORED) - } else { - (src_qf, dst_qf) - }; let b = vk::ImageMemoryBarrier::default() .src_access_mask(vk::AccessFlags::empty()) .dst_access_mask(vk::AccessFlags::SHADER_READ) - .old_layout(old_layout) - .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) - .src_queue_family_index(src) - .dst_queue_family_index(dst) + .old_layout(from) + .new_layout(to) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .image(image) - .subresource_range(subresource_range()); + .subresource_range( + vk::ImageSubresourceRange::default() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .level_count(1) + .base_array_layer(layer) + .layer_count(1), + ); // SAFETY: per the Vulkan contract above - recorded into a command buffer this code owns and // has begun, referencing handles it also owns; nothing is submitted until the recording is // ended. diff --git a/crates/pf-presenter/src/vk/mod.rs b/crates/pf-presenter/src/vk/mod.rs index 31379352..814cb780 100644 --- a/crates/pf-presenter/src/vk/mod.rs +++ b/crates/pf-presenter/src/vk/mod.rs @@ -1,13 +1,21 @@ -//! The Vulkan presenter: swapchain + two frame paths into one device-local RGBA video +//! The Vulkan presenter: swapchain + several frame paths into one device-local RGBA video //! image, then a letterboxed `vkCmdBlitImage` composite. //! -//! * **Software** (`FrameInput::Cpu`): staging upload + `copy_buffer_to_image` (row -//! stride via `buffer_row_length`) — transfer-only, runs on every GPU. +//! * **Software** (`FrameInput::Cpu`): since M8 the CPU rung hands over tightly-packed +//! 8-bit I420 PLANES, not RGBA. They are staged into three R8 images +//! (`CpuPlanes`, no `buffer_row_length` — the planes carry no stride) and converted by +//! the PLANAR CSC render pass, the same pass and the same `csc_rows` coefficients the +//! hardware lanes use. That is what deleted this lane's second colour implementation +//! (swscale's BT.601 default) and its missing tone-map along with it. //! * **Hardware** (`FrameInput::Dmabuf`): the decoder's NV12 dmabuf imported per-plane -//! (`dmabuf.rs`) and converted by the CSC render pass (`csc.rs`) — zero-copy, gated on -//! the four import extensions at device creation; boxes without them (NVIDIA +//! (`dmabuf.rs`) and converted by the two-plane CSC render pass (`csc.rs`) — zero-copy, +//! gated on the four import extensions at device creation; boxes without them (NVIDIA //! proprietary by design) report `supports_dmabuf() == false` and the caller keeps the //! decoder on software. +//! * Plus the lanes that arrive already on this device: `NativeVk` (Vulkan Video — +//! pf-vkdecode on this very VkDevice), `D3d11` (Windows shared textures) and +//! `PyroWave` (three compute-decoded planes, through the same planar pass as the +//! software lane). //! //! Pacing: one frame in flight (the submit fence is waited before each record), MAILBOX //! when available, FIFO otherwise (`PUNKTFUNK_PRESENT_MODE=fifo|mailbox|immediate` @@ -23,7 +31,7 @@ use crate::overlay::SharedDevice; use ash::vk; #[cfg(target_os = "linux")] use pf_client_core::video::DmabufFrame; -use pf_client_core::video::{CpuFrame, VkVideoFrame}; +use pf_client_core::video::{CpuPlanarFrame, NativeVkFrame}; mod gpu; mod overlay_pipe; @@ -33,17 +41,24 @@ mod reconfig; mod resources; mod setup; -pub use setup::{list_adapters, PresentPref}; +pub use setup::{list_adapters, probe_decode, AdapterDecode, PresentPref}; + +/// The video-format probe behind [`AdapterDecode::formats`], re-exported so a caller +/// that prints the report does not need its own `pf-vkdecode` dependency (and cannot +/// end up printing a DIFFERENT crate version's idea of the flag names). +pub use pf_vkdecode::probe; /// One presenter iteration's video input. pub enum FrameInput<'a> { /// No new frame — re-composite the retained video image (expose/resize). Redraw, - Cpu(&'a CpuFrame), + /// Software-decoded I420 planes (M8): uploaded into three R8 images and converted by + /// the planar CSC pass, exactly like the hardware lanes' planes — so PQ tone-mapping, + /// range and matrix all come from the ONE shader, and the CPU lane stops being the + /// odd one out that arrived pre-converted (and pre-converted wrong). + Cpu(&'a CpuPlanarFrame), #[cfg(target_os = "linux")] Dmabuf(DmabufFrame), - /// FFmpeg Vulkan Video output — a VkImage already on THIS device (zero copy). - VkFrame(VkVideoFrame), /// D3D11VA hand-off — a shareable NT-handle texture to import (`d3d11.rs`). #[cfg(windows)] D3d11(pf_client_core::video::D3d11Frame), @@ -51,6 +66,12 @@ pub enum FrameInput<'a> { /// fence-complete, GENERAL layout (`pf_client_core::video_pyrowave`). #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] PyroWave(pf_client_core::video_pyrowave::PyroWavePlanarFrame), + /// Native Vulkan Video output (pf-vkdecode) — an NV12 image + plane views already + /// on THIS device: wait the frame's timeline pair on the submit, transition its + /// layer for sampling and BACK to its decode layout, CSC with the coded-vs-display + /// UV scale. Dropping the frame (after the sampling fence) releases the decoder's + /// slot via its guard. + NativeVk(NativeVkFrame), } /// The dmabuf/CSC machinery, present only when the device carries the import extensions. @@ -67,17 +88,17 @@ struct HwCtxWin { } /// A submitted hardware frame parked until the in-flight fence proves the GPU reads -/// done: imported dmabuf planes, or a Vulkan-Video frame (FFmpeg's image — we own only -/// the plane views; dropping the frame's guard releases the AVFrame back to the pool). +/// done: imported dmabuf planes, an imported D3D11 shared texture, or a native +/// Vulkan-Video frame. enum Retired { #[cfg(target_os = "linux")] Dmabuf(HwFrame), #[cfg(windows)] D3d11(crate::d3d11::HwFrame), - Vk { - frame: VkVideoFrame, - views: [vk::ImageView; 2], - }, + /// A native (pf-vkdecode) frame: image + views are the DECODER's — nothing to + /// destroy here; dropping the frame after the fence wait sends its release token, + /// which is what returns the decode slot (the release-after-fence contract). + NativeVk(NativeVkFrame), } /// The overlay composite: one premultiplied-alpha quad blended over the swapchain image @@ -97,8 +118,32 @@ struct OverlayPipe { framebuffers: Vec, } -/// The one video image (device-local RGBA the size of the decoded stream) + its staging. -/// `view`/`framebuffer` exist only on hw-capable devices (the CSC pass renders into it). +/// The software rung's plane images: three R8 pictures the CPU frame's tightly-packed +/// I420 is uploaded into, then sampled by the planar CSC pass. Sized to the LUMA picture +/// and its 4:2:0 chroma halves; rebuilt whenever the stream size changes. +/// +/// Owned by the presenter rather than parked in `Retired` like the imported hardware +/// frames: nothing outside this device ever refers to them, and the single in-flight +/// fence is waited before each record, so re-uploading into the same images is safe +/// without a ring. +struct CpuPlanes { + images: [vk::Image; 3], + memory: [vk::DeviceMemory; 3], + views: [vk::ImageView; 3], + /// Luma size; chroma is derived (`div_ceil(2)`), the same rule the frame uses. + width: u32, + height: u32, + /// True once the images have been transitioned out of UNDEFINED at least once — the + /// first upload must come from UNDEFINED (nothing to preserve), every later one from + /// SHADER_READ_ONLY_OPTIMAL (where the previous frame's CSC pass left them). + initialized: bool, +} + +/// The one video image: device-local RGBA the size of the decoded stream, the single +/// target every lane converges on before the letterboxed blit. `view` + `framebuffer` are +/// unconditional since M8 — the CSC pass renders into it on EVERY device, because the +/// software lane goes through the planar pass too and there is no lane left that writes +/// this image with a plain transfer. struct VideoImage { image: vk::Image, memory: vk::DeviceMemory, @@ -108,6 +153,8 @@ struct VideoImage { height: u32, } +/// The host-visible upload buffer the software rung's three planes are copied into before +/// the record step's `vkCmdCopyBufferToImage`s. Grows, never shrinks. struct Staging { buffer: vk::Buffer, memory: vk::DeviceMemory, @@ -136,24 +183,29 @@ pub struct Presenter { #[cfg(windows)] hw_win: Option, csc: CscPass, - /// The planar (3-plane) CSC variant for PyroWave frames; built only when the device - /// passed the pyrowave probe. - #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] - csc_planar: Option, - /// FFmpeg Vulkan Video decode handles — `None` when the stack can't do it. + /// The planar (3-plane) CSC variant. Unconditional since M8: the SOFTWARE rung + /// renders through it, and the software rung is the ladder's last one — a device that + /// failed the pyrowave probe (or a build without the feature) still has to be able to + /// show a picture. + csc_planar: CscPass, + /// The software rung's three uploaded plane images (Y/Cb/Cr, R8), rebuilt on a + /// stream-size change. `None` until the first CPU frame — a hardware session never + /// allocates them. + cpu_planes: Option, + /// The shared Vulkan device handles the decode lane runs on — `None` when the stack + /// can't do Vulkan Video at all. video_export: Option, /// The console-UI composite quad (§6.1's presenter half). overlay_pipe: OverlayPipe, - /// The submitted hardware frame (dmabuf plane images + guard, or a Vulkan-Video - /// frame + our plane views): its GPU reads end with the in-flight fence, so it's - /// destroyed right after the next fence wait. + /// The submitted hardware frame (dmabuf plane images + guard, an imported D3D11 + /// texture, or a native Vulkan-Video frame): its GPU reads end with the in-flight + /// fence, so it's released right after the next fence wait. retired_hw: Option, - /// External-sync lock over this device's queues, shared with FFmpeg (via - /// [`pf_client_core::video::VulkanDecodeDevice::queue_lock`] → its - /// `lock_queue`/`unlock_queue` callbacks) and the Skia overlay: FFmpeg preps on the - /// SAME graphics queue from the pump thread, so every `vkQueueSubmit`/ - /// `vkQueuePresentKHR`/`vkQueueWaitIdle`/`vkDeviceWaitIdle` here must hold it — - /// the unsynchronized overlap was an intermittent `VK_ERROR_DEVICE_LOST`. + /// External-sync lock over this device's queues, shared with the DECODE lane (via + /// [`pf_client_core::video::VulkanDecodeDevice::queue_lock`]) and the Skia overlay: + /// the decoder submits on the SAME graphics queue from the pump thread, so every + /// `vkQueueSubmit`/`vkQueuePresentKHR`/`vkQueueWaitIdle`/`vkDeviceWaitIdle` here must + /// hold it — the unsynchronized overlap was an intermittent `VK_ERROR_DEVICE_LOST`. queue_lock: std::sync::Arc, format: vk::SurfaceFormatKHR, /// The surface's HDR10/ST.2084 pairing, when the stack offers one. @@ -217,15 +269,15 @@ impl Presenter { self.hw_win.is_some() } - /// The FFmpeg Vulkan Video decode handle bundle — `None` when this stack can't - /// (device < 1.3, missing video extensions/queue/features). The decoder chain - /// falls back to VAAPI/software then. + /// The Vulkan Video decode handle bundle — `None` when this stack can't + /// (device < 1.3, missing video extensions/queue/features). The decoder ladder + /// falls through to the platform rung / software then. pub fn vulkan_decode(&self) -> Option { self.video_export.clone() } /// Full device idle — TEARDOWN ONLY, and only after the session pump thread has - /// been joined (it submits FFmpeg decode work; wait-idle's external-sync rule + /// been joined (it submits decode work; wait-idle's external-sync rule /// covers every queue on the device). Mid-session code uses the fence quiesce. /// The queue lock is held as cheap insurance against a straggling submitter. pub fn wait_idle(&self) { @@ -375,8 +427,8 @@ impl Drop for Presenter { #[cfg(target_os = "linux")] self.hw.take(); self.csc.destroy(&self.device); - #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] - if let Some(p) = &self.csc_planar { + self.csc_planar.destroy(&self.device); + if let Some(p) = self.cpu_planes.take() { p.destroy(&self.device); } self.overlay_pipe.destroy(&self.device); diff --git a/crates/pf-presenter/src/vk/present.rs b/crates/pf-presenter/src/vk/present.rs index a8b5bd12..00b517ed 100644 --- a/crates/pf-presenter/src/vk/present.rs +++ b/crates/pf-presenter/src/vk/present.rs @@ -6,10 +6,10 @@ use crate::csc::csc_rows; #[cfg(target_os = "linux")] use crate::dmabuf::{self, HwFrame}; use crate::overlay::OverlayFrame; -use anyhow::{bail, Context as _, Result}; +use anyhow::{Context as _, Result}; use ash::vk; use ash::vk::Handle as _; -use pf_client_core::video::VkVideoFrame; +use pf_client_core::video::{NativeVkFrame, NativeVkLayout, RawVkFormat}; impl Presenter { /// Present one frame: route `input` into the video image (staging upload or dmabuf @@ -31,43 +31,24 @@ impl Presenter { // offers HDR10 — otherwise PQ stays on the SDR swapchain and the CSC shader // tonemaps (mode 1). // - // CPU frames NEVER take the HDR10 surface: software decode uploads swscale RGBA with - // no CSC/tonemap pass, so on a mode-0 swapchain that sRGB-encoded content would be - // composed as PQ — the field-reported psychedelic cyan/magenta picture (reproduced - // 2026-07-21: Fedora-class client, no hw HEVC decode, GNOME/Mesa offering HDR10 even - // on an SDR desktop). On the SDR swapchain the same frames are merely untonemapped - // (washed out) — wrong in the known, benign way until the CPU lane grows a real - // PQ→sRGB pass. + // The CPU lane used to be the exception here: it arrived as swscale RGBA with no + // CSC/tonemap pass at all, so it was pinned to the SDR swapchain (a mode-0 + // composite of sRGB content as PQ is the field-reported psychedelic cyan/magenta + // picture, reproduced 2026-07-21 on a Fedora-class client with no hw HEVC decode + // and GNOME/Mesa offering HDR10 on an SDR desktop) and a PQ stream simply came out + // washed out. Since M8 it goes through the SAME planar CSC pass as every hardware + // lane, so it gets the same answer as every hardware lane: PQ where the surface + // offers HDR10, the shader's mode-1 tonemap where it does not. let frame_pq = match &input { FrameInput::Redraw => None, - FrameInput::Cpu(f) => { - // The swapchain answer stays `false` (above) — but a PQ stream on this - // lane is then shown RAW: no PQ→sRGB pass exists here (the CSC mode-1 - // tonemap is hardware-lane only; CPU frames are a straight RGBA upload), - // so the picture is washed out and the pq-downgrade warn below never - // fires. Say so once, or the only trace is an OSD badge. (A process-once - // latch, same idiom as the decoders' first-frame layout dumps — the - // condition is a property of the lane, not of one Presenter.) - if f.color.is_pq() { - use std::sync::atomic::{AtomicBool, Ordering}; - static WARNED: AtomicBool = AtomicBool::new(false); - if !WARNED.swap(true, Ordering::Relaxed) { - tracing::warn!( - "HDR10 (PQ) stream on the software-decode lane — it has no \ - PQ→sRGB pass, so the picture is shown untonemapped (washed \ - out). Hardware decode restores correct colour." - ); - } - } - Some(false) - } + FrameInput::Cpu(f) => Some(f.color.is_pq()), #[cfg(target_os = "linux")] FrameInput::Dmabuf(d) => Some(d.color.is_pq()), - FrameInput::VkFrame(v) => Some(v.color.is_pq()), #[cfg(windows)] FrameInput::D3d11(d) => Some(d.color.is_pq()), #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] FrameInput::PyroWave(f) => Some(f.color.is_pq()), + FrameInput::NativeVk(f) => Some(f.color.is_pq()), }; if let Some(pq) = frame_pq { // A PQ stream we can only tone-map (no HDR10 surface) is the silent failure behind @@ -95,9 +76,13 @@ impl Presenter { let mut hw_frame: Option = None; #[cfg(windows)] let mut win_frame: Option = None; - let mut vk_frame: Option<(VkVideoFrame, [vk::ImageView; 2])> = None; + let mut native_frame: Option = None; #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] let mut pyro_frame: Option = None; + // A real frame that is NOT a CPU one — the signal that the software rung's plane + // images are dead weight (see below). `Redraw` is deliberately not one: it + // re-blits the retained video image and says nothing about which lane is decoding. + let mut hw_lane = false; let cpu_frame = match input { FrameInput::Redraw => None, FrameInput::Cpu(f) => Some(f), @@ -108,6 +93,7 @@ impl Presenter { .as_ref() .context("hardware frame without dmabuf support")?; hw_frame = Some(dmabuf::import(&self.device, &hw.ext_mem_fd, d)?); + hw_lane = true; None } #[cfg(windows)] @@ -117,16 +103,20 @@ impl Presenter { .as_ref() .context("D3D11 frame without win32 import support")?; win_frame = Some(crate::d3d11::import(&self.device, &hw.ext_mem_win32, &d)?); - None - } - FrameInput::VkFrame(v) => { - let views = self.vkframe_plane_views(&v)?; - vk_frame = Some((v, views)); + hw_lane = true; None } #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] FrameInput::PyroWave(f) => { pyro_frame = Some(f); + hw_lane = true; + None + } + // Same device, and the decoder already made the per-plane views — no + // import, no view creation, nothing that can fail out here. + FrameInput::NativeVk(f) => { + native_frame = Some(f); + hw_lane = true; None } }; @@ -145,10 +135,22 @@ impl Presenter { if let Some(old) = self.retired_hw.take() { old.destroy(&self.device); } - - if let Some(f) = cpu_frame { - self.stage_frame(f)?; + // A hardware frame after a software one: the plane images are ~12 MB at 4K and + // nothing will sample them again. This is not hypothetical — M8's codec fallback + // starts a NEW session on this same presenter, and that one can be hardware where + // the one that raised it was not. The fence wait above is what makes them + // unreferenced, so this is the first safe moment. + if hw_lane { + if let Some(p) = self.cpu_planes.take() { + tracing::debug!("freeing the software rung's plane images (hardware lane)"); + p.destroy(&self.device); + } } + + let cpu_offsets = match cpu_frame { + Some(f) => Some(self.stage_frame(f)?), + None => None, + }; #[cfg(target_os = "linux")] if let Some(f) = &hw_frame { if self @@ -174,7 +176,7 @@ impl Presenter { tracing::info!(width = f.width, height = f.height, "video image (re)built"); } } - if let Some((f, views)) = &vk_frame { + if let Some(f) = &native_frame { if self .video .as_ref() @@ -183,7 +185,28 @@ impl Presenter { self.rebuild_video_image(f.width, f.height)?; tracing::info!(width = f.width, height = f.height, "video image (re)built"); } - self.csc.bind_planes(&self.device, views[0], views[1]); + // The UV-scale crop below assumes an origin crop (punktfunk hosts emit + // nothing else); a nonzero origin would display the wrong window — say so + // rather than be silently wrong. + if f.crop_x != 0 || f.crop_y != 0 { + use std::sync::atomic::{AtomicBool, Ordering}; + static WARNED: AtomicBool = AtomicBool::new(false); + if !WARNED.swap(true, Ordering::Relaxed) { + tracing::warn!( + crop_x = f.crop_x, + crop_y = f.crop_y, + "native frame carries a non-origin conformance crop — the UV \ + scale only handles origin crops; picture offset expected" + ); + } + } + // Decoder-owned plane views (R8 + R8G8); the fence wait above is what + // makes the descriptor set rebindable. + self.csc.bind_planes( + &self.device, + vk::ImageView::from_raw(f.plane_views[0]), + vk::ImageView::from_raw(f.plane_views[1]), + ); } #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] if let Some(f) = &pyro_frame { @@ -195,11 +218,26 @@ impl Presenter { self.rebuild_video_image(f.width, f.height)?; tracing::info!(width = f.width, height = f.height, "video image (re)built"); } - let planar = self - .csc_planar + // The decode leaves them in GENERAL — the software rung's uploaded planes are + // the other producer for this pass and arrive in SHADER_READ_ONLY_OPTIMAL. + self.csc_planar.bind_planes_planar( + &self.device, + f.views.map(vk::ImageView::from_raw), + vk::ImageLayout::GENERAL, + ); + } + if cpu_offsets.is_some() { + // Safe while nothing in flight references the set — the fence wait above. + let views = self + .cpu_planes .as_ref() - .context("PyroWave frame but the device failed the pyrowave probe")?; - planar.bind_planes_planar(&self.device, f.views.map(vk::ImageView::from_raw)); + .context("software frame without plane images")? + .views; + self.csc_planar.bind_planes_planar( + &self.device, + views, + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + ); } if let Some(o) = overlay { // Point the composite at this overlay image (same fence-wait safety). @@ -324,31 +362,51 @@ impl Presenter { ); } - // Vulkan-Video frame: the decoded image is already on THIS device. Read the - // live sync state under the frames lock (held through submission — the - // AVVulkanFramesContext contract), acquire from the decode queue family, - // then the same CSC pass. - let mut vk_sync: Option = None; - if let (Some((f, _)), Some(v)) = (&vk_frame, &self.video) { - let sync = lock_vkframe(f); - vkframe_acquire_barrier( + // Native (pf-vkdecode) frame: the decoded image is already on THIS device, + // and the sync facts ride the frame itself (no frames lock — the decoder + // stamped layout/semaphore/value at delivery and nothing mutates them; the + // FFmpeg-Vulkan rung that shared this path until M10 needed one because + // libavcodec mutated the state per submission). + // Transition the picture's LAYER for sampling, run the same CSC pass with + // the coded-vs-display UV scale (the 1088-row lesson), then transition BACK + // to the decode layout the frame names — and the submit below signals the + // image's timeline at `value + 1` when these reads/restores complete, which + // the decoder (told via the release token) waits before that image's next + // decode use: the layout round-trip is ORDERED against decode, not raced. + // The pool images are created CONCURRENT across the graphics+decode + // families, so these are plain layout transitions — no queue-family + // ownership transfer. + let mut native_wait: Option<(vk::Semaphore, u64)> = None; + if let (Some(f), Some(v)) = (&native_frame, &self.video) { + let image = vk::Image::from_raw(f.image); + let decode_layout = match f.layout { + NativeVkLayout::DecodeDst => vk::ImageLayout::VIDEO_DECODE_DST_KHR, + NativeVkLayout::DecodeDpb => vk::ImageLayout::VIDEO_DECODE_DPB_KHR, + }; + native_layer_barrier( &self.device, self.cmd_buf, - vk::Image::from_raw(sync.image), - vk::ImageLayout::from_raw(sync.layout), - sync.queue_family, - self.qfi, + image, + f.layer, + decode_layout, + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, ); let extent = vk::Extent2D { width: v.width, height: v.height, }; - let ten_bit = - f.vk_format == vk::Format::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16.as_raw(); - // The one path that samples a surface BIGGER than the picture: FFmpeg's - // pool is the coded size (1080 → 1088 rows). Scale the UVs to the visible - // crop or the alignment padding — the last picture row, replicated by the - // encoder — is stretched into the bottom of the image. + // Bit depth and MSB packing come from the PICTURE's own format, which + // the decoder stamps on every frame — H.264 and HEVC Main deliver + // NV12 (8-bit), Main 10 delivers P010 (10 significant bits in the + // MSBs of 16), RExt delivers the two-plane 4:4:4 pair — and which can + // change mid-stream when the host renegotiates. Nothing here assumes + // a codec: 8-bit transfer/range math over a P010 surface decodes + // correctly and displays wrong, the plausible-looking-and-wrong class + // this program refuses. Chroma siting needs no decision — the CSC + // shader's quarter-texel 4:2:0 correction self-disables when the + // chroma plane is full width, so the 4:4:4 formats are already right. + // Colour rides the frame (BT.709-limited SDR default). + let (depth, msb_packed) = csc_depth_packing_or_8bit(f.vk_format); self.record_csc( v.framebuffer, extent, @@ -357,10 +415,18 @@ impl Presenter { f.height as f32 / f.coded_height as f32, ], f.color, - if ten_bit { 10 } else { 8 }, - ten_bit, + depth, + msb_packed, ); - vk_sync = Some(sync); + native_layer_barrier( + &self.device, + self.cmd_buf, + image, + f.layer, + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + decode_layout, + ); + native_wait = Some((vk::Semaphore::from_raw(f.semaphore), f.semaphore_value)); } // PyroWave frame: the planes are already on THIS device, decode @@ -372,40 +438,80 @@ impl Presenter { width: v.width, height: v.height, }; - self.record_csc_planar(v.framebuffer, extent, f.color); + // An HDR (PQ) pyrowave session carries P010-style 10-bit studio codes + // MSB-packed into 16-bit planes (design/pyrowave-444-hdr.md §2.2) — same + // sampling scale as the P010 path; SDR sessions are plain 8-bit BT.709 + // limited. Depth follows THIS codec's colour contract (negotiation + // couples 10-bit ⟺ PQ for it), which is why it is decided here and not + // inside the shared record. + let (depth, msb_packed) = if f.color.is_pq() { + (10, true) + } else { + (8, false) + }; + self.record_csc_planar(v.framebuffer, extent, f.color, depth, msb_packed); } - // New frame: staging → video image (stride carried by buffer_row_length). - if let (Some(f), Some(v), Some(s)) = (cpu_frame, &self.video, &self.staging) { - barrier( - &self.device, - self.cmd_buf, - v.image, - vk::ImageLayout::UNDEFINED, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - ); - let region = vk::BufferImageCopy::default() - .buffer_row_length((f.stride / 4) as u32) - .image_subresource(subresource_layers()) - .image_extent(vk::Extent3D { - width: v.width, - height: v.height, - depth: 1, - }); - self.device.cmd_copy_buffer_to_image( - self.cmd_buf, - s.buffer, - v.image, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - &[region], - ); - barrier( - &self.device, - self.cmd_buf, - v.image, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - vk::ImageLayout::TRANSFER_SRC_OPTIMAL, - ); + // Software frame (M8): staging → three R8 plane images → the planar CSC pass, + // the same pass and the same `csc_rows` coefficients the hardware lanes use. + // The planes are tightly packed by construction (`CpuPlanarFrame`), so no + // `buffer_row_length` is needed and none is set — a stride here would be a + // second place for the layout to be wrong. + if let (Some(f), Some(offsets), Some(v), Some(s), Some(p)) = ( + cpu_frame, + cpu_offsets, + &self.video, + &self.staging, + &self.cpu_planes, + ) { + // First upload into freshly built images comes from UNDEFINED (there is + // nothing to preserve); every later one from where the previous frame's + // CSC pass left them. + let from = if p.initialized { + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL + } else { + vk::ImageLayout::UNDEFINED + }; + for (i, offset) in offsets.iter().enumerate() { + let (w, h) = f.plane_dims(i); + barrier( + &self.device, + self.cmd_buf, + p.images[i], + from, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + ); + let region = vk::BufferImageCopy::default() + .buffer_offset(*offset as u64) + .image_subresource(subresource_layers()) + .image_extent(vk::Extent3D { + width: w, + height: h, + depth: 1, + }); + self.device.cmd_copy_buffer_to_image( + self.cmd_buf, + s.buffer, + p.images[i], + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + &[region], + ); + barrier( + &self.device, + self.cmd_buf, + p.images[i], + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + ); + } + let extent = vk::Extent2D { + width: v.width, + height: v.height, + }; + // Always 8-bit, no MSB packing — R8 planes, whatever the stream signals. + // A PQ AV1 stream on this rung therefore tone-maps through the shader's + // mode 1 like every other lane, instead of being read as 10-bit. + self.record_csc_planar(v.framebuffer, extent, f.color, 8, false); } // Swapchain image: discard old content, clear to black (the letterbox bars), @@ -524,23 +630,38 @@ impl Presenter { ); } self.device.end_command_buffer(self.cmd_buf)?; + // The plane images now have content and a real layout, so the NEXT upload + // must transition from SHADER_READ_ONLY_OPTIMAL rather than discard them from + // UNDEFINED. Recorded, not submitted — but the only path from here to another + // record goes through this command buffer, and a submit failure below tears + // the presenter down rather than re-recording. + if let Some(p) = self.cpu_planes.as_mut() { + p.initialized = true; + } let render_sem = self.render_sems[index as usize]; let cmd_bufs = [self.cmd_buf]; let mut wait_sems = vec![self.acquire_sem]; let mut wait_stages = vec![vk::PipelineStageFlags::TRANSFER]; let mut signal_sems = vec![render_sem]; - // The Vulkan-Video frame's timeline semaphore: wait for the decoder's value, - // signal value+1 when our reads are done (FFmpeg's per-submission contract). + // The decoded frame's timeline semaphore. let mut wait_values = vec![0u64]; let mut signal_values = vec![0u64]; - if let Some(sync) = &vk_sync { - let sem = vk::Semaphore::from_raw(sync.semaphore); - wait_sems.push(sem); + // Wait the decode-complete value at FRAGMENT_SHADER (chaining with the layer + // barrier — the same dependency-chain rule `native_layer_barrier` documents), + // and SIGNAL `value + 1` when our reads and the layout restore are done. The + // decoder learns of the enqueued signal through the release token + // (`mark_presented`) and waits it before the image's next decode use; + // per-IMAGE timelines make the value spaces private, so this cannot collide + // with any other image's counter. (This is the same write-back contract + // libavcodec's `AVVkFrame` demanded — that rung is gone, the contract is + // not.) + if let Some((sem, value)) = &native_wait { + wait_sems.push(*sem); wait_stages.push(vk::PipelineStageFlags::FRAGMENT_SHADER); - wait_values.push(sync.sem_value); - signal_sems.push(sem); - signal_values.push(sync.sem_value + 1); + wait_values.push(*value); + signal_sems.push(*sem); + signal_values.push(*value + 1); } let mut timeline = vk::TimelineSemaphoreSubmitInfo::default() .wait_semaphore_values(&wait_values) @@ -550,7 +671,7 @@ impl Presenter { .wait_dst_stage_mask(&wait_stages) .command_buffers(&cmd_bufs) .signal_semaphores(&signal_sems); - if vk_sync.is_some() { + if native_wait.is_some() { submit = submit.push_next(&mut timeline); } // D3D11 frame: bracket the submit in the shared texture's keyed mutex, key 0 @@ -581,32 +702,16 @@ impl Presenter { } } let submitted = { - // Queue external sync vs the pump's FFmpeg submits (see `queue_lock`). + // Queue external sync vs the pump's decode submits (see `queue_lock`). let _q = self.queue_lock.guard(); self.device.queue_submit(self.queue, &[submit], self.fence) }; - // Write the new sync state back and release the frames lock REGARDLESS of - // the submit outcome (an abandoned lock would wedge the decoder). - if let Some(sync) = vk_sync.take() { - let ok = submitted.is_ok(); - unlock_vkframe( - vk_frame - .as_ref() - .map(|(f, _)| f) - .expect("vk_sync implies vk_frame"), - &sync, - ok, - self.qfi, - ); - } submitted?; self.submitted = true; // The hw frame is on the GPU now — park it until the fence proves the reads - // done (destroyed at the next present's fence wait, or in Drop). At most one - // of hw_frame/vk_frame is set (they route from the same `input`). - self.retired_hw = vk_frame - .take() - .map(|(frame, views)| Retired::Vk { frame, views }); + // done (released at the next present's fence wait, or in Drop). At most one of + // hw_frame/win_frame/native_frame is set (they route from the same `input`). + self.retired_hw = None; #[cfg(target_os = "linux")] if let Some(f) = hw_frame.take() { self.retired_hw = Some(Retired::Dmabuf(f)); @@ -615,6 +720,16 @@ impl Presenter { if let Some(f) = win_frame.take() { self.retired_hw = Some(Retired::D3d11(f)); } + // Native frame: the submit above enqueued our `value + 1` signal — mark + // the token so the decoder waits that write-back before reusing the + // image (a failed submit skipped this whole block, leaving the token + // unmarked: no phantom signal is ever promised). Then park until the + // fence proves the sampling reads done — the drop THEN sends the + // release token (never at record time). + if let Some(mut f) = native_frame.take() { + f.guard.mark_presented(); + self.retired_hw = Some(Retired::NativeVk(f)); + } let swapchains = [self.swapchain]; let indices = [index]; @@ -754,18 +869,22 @@ impl Presenter { } } - /// [`record_csc`] over the planar (PyroWave) pass — always 8-bit, no MSB packing. - #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] + /// [`record_csc`] over the planar (3-plane) pass — the PyroWave decode output and, + /// since M8, the software rung's uploaded I420. + /// + /// `depth`/`msb_packed` are the PRODUCER's, never inferred from the colour: pyrowave + /// couples 10-bit to PQ by negotiation, the software rung is 8-bit whatever it is + /// showing, and reading PQ as "therefore 10-bit MSB-packed" over an 8-bit plane + /// samples at a quarter scale — decoded correctly, displayed wrong. unsafe fn record_csc_planar( &self, framebuffer: vk::Framebuffer, extent: vk::Extent2D, color: pf_client_core::video::ColorDesc, + depth: u8, + msb_packed: bool, ) { - // The planar pass exists whenever a PyroWave frame reached us (checked at bind). - let Some(planar) = self.csc_planar.as_ref() else { - return; - }; + let planar = &self.csc_planar; // SAFETY: per the Vulkan contract above - recorded into a command buffer this code owns // and has begun, referencing handles it also owns; nothing is submitted until the // recording is ended. @@ -814,15 +933,6 @@ impl Presenter { &[planar.desc_set], &[], ); - // An HDR (PQ) pyrowave session carries P010-style 10-bit studio codes MSB-packed - // into 16-bit planes (design/pyrowave-444-hdr.md §2.2) — same sampling scale as - // the P010 path; SDR sessions are plain 8-bit BT.709 limited. Depth follows the - // colour contract (negotiation couples 10-bit ⟺ PQ for this codec). - let (depth, msb_packed) = if color.is_pq() { - (10, true) - } else { - (8, false) - }; let rows = csc_rows(color, depth, msb_packed); // Mode 1 = PQ→SDR tonemap (PQ stream without an HDR10 surface); mode 0 passes // the transfer through — identical to the NV12 arm above. @@ -851,90 +961,67 @@ impl Presenter { self.device.cmd_end_render_pass(self.cmd_buf); } } - - /// Per-plane views over a Vulkan-Video frame's multiplanar image — the CSC pass's - /// exact sampling contract (the frames pool was created MUTABLE_FORMAT for this). - /// See [`vkframe_plane_formats`] for the accepted pool formats. - fn vkframe_plane_views(&self, f: &VkVideoFrame) -> Result<[vk::ImageView; 2]> { - let Some((luma_fmt, chroma_fmt)) = vkframe_plane_formats(f.vk_format) else { - bail!( - "Vulkan-Video pool format {} unsupported (expected 2-plane 4:2:0 or 4:4:4, \ - 8/10-bit — 3-plane layouts need a third CSC binding)", - f.vk_format - ); - }; - // img[0] is creation-constant (only the sync fields need the frames lock). - let image = vk::Image::from_raw( - // SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned - // by this type and live for the call, and every builder struct is a local that - // outlives it. - unsafe { (*(f.vkframe as *const pf_ffvk::AVVkFrame)).img[0] } as u64, - ); - let make = |aspect: vk::ImageAspectFlags, format: vk::Format| { - // SAFETY: per the Vulkan contract above - a create/allocate call on the live device, - // over builder structs that are locals outliving the call; the handle it returns is - // owned by the value being built here. - unsafe { - self.device.create_image_view( - &vk::ImageViewCreateInfo::default() - .image(image) - .view_type(vk::ImageViewType::TYPE_2D) - .format(format) - .subresource_range( - vk::ImageSubresourceRange::default() - .aspect_mask(aspect) - .level_count(1) - .layer_count(1), - ), - None, - ) - } - .context("vk-frame plane view") - }; - let luma = make(vk::ImageAspectFlags::PLANE_0, luma_fmt)?; - let chroma = match make(vk::ImageAspectFlags::PLANE_1, chroma_fmt) { - Ok(v) => v, - Err(e) => { - // SAFETY: per the Vulkan contract above - this destroys objects this type owns, - // and the GPU is known idle for them (the fence/queue-wait on the path here, or - // the swapchain being retired), which is the obligation that makes a destroy sound - // rather than the handle merely being non-null. - unsafe { self.device.destroy_image_view(luma, None) }; - return Err(e); - } - }; - Ok([luma, chroma]) - } } -/// The (luma, chroma) per-plane view formats for a Vulkan-Video pool format, or `None` -/// when this presenter can't sample it (the caller bails; the decoder demotes to -/// software — never a black screen). +/// The CSC pass's `(bit depth, MSB-packed)` pair for a decoded picture's `VkFormat`, +/// or `None` for a format this presenter has no colour math for. /// -/// The decision table IS the desktop 4:4:4 display contract, so it's a pure function -/// with a test pinning it: -/// - 2-plane 4:2:0, 8-bit (NV12-layout) and 10-bit (P010/X6) — the classic pair. -/// - 2-plane 4:4:4, 8- and 10-bit — what NVIDIA's Vulkan Video reports for HEVC RExt -/// full-chroma decode (semi-planar, like all NVDEC output). The CSC shader already -/// handles the full-size chroma plane (its 4:2:0 siting correction self-disables when -/// the plane widths match), so accepting the format here is all hardware 4:4:4 needs. -/// - 3-plane 4:4:4 stays rejected: the CSC pass samples exactly two planes (luma + -/// interleaved chroma); a triplanar pool needs a third binding + shader variant. No -/// supported driver reports it for HEVC decode today — revisit when one does. -fn vkframe_plane_formats(raw: i32) -> Option<(vk::Format, vk::Format)> { - let eight = (vk::Format::R8_UNORM, vk::Format::R8G8_UNORM); - let ten = ( - vk::Format::R10X6_UNORM_PACK16, - vk::Format::R10X6G10X6_UNORM_2PACK16, - ); +/// This is the whole of what the shader needs to know about the picture format, and +/// it is a property of the STREAM, never of the codec — the frame carries the real +/// format ([`NativeVkFrame::vk_format`], from pf-vkdecode) and it is read here: +/// - 8-bit two-plane (NV12-layout and its 4:4:4 sibling) → depth 8, unpacked. +/// - 10-bit two-plane `3PACK16` (P010-layout and its 4:4:4 sibling) → depth 10, +/// MSB-packed: 10 significant bits live in the MSBs of 16, so a UNORM16 sample +/// reads `code·64/65535` and `csc_rows` folds in the `65535/65472` correction. +/// Rendering those with 8-bit math is not a subtle error — range expansion and the +/// PQ curve both land wrong — but it is a silent one, which is why the depth is +/// derived rather than assumed. +/// +/// Chroma subsampling deliberately does NOT appear: the CSC shader samples both +/// planes in normalized coordinates and self-disables its quarter-texel 4:2:0 siting +/// correction when the chroma plane is full width, so 4:2:0 and 4:4:4 differ only in +/// what the sampler reads. Pure, with a test pinning the table. +fn csc_depth_packing(raw: RawVkFormat) -> Option<(u8, bool)> { [ - (vk::Format::G8_B8R8_2PLANE_420_UNORM, eight), - (vk::Format::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16, ten), - (vk::Format::G8_B8R8_2PLANE_444_UNORM, eight), - (vk::Format::G10X6_B10X6R10X6_2PLANE_444_UNORM_3PACK16, ten), + (vk::Format::G8_B8R8_2PLANE_420_UNORM, (8, false)), + (vk::Format::G8_B8R8_2PLANE_444_UNORM, (8, false)), + ( + vk::Format::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16, + (10, true), + ), + ( + vk::Format::G10X6_B10X6R10X6_2PLANE_444_UNORM_3PACK16, + (10, true), + ), ] .into_iter() - .find_map(|(f, planes)| (f.as_raw() == raw).then_some(planes)) + .find_map(|(f, dp)| (f.as_raw() == raw.0).then_some(dp)) +} + +/// [`csc_depth_packing`] with the 8-bit fallback for a format the decode lane should +/// never hand us — pf-vkdecode refuses a picture format it has no plane mapping for +/// before a session exists. Unreachable is not impossible, so it is said once PER +/// FORMAT rather than silently guessed forever. +/// +/// Per format, not once per process: a session can renegotiate its picture format +/// mid-stream (the ABR/HDR flips this program exists around), so a single latch +/// would let the first unmapped format silence every later, DIFFERENT one — and the +/// second one is the interesting one, because the pair says the gap is systematic. +fn csc_depth_packing_or_8bit(raw: RawVkFormat) -> (u8, bool) { + csc_depth_packing(raw).unwrap_or_else(|| { + use std::sync::Mutex; + static WARNED: Mutex> = Mutex::new(Vec::new()); + let mut seen = WARNED.lock().unwrap_or_else(|e| e.into_inner()); + if !seen.contains(&raw) { + seen.push(raw); + tracing::warn!( + vk_format = raw.0, + "decoded picture in a format the CSC pass has no depth mapping for — \ + rendering it as 8-bit, which is wrong if it is not" + ); + } + (8, false) + }) } /// Flatten the 3×vec4 rows for the push-constant block. @@ -943,95 +1030,65 @@ fn bytemuck_rows(rows: &[[f32; 4]; 3]) -> &[f32] { unsafe { std::slice::from_raw_parts(rows.as_ptr().cast::(), 12) } } -/// The live sync state of an `AVVkFrame`, snapshotted under the frames lock. -struct VkFrameSync { - image: u64, - semaphore: u64, - sem_value: u64, - layout: i32, - queue_family: u32, -} - -/// Lock the frame and read its live sync state (the presenter's submit must wait -/// `sem_value` and signal `sem_value + 1`). The lock is held until [`unlock_vkframe`]. -// bindgen's enum repr is target-dependent (u32 Linux/clang, i32 MSVC) — the layout cast -// is required on one platform and a no-op on the other. -#[allow(clippy::unnecessary_cast)] -fn lock_vkframe(f: &VkVideoFrame) -> VkFrameSync { - // SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this type - // and live for the call, and every builder struct is a local that outlives it. - unsafe { - let lock: unsafe extern "C" fn(*mut pf_ffvk::AVHWFramesContext, *mut pf_ffvk::AVVkFrame) = - std::mem::transmute(f.lock_frame); - let fc = f.frames_ctx as *mut pf_ffvk::AVHWFramesContext; - let vkf = f.vkframe as *mut pf_ffvk::AVVkFrame; - lock(fc, vkf); - VkFrameSync { - image: (*vkf).img[0] as u64, - semaphore: (*vkf).sem[0] as u64, - sem_value: (*vkf).sem_value[0], - layout: (*vkf).layout[0] as i32, - queue_family: (*vkf).queue_family[0], - } - } -} - -/// Write the post-submission state back (FFmpeg waits these on its next use of the -/// frame) and release the lock. On a failed submit only the lock is released. -fn unlock_vkframe(f: &VkVideoFrame, sync: &VkFrameSync, submitted: bool, graphics_qf: u32) { - // SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this type - // and live for the call, and every builder struct is a local that outlives it. - unsafe { - let vkf = f.vkframe as *mut pf_ffvk::AVVkFrame; - if submitted { - (*vkf).sem_value[0] = sync.sem_value + 1; - (*vkf).layout[0] = - vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL.as_raw() as pf_ffvk::VkImageLayout; - if sync.queue_family != vk::QUEUE_FAMILY_IGNORED { - (*vkf).queue_family[0] = graphics_qf; - } - } - let unlock: unsafe extern "C" fn(*mut pf_ffvk::AVHWFramesContext, *mut pf_ffvk::AVVkFrame) = - std::mem::transmute(f.unlock_frame); - unlock(f.frames_ctx as *mut pf_ffvk::AVHWFramesContext, vkf); - } -} - #[cfg(test)] mod tests { use super::*; - /// The pool-format decision table (what this presenter can sample → what stays on the - /// hardware path, everything else demotes to software decode). Pinned so a format - /// added or dropped here is a deliberate act, not a drive-by. + /// What bit depth and packing the CSC pass runs for a decoded picture's format. + /// The lane reads it off the frame — an HEVC Main 10 stream reaches the decoder as + /// P010 — and rendering that with 8-bit range/transfer math is wrong in a way only a + /// side-by-side would catch. #[test] - fn vkframe_pool_format_decision_table() { - let eight = Some((vk::Format::R8_UNORM, vk::Format::R8G8_UNORM)); - let ten = Some(( - vk::Format::R10X6_UNORM_PACK16, - vk::Format::R10X6G10X6_UNORM_2PACK16, - )); - // 2-plane 4:2:0, both depths — the classic pair. - let f = |fmt: vk::Format| vkframe_plane_formats(fmt.as_raw()); - assert_eq!(f(vk::Format::G8_B8R8_2PLANE_420_UNORM), eight); + fn csc_depth_and_packing_follow_the_pictures_format() { + let d = |fmt: vk::Format| csc_depth_packing(RawVkFormat(fmt.as_raw())); + // 8-bit: H.264, HEVC Main, and the 4:4:4 RExt 8-bit sibling. + assert_eq!(d(vk::Format::G8_B8R8_2PLANE_420_UNORM), Some((8, false))); + assert_eq!(d(vk::Format::G8_B8R8_2PLANE_444_UNORM), Some((8, false))); + // 10-bit, MSB-packed into 16: HEVC Main 10 and its 4:4:4 sibling. The packing + // flag is what recovers exact `code/1023` from a UNORM16 sample. assert_eq!( - f(vk::Format::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16), - ten + d(vk::Format::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16), + Some((10, true)) ); - // 2-plane 4:4:4, both depths — hardware full-chroma (NVIDIA RExt decode). Same - // per-plane view formats; the full-size chroma plane is the shader's business. - assert_eq!(f(vk::Format::G8_B8R8_2PLANE_444_UNORM), eight); assert_eq!( - f(vk::Format::G10X6_B10X6R10X6_2PLANE_444_UNORM_3PACK16), - ten + d(vk::Format::G10X6_B10X6R10X6_2PLANE_444_UNORM_3PACK16), + Some((10, true)) ); - // 3-plane 4:4:4 and 2-plane 4:2:2: real formats a driver could report, NOT - // sampleable by the two-binding CSC — they must demote, not corrupt. - assert_eq!(f(vk::Format::G8_B8_R8_3PLANE_444_UNORM), None); - assert_eq!(f(vk::Format::G8_B8R8_2PLANE_422_UNORM), None); - assert_eq!(f(vk::Format::G16_B16R16_2PLANE_444_UNORM), None); - // Garbage never maps. - assert_eq!(vkframe_plane_formats(0), None); - assert_eq!(vkframe_plane_formats(-1), None); + // Formats the two-binding CSC pass cannot sample at all (3-plane 4:4:4, + // 16-bit) — pf-vkdecode never produces them — have no mapping rather than a + // plausible default. + assert_eq!(d(vk::Format::G8_B8_R8_3PLANE_444_UNORM), None); + assert_eq!(d(vk::Format::G16_B16R16_2PLANE_444_UNORM), None); + assert_eq!(csc_depth_packing(RawVkFormat(0)), None); + assert_eq!(csc_depth_packing(RawVkFormat(-1)), None); + // …and the fallback says 8-bit for those rather than panicking, because a + // wrong-looking picture beats a dead session. + assert_eq!(csc_depth_packing_or_8bit(RawVkFormat(0)), (8, false)); + } + + /// The decode lane's CLOSURE, and since M10 the only cross-check that can state + /// it: the producer is pf-vkdecode, whose output-format vocabulary this presenter + /// has no dependency on — so the check is against + /// [`pf_client_core::video::native_picture_formats`], which forwards + /// `pf_vkdecode::OUTPUT_FORMATS` verbatim. + /// + /// Without it, pf-vkdecode growing a fifth output format (12-bit RExt) would + /// build images fine, reach `csc_depth_packing_or_8bit`, render 10 or 12 bits as + /// 8 behind one warn line — and the table test above would stay green, because it + /// only asks this file's own table about itself. Note there is NO converse + /// assertion: pf-vkdecode is not obliged to produce every format the CSC pass can + /// sample. + #[test] + fn every_format_the_native_decoder_can_deliver_has_colour_math_here() { + let produced = pf_client_core::video::native_picture_formats(); + assert!(!produced.is_empty(), "the vocabulary must not be empty"); + for raw in produced { + assert!( + csc_depth_packing(raw).is_some(), + "pf-vkdecode delivers vk_format {} and the CSC pass has no depth \ + mapping for it — it would render as 8-bit", + raw.0 + ); + } } } diff --git a/crates/pf-presenter/src/vk/reconfig.rs b/crates/pf-presenter/src/vk/reconfig.rs index 5e9d39a5..a24bda58 100644 --- a/crates/pf-presenter/src/vk/reconfig.rs +++ b/crates/pf-presenter/src/vk/reconfig.rs @@ -42,7 +42,7 @@ impl Presenter { // OUR submit, not the presentation engine's semaphore consumption: // VUID-vkDestroySemaphore-05149 / VUID-vkDestroySwapchainKHR-01282 on every // recreate, and destroy-in-use is exactly the kind of misuse that turns into an - // intermittent VK_ERROR_DEVICE_LOST.) Safe against the pump's FFmpeg submits — + // intermittent VK_ERROR_DEVICE_LOST.) Safe against the pump's decode submits — // both sides hold the shared queue lock — and cheap: a recreate already stalls // the stream for a frame, and only happens on resize/HDR-flip/OUT_OF_DATE. { @@ -274,14 +274,11 @@ impl Presenter { }; self.csc.destroy(&self.device); // fence-safe: only our cmd bufs reference it self.csc = CscPass::new(&self.device, self.video_format)?; - // The planar (PyroWave) pass renders to the same intermediate — rebuild it at the - // new format too (an HDR pyrowave session needs the 10-bit intermediate exactly - // like the H.26x path; 8-bit PQ bands visibly). - #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] - if let Some(p) = self.csc_planar.take() { - p.destroy(&self.device); - self.csc_planar = Some(CscPass::new_planar(&self.device, self.video_format)?); - } + // The planar pass (PyroWave + the software rung) renders to the same intermediate + // — rebuild it at the new format too (an HDR session needs the 10-bit + // intermediate exactly like the H.26x path; 8-bit PQ bands visibly). + self.csc_planar.destroy(&self.device); + self.csc_planar = CscPass::new_planar(&self.device, self.video_format)?; if let Some(v) = self.video.take() { // SAFETY: per the Vulkan contract above - this destroys objects this type owns, and // the GPU is known idle for them (the fence/queue-wait on the path here, or the diff --git a/crates/pf-presenter/src/vk/resources.rs b/crates/pf-presenter/src/vk/resources.rs index 24f14e0f..b3aa007e 100644 --- a/crates/pf-presenter/src/vk/resources.rs +++ b/crates/pf-presenter/src/vk/resources.rs @@ -1,10 +1,10 @@ //! Video-image / staging-buffer (re)build + retired-frame destruction. use super::gpu::subresource_range; -use super::{Presenter, Retired, Staging, VideoImage}; +use super::{CpuPlanes, Presenter, Retired, Staging, VideoImage}; use anyhow::Result; use ash::vk; -use pf_client_core::video::CpuFrame; +use pf_client_core::video::CpuPlanarFrame; impl Retired { pub(super) fn destroy(self, device: &ash::Device) { @@ -13,31 +13,57 @@ impl Retired { Retired::Dmabuf(f) => f.destroy(device), #[cfg(windows)] Retired::D3d11(f) => f.destroy(device), - Retired::Vk { frame, views } => { - // SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned - // by this type and live for the call, and every builder struct is a local that - // outlives it. - unsafe { - for v in views { - device.destroy_image_view(v, None); - } - } - drop(frame); // guard drops here — AVFrame (and the VkImage) released + // The image and plane views belong to the DECODER's pools — nothing of ours + // to destroy. The drop sends the release token (the caller reaches here only + // after the sampling fence, so the token honestly means "GPU reads done"). + Retired::NativeVk(frame) => drop(frame), + } + } +} + +/// Staging offset of plane `i` for a picture of this size, plus the total bytes needed. +/// +/// Each plane starts on a 16-byte boundary so `bufferOffset` satisfies the copy's +/// "multiple of 4" rule whatever the picture dimensions are — with a 1-byte-per-texel +/// format an odd width would otherwise land a later plane on an odd offset. +fn plane_staging_offsets(f: &CpuPlanarFrame) -> ([usize; 3], usize) { + let mut offsets = [0usize; 3]; + let mut at = 0usize; + for (i, off) in offsets.iter_mut().enumerate() { + let (w, h) = f.plane_dims(i); + *off = at; + at += (w as usize * h as usize).next_multiple_of(16); + } + (offsets, at) +} + +impl CpuPlanes { + /// Destroy every handle this value holds. Null handles are fine — Vulkan defines + /// destroy/free on `VK_NULL_HANDLE` as a no-op — which is what lets + /// [`Presenter::rebuild_cpu_planes`] unwind a build that failed part-way. + pub(super) fn destroy(self, device: &ash::Device) { + // SAFETY: per the Vulkan contract above - this destroys objects this type owns, and the + // GPU is known idle for them (the fence/queue-wait on the path here, or the swapchain + // being retired), which is the obligation that makes a destroy sound rather than the + // handle merely being non-null. + unsafe { + for i in 0..3 { + device.destroy_image_view(self.views[i], None); + device.destroy_image(self.images[i], None); + device.free_memory(self.memory[i], None); } } } } impl Presenter { - /// Copy the frame's RGBA into the staging buffer and (re)build the video image on a - /// stream-size change. Rows keep their stride — `buffer_row_length` unpacks it. - pub(super) fn stage_frame(&mut self, f: &CpuFrame) -> Result<()> { - anyhow::ensure!( - f.stride % 4 == 0 && f.stride >= f.width as usize * 4, - "unexpected RGBA stride {} for width {}", - f.stride, - f.width - ); + /// Copy the frame's three tightly-packed planes into the staging buffer and (re)build + /// the plane images + video image on a stream-size change. + /// + /// Returns the per-plane staging offsets the record step copies from. Nothing here + /// touches the queue: a rebuild that fails must fail BEFORE the acquire, the same + /// rule the hardware imports follow. + pub(super) fn stage_frame(&mut self, f: &CpuPlanarFrame) -> Result<[usize; 3]> { if self .video .as_ref() @@ -46,15 +72,107 @@ impl Presenter { self.rebuild_video_image(f.width, f.height)?; tracing::info!(width = f.width, height = f.height, "video image (re)built"); } - let needed = f.stride * f.height as usize; + if self + .cpu_planes + .as_ref() + .is_none_or(|p| p.width != f.width || p.height != f.height) + { + self.rebuild_cpu_planes(f.width, f.height)?; + } + let (offsets, needed) = plane_staging_offsets(f); if self.staging.as_ref().is_none_or(|s| s.capacity < needed) { self.rebuild_staging(needed)?; } let s = self.staging.as_ref().unwrap(); - let n = f.rgba.len().min(needed); - // SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by this - // type and live for the call, and every builder struct is a local that outlives it. - unsafe { std::ptr::copy_nonoverlapping(f.rgba.as_ptr(), s.ptr, n) }; + for (i, off) in offsets.iter().enumerate() { + let plane = f.plane(i); + // SAFETY: per the Vulkan contract above - `s.ptr` maps a HOST_VISIBLE allocation of + // `s.capacity >= needed` bytes and `plane_staging_offsets` placed `off + plane.len()` + // inside `needed`; source and destination are distinct allocations. + unsafe { std::ptr::copy_nonoverlapping(plane.as_ptr(), s.ptr.add(*off), plane.len()) }; + } + Ok(offsets) + } + + /// (Re)build the software rung's three R8 plane images for a luma size. + fn rebuild_cpu_planes(&mut self, width: u32, height: u32) -> Result<()> { + // Fence-quiesce: the old images are only ever referenced by OUR command buffers. + self.quiesce_own()?; + if let Some(p) = self.cpu_planes.take() { + p.destroy(&self.device); + } + let (cw, ch) = CpuPlanarFrame::chroma_dims(width, height); + let dims = [(width, height), (cw, ch), (cw, ch)]; + // Built INTO the owning value, not into loose arrays: nine fallible steps (three + // images, three allocations, three views) used to `?` straight out and leak + // everything created before the one that failed — up to ~12 MB per size change at + // 4K, on the rung the client reaches because something already went wrong. + // `destroy` tolerates the nulls a partial build leaves (Vulkan defines + // destroy/free on `VK_NULL_HANDLE` as a no-op), so one call unwinds any prefix. + let mut planes = CpuPlanes { + images: [vk::Image::null(); 3], + memory: [vk::DeviceMemory::null(); 3], + views: [vk::ImageView::null(); 3], + width, + height, + initialized: false, + }; + for (i, dim) in dims.into_iter().enumerate() { + if let Err(e) = self.build_cpu_plane(&mut planes, i, dim) { + planes.destroy(&self.device); + return Err(e); + } + } + tracing::info!(width, height, "software plane images (re)built"); + self.cpu_planes = Some(planes); + Ok(()) + } + + /// One R8 plane of [`CpuPlanes`], written into `planes` as each handle is created so + /// a failure part-way leaves the caller something it can destroy. + fn build_cpu_plane(&self, planes: &mut CpuPlanes, i: usize, (w, h): (u32, u32)) -> Result<()> { + // SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by + // this type and live for the call, and every builder struct is a local that outlives + // it. + let image = unsafe { + self.device.create_image( + &vk::ImageCreateInfo::default() + .image_type(vk::ImageType::TYPE_2D) + .format(vk::Format::R8_UNORM) + .extent(vk::Extent3D { + width: w, + height: h, + depth: 1, + }) + .mip_levels(1) + .array_layers(1) + .samples(vk::SampleCountFlags::TYPE_1) + .tiling(vk::ImageTiling::OPTIMAL) + .usage(vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED) + .initial_layout(vk::ImageLayout::UNDEFINED), + None, + ) + }?; + planes.images[i] = image; + // SAFETY: per the Vulkan contract above - a read-only query on the live device. + let reqs = unsafe { self.device.get_image_memory_requirements(image) }; + planes.memory[i] = self.allocate(reqs, vk::MemoryPropertyFlags::DEVICE_LOCAL)?; + // SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by + // this type and live for the call. + unsafe { self.device.bind_image_memory(image, planes.memory[i], 0) }?; + // SAFETY: per the Vulkan contract above - the Vulkan handles used here are owned by + // this type and live for the call, and every builder struct is a local that outlives + // it. + planes.views[i] = unsafe { + self.device.create_image_view( + &vk::ImageViewCreateInfo::default() + .image(image) + .view_type(vk::ImageViewType::TYPE_2D) + .format(vk::Format::R8_UNORM) + .subresource_range(subresource_range()), + None, + ) + }?; Ok(()) } diff --git a/crates/pf-presenter/src/vk/setup.rs b/crates/pf-presenter/src/vk/setup.rs index 5ded5716..9930ceaf 100644 --- a/crates/pf-presenter/src/vk/setup.rs +++ b/crates/pf-presenter/src/vk/setup.rs @@ -13,6 +13,90 @@ use ash::vk; use ash::vk::Handle as _; use std::ffi::{c_char, CString}; +/// The two extensions Vulkan Video decode cannot work without, whatever the codec. +/// +/// Module scope, not function scope, because [`probe_decode`] answers "can this GPU do +/// Vulkan Video decode?" and MUST answer it with the same list the device creation path +/// gates on. A probe that keeps its own copy is a probe that eventually lies — and a +/// lying capability report is worse than none, because it sends a field reporter looking +/// in the wrong half of the stack. +pub(crate) const VIDEO_BASE: [&std::ffi::CStr; 2] = [ + ash::khr::video_queue::NAME, + ash::khr::video_decode_queue::NAME, +]; + +/// The per-codec decode extensions, same sharing rule as [`VIDEO_BASE`]. AV1's name is +/// spelled out because ash 0.38's headers (1.3.281) predate its promotion. +pub(crate) const VIDEO_CODECS: [&std::ffi::CStr; 3] = [ + ash::khr::video_decode_h264::NAME, + ash::khr::video_decode_h265::NAME, + c"VK_KHR_video_decode_av1", +]; + +/// The Vulkan Video decode gate: all five must hold. One function so the device creation +/// path and [`probe_decode`] cannot drift into disagreeing about what "supported" means — +/// the failure that produces is a probe reporting a capability the session then refuses, +/// which reads to everyone as a bug in the decoder rather than in the probe. +pub(crate) fn video_decode_gate( + api_1_3: bool, + features_ok: bool, + has_decode_family: bool, + base_exts_present: bool, + any_codec_ext: bool, +) -> bool { + api_1_3 && features_ok && has_decode_family && base_exts_present && any_codec_ext +} + +/// What one physical device can do for Vulkan Video decode, without creating a logical +/// device or a surface — the answer `punktfunk-session --probe-decode` prints. +/// +/// Exists because "I pinned the Vulkan rung and got D3D11VA instead" was, until this, +/// only answerable by starting a session and reading a log. Every field question about +/// hardware decode starts here: WHICH adapter, and does it advertise the codec. +#[derive(Debug, Clone)] +pub struct AdapterDecode { + /// The device's position in the RAW `vkEnumeratePhysicalDevices` order — and + /// therefore the value `PUNKTFUNK_VK_DEVICE` takes, because `pick_device` indexes the + /// unsorted list (`devices.get(i)`) before any ranking runs. + /// + /// ⚠ NOT the display position. This list is sorted discrete-first for readability, + /// while enumeration order puts the iGPU first on some hybrids — so the two disagree + /// on exactly the machines this probe exists to diagnose. Printing the display + /// position as if it were the env value would hand a hybrid-laptop reporter the + /// number for the other GPU. + pub index: usize, + /// Marketing name — also the `PUNKTFUNK_VK_ADAPTER` match key. Not necessarily + /// unique: a hybrid can expose the same iGPU twice, and a name match then resolves to + /// whichever enumerates first. + pub name: String, + /// Discrete GPUs sort first, exactly as `pick_device` ranks them, so index 0 here is + /// the device a default run will pick. + pub discrete: bool, + pub api_1_3: bool, + pub features_ok: bool, + /// The queue family index that advertises `VIDEO_DECODE_KHR`, if any. + pub decode_family: Option, + /// Raw `VkVideoCodecOperationFlagsKHR` from that family — what the DRIVER says it can + /// decode, independent of which extensions are exposed. + pub codec_ops: u32, + /// Required base extensions this device does NOT expose. + pub base_missing: Vec, + /// Per-codec decode extensions it does. + pub codec_exts: Vec, + /// [`video_decode_gate`] over the fields above. + pub usable: bool, + /// What the driver answers about video image formats, verbatim — one row per + /// (profile, usage) question ([`pf_vkdecode::probe`]). + /// + /// This is the half of the report that says why a device which passes every gate + /// above still cannot host the decoder. The five conjuncts answer "is Vulkan Video + /// here at all"; this answers "can the pipeline actually use it", which on at least + /// one shipping driver (Intel Arc, Windows) is a different question with a different + /// answer. Empty when the gate already failed — there is nothing to ask a device + /// with no video queue. + pub formats: Vec, +} + /// `VK_EXT_present_mode_fifo_latest_ready`, hand-declared: it postdates the Vulkan headers /// ash 0.38 is generated from (1.3.281), so there is no binding for it — which is also why /// an unenabled driver reports the mode back as the bare number `1000361000`. @@ -68,9 +152,9 @@ impl Presenter { let entry = unsafe { ash::Entry::load() }.context("libvulkan not loadable")?; let app_name = CString::new("punktfunk-session").unwrap(); - // 1.3: FFmpeg's Vulkan hwcontext requires an instance of at least 1.3 (any - // current loader accepts it regardless of device support; device-level gating - // happens below). + // 1.3: Vulkan Video decode and PyroWave's compute kernels both need a 1.3 + // device, and the instance version caps what the device can report (any current + // loader accepts 1.3 regardless of device support; device-level gating is below). let app_info = vk::ApplicationInfo::default() .application_name(&app_name) .api_version(vk::API_VERSION_1_3); @@ -189,10 +273,10 @@ impl Presenter { dev_exts.push(ash::ext::hdr_metadata::NAME.as_ptr()); } - // --- Vulkan Video decode (the FFmpeg-on-our-device path) --------------------- + // --- Vulkan Video decode (pf-vkdecode, on THIS device) ----------------------- // Probed, never required: a capable stack gets the video extensions, a second - // (decode) queue, and the features FFmpeg's decoder needs; anything less means - // `vulkan_decode() == None` and the decoder chain falls back (VAAPI/software). + // (decode) queue, and the features the decoder needs; anything less means + // `vulkan_decode() == None` and the ladder falls through (VAAPI/D3D11VA/software). // SAFETY: per the Vulkan contract above - a read-only query on the live instance/device, // filling locals returned by value. let dev_props = unsafe { instance.get_physical_device_properties(pdev) }; @@ -283,29 +367,23 @@ impl Presenter { .map(|(i, (_, v))| (i as u32, v.video_codec_operations)) }; - const VIDEO_BASE: [&std::ffi::CStr; 2] = [ - ash::khr::video_queue::NAME, - ash::khr::video_decode_queue::NAME, - ]; - const VIDEO_CODECS: [&std::ffi::CStr; 3] = [ - ash::khr::video_decode_h264::NAME, - ash::khr::video_decode_h265::NAME, - c"VK_KHR_video_decode_av1", - ]; let codec_exts: Vec<&std::ffi::CStr> = VIDEO_CODECS.into_iter().filter(|n| has(n)).collect(); - let video_ok = dev_is_13 - && features_ok - && decode_family.is_some() - && VIDEO_BASE.iter().all(|n| has(n)) - && !codec_exts.is_empty(); + let video_ok = video_decode_gate( + dev_is_13, + features_ok, + decode_family.is_some(), + VIDEO_BASE.iter().all(|n| has(n)), + !codec_exts.is_empty(), + ); let (decode_qf, decode_caps) = decode_family.unwrap_or((qfi, Default::default())); let mut video_ext_names: Vec<&std::ffi::CStr> = Vec::new(); if video_ok { video_ext_names.extend(VIDEO_BASE); video_ext_names.extend(&codec_exts); - // Optional decoder niceties FFmpeg uses when present. + // Optional decoder niceties, enabled when present (pf-vkdecode probes for + // them rather than requiring them). for opt in [c"VK_KHR_video_maintenance1", c"VK_KHR_video_maintenance2"] { if has(opt) { video_ext_names.push(opt); @@ -319,11 +397,42 @@ impl Presenter { "Vulkan Video decode available on this device" ); } else { + // ALL FIVE conjuncts, and the evidence behind each. The three this used to + // print could every one be true while the answer was still no — a device with + // Vulkan 1.3, the features and a decode queue family, but missing a codec + // extension, logged `dev_is_13=true features_ok=true decode_family=true` next + // to the word "unavailable" and named nothing that could be acted on. A field + // reporter cannot then tell "this build never tried" from "the driver said + // no", which is the single most expensive ambiguity a fallback can have: it + // makes a missing capability and a bug in our gate look identical. + // + // `queue_codec_ops` matters most on a device that HAS a decode queue: the + // driver names the codecs it can decode there, so an empty `codec_exts` + // beside a non-empty ops mask means the extensions are what is missing, not + // the hardware. + let base_missing: Vec<&str> = VIDEO_BASE + .iter() + .filter(|n| !has(n)) + .map(|n| n.to_str().unwrap_or("?")) + .collect(); + let codec_ext_names: Vec<&str> = codec_exts + .iter() + .map(|n| n.to_str().unwrap_or("?")) + .collect(); tracing::info!( dev_is_13, features_ok, decode_family = decode_family.is_some(), - "Vulkan Video decode unavailable — decoder falls back (VAAPI/software)" + video_base_missing = ?base_missing, + codec_exts_present = ?codec_ext_names, + queue_codec_ops = ?decode_family.map(|(_, ops)| ops), + device = %dev_props + .device_name_as_c_str() + .map(|c| c.to_string_lossy().into_owned()) + .unwrap_or_default(), + vendor_id = format_args!("0x{:04X}", dev_props.vendor_id), + "Vulkan Video decode unavailable on this device — the decoder falls back \ + one rung (D3D11VA on Windows, VAAPI on Linux, then software)" ); } @@ -344,7 +453,7 @@ impl Presenter { let mut en_pwait = vk::PhysicalDevicePresentWaitFeaturesKHR::default().present_wait(true); // Enable only the features the video path needs, and only where supported - // (harmless when the path is off; reported to FFmpeg via device_features). + // (harmless when the path is off; reported to the decode lane via device_features). let mut en_f11 = vk::PhysicalDeviceVulkan11Features::default() .sampler_ycbcr_conversion(have_f11.sampler_ycbcr_conversion == vk::TRUE); let mut en_f12 = vk::PhysicalDeviceVulkan12Features::default() @@ -421,31 +530,28 @@ impl Presenter { ext_mem_win32: ash::khr::external_memory_win32::Device::new(&instance, &device), }); let csc = CscPass::new(&device, vk::Format::R8G8B8A8_UNORM)?; - // Starts SDR like `csc`; an HDR (PQ) pyrowave session rebuilds it at the 10-bit + // Starts SDR like `csc`; an HDR (PQ) session rebuilds it at the 10-bit // intermediate via `set_hdr_mode`, exactly like the H.26x pass. - #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] - let csc_planar = if pyrowave_ok { - Some(CscPass::new_planar(&device, vk::Format::R8G8B8A8_UNORM)?) - } else { - None - }; + // + // Unconditional since M8. It used to be built only for a device that passed the + // pyrowave probe; the SOFTWARE rung now renders through it too, and that rung is + // the ladder's last one — gating it on a probe would leave the boxes that failed + // the probe with no way to show a software-decoded frame at all. + let csc_planar = CscPass::new_planar(&device, vk::Format::R8G8B8A8_UNORM)?; - // The exported handle bundle: FFmpeg Vulkan Video handles when the device can - // decode, AND (Windows) the D3D11-interop facts — so it's built whenever EITHER - // consumer needs it; `video_decode`/`d3d11_import` tell the decoder chain which - // paths are real. Extension lists must mirror creation exactly — FFmpeg keys its - // code paths off the strings. - // One lock per device for queue external sync (FFmpeg + Skia + this presenter - // all funnel their queue calls through it — see the `queue_lock` field docs). + // The exported handle bundle: this device's Vulkan handles when it can decode, + // AND (Windows) the D3D11-interop facts — so it's built whenever EITHER + // consumer needs it; `video_decode`/`d3d11_import` tell the decode ladder which + // paths are real. The extension LISTS must mirror creation exactly: the pyrowave + // decoder replays them verbatim into its pinned create-info reconstruction. + // One lock per device for queue external sync (the decode lane + Skia + this + // presenter all funnel their queue calls through it — see the `queue_lock` docs). let queue_lock = std::sync::Arc::new(pf_client_core::video::QueueLock::new()); #[cfg(windows)] let export_worthy = video_ok || win_capable || pyrowave_ok; #[cfg(not(windows))] let export_worthy = video_ok || pyrowave_ok; let video_export = if export_worthy { - // SAFETY: per the Vulkan contract above - a read-only query on the live - // instance/device, filling locals returned by value. - let qf_props = unsafe { instance.get_physical_device_queue_family_properties(pdev) }; let mut device_extensions: Vec = vec![CString::from(ash::khr::swapchain::NAME)]; #[cfg(target_os = "linux")] @@ -476,7 +582,6 @@ impl Presenter { .map(|c| c.to_string_lossy().into_owned()) .unwrap_or_default(), graphics_qf: qfi, - graphics_queue_flags: qf_props[qfi as usize].queue_flags.as_raw(), decode_qf, decode_video_caps: decode_caps.as_raw(), instance_extensions: instance_extensions @@ -590,8 +695,8 @@ impl Presenter { #[cfg(windows)] hw_win, csc, - #[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))] csc_planar, + cpu_planes: None, video_export, overlay_pipe, retired_hw: None, @@ -624,6 +729,166 @@ impl Presenter { } } +/// Every physical device's Vulkan Video decode capability +/// (`punktfunk-session --probe-decode`). No surface, no logical device, no session. +/// +/// The question this answers is the one every hardware-decode field report starts with: +/// the rung was pinned and something else ran — did this build never try, or did the +/// driver refuse? Ordered like [`list_adapters`] (discrete first, `pick_device`'s +/// tie-break), so the FIRST entry is the device a default run presents on — which is also +/// the device Vulkan Video decodes on, because the decoder shares the presenter's device +/// by design. On a hybrid laptop those two facts together are usually the whole answer: +/// pinning the decoder does not move the presenter, so probing the iGPU's capability +/// while the dGPU is presenting reports on the wrong GPU. `PUNKTFUNK_VK_DEVICE=` +/// is the knob that moves it. +pub fn probe_decode() -> Result> { + // SAFETY: per the Vulkan contract above - a create/allocate call on the live device, over + // builder structs that are locals outliving the call; the handle it returns is owned by the + // value being built here. + let entry = unsafe { ash::Entry::load() }.context("libvulkan not loadable")?; + let app_name = CString::new("punktfunk-session").unwrap(); + let app_info = vk::ApplicationInfo::default() + .application_name(&app_name) + .api_version(vk::API_VERSION_1_3); + // SAFETY: per the Vulkan contract above - a create/allocate call on the live device, over + // builder structs that are locals outliving the call; the handle it returns is owned by the + // value being built here. + let instance = unsafe { + entry.create_instance( + &vk::InstanceCreateInfo::default().application_info(&app_info), + None, + ) + } + .context("vkCreateInstance")?; + + // SAFETY: per the Vulkan contract above - a read-only query on the live instance/device, + // filling locals returned by value. + let devices = unsafe { instance.enumerate_physical_devices() }?; + let mut out: Vec<(u8, AdapterDecode)> = Vec::with_capacity(devices.len()); + // `enumerate()` BEFORE any filtering or sorting: this index is what + // `PUNKTFUNK_VK_DEVICE` selects, so it has to survive both. + for (raw_index, pdev) in devices.into_iter().enumerate() { + // SAFETY: per the Vulkan contract above - a read-only query on the live + // instance/device, filling locals returned by value. + let props = unsafe { instance.get_physical_device_properties(pdev) }; + let name = props + .device_name_as_c_str() + .map(|c| c.to_string_lossy().into_owned()) + .unwrap_or_default(); + if name.is_empty() { + continue; + } + let rank = match props.device_type { + vk::PhysicalDeviceType::DISCRETE_GPU => 0u8, + vk::PhysicalDeviceType::INTEGRATED_GPU => 1, + _ => 2, + }; + let api_1_3 = vk::api_version_major(props.api_version) > 1 + || vk::api_version_minor(props.api_version) >= 3; + + // The same three features the creation path demands (`features_ok` there). + let mut f11 = vk::PhysicalDeviceVulkan11Features::default(); + let mut f12 = vk::PhysicalDeviceVulkan12Features::default(); + let mut f13 = vk::PhysicalDeviceVulkan13Features::default(); + let mut feats = vk::PhysicalDeviceFeatures2::default() + .push_next(&mut f11) + .push_next(&mut f12) + .push_next(&mut f13); + // SAFETY: per the Vulkan contract above - a read-only query on the live + // instance/device, filling locals returned by value. + unsafe { instance.get_physical_device_features2(pdev, &mut feats) }; + let features_ok = f11.sampler_ycbcr_conversion == vk::TRUE + && f12.timeline_semaphore == vk::TRUE + && f13.synchronization2 == vk::TRUE; + + // SAFETY: per the Vulkan contract above - a read-only query on the live + // instance/device, filling locals returned by value. + let ext_props = + unsafe { instance.enumerate_device_extension_properties(pdev) }.unwrap_or_default(); + let has = |n: &std::ffi::CStr| { + ext_props + .iter() + .any(|e| e.extension_name_as_c_str() == Ok(n)) + }; + let base_missing: Vec = VIDEO_BASE + .iter() + .filter(|n| !has(n)) + .map(|n| n.to_string_lossy().into_owned()) + .collect(); + let codec_exts: Vec = VIDEO_CODECS + .iter() + .filter(|n| has(n)) + .map(|n| n.to_string_lossy().into_owned()) + .collect(); + + // The decode queue family and the codec operations the DRIVER claims for it — + // reported even when the extensions are absent, because "the hardware can, the + // driver does not expose it" is a different conversation from "this GPU cannot". + // SAFETY: per the Vulkan contract above - a read-only query on the live + // instance/device, filling locals returned by value. + let n = unsafe { instance.get_physical_device_queue_family_properties2_len(pdev) }; + let mut video: Vec = + vec![vk::QueueFamilyVideoPropertiesKHR::default(); n]; + let mut qprops: Vec = video + .iter_mut() + .map(|v| vk::QueueFamilyProperties2::default().push_next(v)) + .collect(); + // SAFETY: per the Vulkan contract above - a read-only query on the live + // instance/device, filling locals returned by value. + unsafe { instance.get_physical_device_queue_family_properties2(pdev, &mut qprops) }; + let flags: Vec = qprops + .iter() + .map(|p| p.queue_family_properties.queue_flags) + .collect(); + drop(qprops); + let found = flags + .iter() + .zip(&video) + .enumerate() + .find(|(_, (f, _))| f.contains(vk::QueueFlags::VIDEO_DECODE_KHR)) + .map(|(i, (_, v))| (i as u32, v.video_codec_operations)); + + let usable = video_decode_gate( + api_1_3, + features_ok, + found.is_some(), + base_missing.is_empty(), + !codec_exts.is_empty(), + ); + // Only where the gate passed: the format queries need `VK_KHR_video_queue`'s + // entry points, and asking a device that does not expose them produces a null + // dispatch, not an answer. + let formats = if usable { + // SAFETY: `instance` is the live instance created above and `pdev` one of + // the physical devices it enumerated; the probe only reads. + unsafe { pf_vkdecode::probe::probe_video_formats(&entry, &instance, pdev) } + } else { + Vec::new() + }; + out.push(( + rank, + AdapterDecode { + index: raw_index, + name, + discrete: rank == 0, + api_1_3, + features_ok, + decode_family: found.map(|(i, _)| i), + codec_ops: found.map_or(0, |(_, ops)| ops.as_raw()), + base_missing, + codec_exts, + usable, + formats, + }, + )); + } + out.sort_by_key(|(rank, _)| *rank); + // SAFETY: per the Vulkan contract above - this destroys objects this type owns, and no + // logical device was created against this instance, so nothing is in flight on it. + unsafe { instance.destroy_instance(None) }; + Ok(out.into_iter().map(|(_, a)| a).collect()) +} + /// The physical devices' marketing names — the shells' GPU-picker source /// (`punktfunk-session --list-adapters`). No surface and no logical device; discrete /// GPUs first (mirroring `pick_device`'s tie-break), duplicates collapsed (the name is diff --git a/crates/pf-vaadec/Cargo.toml b/crates/pf-vaadec/Cargo.toml new file mode 100644 index 00000000..a527fb07 --- /dev/null +++ b/crates/pf-vaadec/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "pf-vaadec" +description = "Native VAAPI H.264/HEVC/AV1 decode for the Linux clients (M6, M7): the hand-declared libva decode buffer layouts plus the profile/format/surface decisions — the CPU-testable half; the libva plumbing lives in pf-client-core (design/client-native-decode.md §3.4)" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true + +[dependencies] +# Direct dependency on the vendored parser crate, not just pf-bitstream: the conversion +# consumes the parser's `Sps`/`Pps`/`SliceType` wholesale (pf-bitstream re-exports only a +# subset), and the same path means the one crate instance the workspace already builds. +cros-codecs = { path = "../pf-bitstream/vendor/cros-codecs" } +pf-bitstream = { path = "../pf-bitstream" } +# For `SlotMap`/`SlotError` ONLY — see this crate's lib.rs for why the DPB slot ledger is +# borrowed from the Vulkan crate rather than duplicated or moved. +pf-vkdecode = { path = "../pf-vkdecode" } + +[lints] +workspace = true diff --git a/crates/pf-vaadec/layout-probe.c b/crates/pf-vaadec/layout-probe.c new file mode 100644 index 00000000..c09b975a --- /dev/null +++ b/crates/pf-vaadec/layout-probe.c @@ -0,0 +1,509 @@ +/* + * Layout probe for the hand-declared libva structures in `src/va.rs`, + * `src/va_h265.rs` and `src/va_av1.rs`. + * + * Those modules declare VAAPI's decode buffers as `#[repr(C)]` Rust structs, because + * this crate must build on macOS and in the Linux container where libva headers need + * not exist. This file is how those declarations were CHECKED rather than eyeballed, + * and it is committed so the check is reproducible instead of a claim in a commit + * message. + * + * Run it against real headers (no Linux box required): + * + * docker run --rm --platform linux/amd64 -v "$PWD/crates/pf-vaadec:/w" -w /w \ + * pf-lxcheck2 bash -lc 'apt-get update -qq && apt-get install -y -qq libva-dev \ + * && gcc -w -O0 layout-probe.c -o /tmp/probe && /tmp/probe' + * + * Every number it prints is pinned as a `const` assertion at the bottom of + * `src/va.rs`, so a transcription mistake is a compile error. The bit-field section + * exists because C bit-field allocation order is ABI-defined, not standardised: + * it PROVES least-significant-bit-first on this ABI rather than assuming it. + * + * Last run against libva 2.23.0-1ubuntu1, x86_64-linux-gnu. + */ +#include +#include +#include +#include +#include +#include + +#define S(t) printf("size %-34s %zu align %zu\n", #t, sizeof(t), _Alignof(t)) +#define O(t, f) printf("off %-20s %-28s %zu\n", #t, #f, offsetof(t, f)) + +int main(void) { + S(VAPictureH264); + O(VAPictureH264, picture_id); + O(VAPictureH264, frame_idx); + O(VAPictureH264, flags); + O(VAPictureH264, TopFieldOrderCnt); + O(VAPictureH264, BottomFieldOrderCnt); + O(VAPictureH264, va_reserved); + + S(VAPictureParameterBufferH264); + O(VAPictureParameterBufferH264, CurrPic); + O(VAPictureParameterBufferH264, ReferenceFrames); + O(VAPictureParameterBufferH264, picture_width_in_mbs_minus1); + O(VAPictureParameterBufferH264, picture_height_in_mbs_minus1); + O(VAPictureParameterBufferH264, bit_depth_luma_minus8); + O(VAPictureParameterBufferH264, bit_depth_chroma_minus8); + O(VAPictureParameterBufferH264, num_ref_frames); + O(VAPictureParameterBufferH264, seq_fields); + O(VAPictureParameterBufferH264, num_slice_groups_minus1); + O(VAPictureParameterBufferH264, slice_group_map_type); + O(VAPictureParameterBufferH264, slice_group_change_rate_minus1); + O(VAPictureParameterBufferH264, pic_init_qp_minus26); + O(VAPictureParameterBufferH264, pic_init_qs_minus26); + O(VAPictureParameterBufferH264, chroma_qp_index_offset); + O(VAPictureParameterBufferH264, second_chroma_qp_index_offset); + O(VAPictureParameterBufferH264, pic_fields); + O(VAPictureParameterBufferH264, frame_num); + O(VAPictureParameterBufferH264, va_reserved); + + S(VAIQMatrixBufferH264); + O(VAIQMatrixBufferH264, ScalingList4x4); + O(VAIQMatrixBufferH264, ScalingList8x8); + O(VAIQMatrixBufferH264, va_reserved); + + S(VASliceParameterBufferH264); + O(VASliceParameterBufferH264, slice_data_size); + O(VASliceParameterBufferH264, slice_data_offset); + O(VASliceParameterBufferH264, slice_data_flag); + O(VASliceParameterBufferH264, slice_data_bit_offset); + O(VASliceParameterBufferH264, first_mb_in_slice); + O(VASliceParameterBufferH264, slice_type); + O(VASliceParameterBufferH264, direct_spatial_mv_pred_flag); + O(VASliceParameterBufferH264, num_ref_idx_l0_active_minus1); + O(VASliceParameterBufferH264, num_ref_idx_l1_active_minus1); + O(VASliceParameterBufferH264, cabac_init_idc); + O(VASliceParameterBufferH264, slice_qp_delta); + O(VASliceParameterBufferH264, disable_deblocking_filter_idc); + O(VASliceParameterBufferH264, slice_alpha_c0_offset_div2); + O(VASliceParameterBufferH264, slice_beta_offset_div2); + O(VASliceParameterBufferH264, RefPicList0); + O(VASliceParameterBufferH264, RefPicList1); + O(VASliceParameterBufferH264, luma_log2_weight_denom); + O(VASliceParameterBufferH264, chroma_log2_weight_denom); + O(VASliceParameterBufferH264, luma_weight_l0_flag); + O(VASliceParameterBufferH264, luma_weight_l0); + O(VASliceParameterBufferH264, luma_offset_l0); + O(VASliceParameterBufferH264, chroma_weight_l0_flag); + O(VASliceParameterBufferH264, chroma_weight_l0); + O(VASliceParameterBufferH264, chroma_offset_l0); + O(VASliceParameterBufferH264, luma_weight_l1_flag); + O(VASliceParameterBufferH264, luma_weight_l1); + O(VASliceParameterBufferH264, luma_offset_l1); + O(VASliceParameterBufferH264, chroma_weight_l1_flag); + O(VASliceParameterBufferH264, chroma_weight_l1); + O(VASliceParameterBufferH264, chroma_offset_l1); + O(VASliceParameterBufferH264, va_reserved); + + /* Bit-field allocation order: prove LSB-first rather than assume it. */ + { + VAPictureParameterBufferH264 p; + p.seq_fields.value = 0; + p.seq_fields.bits.chroma_format_idc = 3; + printf("bits seq_fields.chroma_format_idc=3 -> value 0x%08x\n", p.seq_fields.value); + p.seq_fields.value = 0; + p.seq_fields.bits.log2_max_frame_num_minus4 = 0xf; + printf("bits seq_fields.log2_max_frame_num_minus4=0xf -> value 0x%08x\n", p.seq_fields.value); + p.pic_fields.value = 0; + p.pic_fields.bits.reference_pic_flag = 1; + printf("bits pic_fields.reference_pic_flag=1 -> value 0x%08x\n", p.pic_fields.value); + p.pic_fields.value = 0; + p.pic_fields.bits.weighted_bipred_idc = 3; + printf("bits pic_fields.weighted_bipred_idc=3 -> value 0x%08x\n", p.pic_fields.value); + } + + /* ---- HEVC (va_dec_hevc.h) ---- */ + S(VAPictureHEVC); + O(VAPictureHEVC, picture_id); + O(VAPictureHEVC, pic_order_cnt); + O(VAPictureHEVC, flags); + O(VAPictureHEVC, va_reserved); + + S(VAPictureParameterBufferHEVC); + O(VAPictureParameterBufferHEVC, CurrPic); + O(VAPictureParameterBufferHEVC, ReferenceFrames); + O(VAPictureParameterBufferHEVC, pic_width_in_luma_samples); + O(VAPictureParameterBufferHEVC, pic_height_in_luma_samples); + O(VAPictureParameterBufferHEVC, pic_fields); + O(VAPictureParameterBufferHEVC, sps_max_dec_pic_buffering_minus1); + O(VAPictureParameterBufferHEVC, bit_depth_luma_minus8); + O(VAPictureParameterBufferHEVC, bit_depth_chroma_minus8); + O(VAPictureParameterBufferHEVC, pcm_sample_bit_depth_luma_minus1); + O(VAPictureParameterBufferHEVC, pcm_sample_bit_depth_chroma_minus1); + O(VAPictureParameterBufferHEVC, log2_min_luma_coding_block_size_minus3); + O(VAPictureParameterBufferHEVC, log2_diff_max_min_luma_coding_block_size); + O(VAPictureParameterBufferHEVC, log2_min_transform_block_size_minus2); + O(VAPictureParameterBufferHEVC, log2_diff_max_min_transform_block_size); + O(VAPictureParameterBufferHEVC, log2_min_pcm_luma_coding_block_size_minus3); + O(VAPictureParameterBufferHEVC, log2_diff_max_min_pcm_luma_coding_block_size); + O(VAPictureParameterBufferHEVC, max_transform_hierarchy_depth_intra); + O(VAPictureParameterBufferHEVC, max_transform_hierarchy_depth_inter); + O(VAPictureParameterBufferHEVC, init_qp_minus26); + O(VAPictureParameterBufferHEVC, diff_cu_qp_delta_depth); + O(VAPictureParameterBufferHEVC, pps_cb_qp_offset); + O(VAPictureParameterBufferHEVC, pps_cr_qp_offset); + O(VAPictureParameterBufferHEVC, log2_parallel_merge_level_minus2); + O(VAPictureParameterBufferHEVC, num_tile_columns_minus1); + O(VAPictureParameterBufferHEVC, num_tile_rows_minus1); + O(VAPictureParameterBufferHEVC, column_width_minus1); + O(VAPictureParameterBufferHEVC, row_height_minus1); + O(VAPictureParameterBufferHEVC, slice_parsing_fields); + O(VAPictureParameterBufferHEVC, log2_max_pic_order_cnt_lsb_minus4); + O(VAPictureParameterBufferHEVC, num_short_term_ref_pic_sets); + O(VAPictureParameterBufferHEVC, num_long_term_ref_pic_sps); + O(VAPictureParameterBufferHEVC, num_ref_idx_l0_default_active_minus1); + O(VAPictureParameterBufferHEVC, num_ref_idx_l1_default_active_minus1); + O(VAPictureParameterBufferHEVC, pps_beta_offset_div2); + O(VAPictureParameterBufferHEVC, pps_tc_offset_div2); + O(VAPictureParameterBufferHEVC, num_extra_slice_header_bits); + O(VAPictureParameterBufferHEVC, st_rps_bits); + O(VAPictureParameterBufferHEVC, va_reserved); + + S(VASliceParameterBufferHEVC); + O(VASliceParameterBufferHEVC, slice_data_size); + O(VASliceParameterBufferHEVC, slice_data_offset); + O(VASliceParameterBufferHEVC, slice_data_flag); + O(VASliceParameterBufferHEVC, slice_data_byte_offset); + O(VASliceParameterBufferHEVC, slice_segment_address); + O(VASliceParameterBufferHEVC, RefPicList); + O(VASliceParameterBufferHEVC, LongSliceFlags); + O(VASliceParameterBufferHEVC, collocated_ref_idx); + O(VASliceParameterBufferHEVC, num_ref_idx_l0_active_minus1); + O(VASliceParameterBufferHEVC, num_ref_idx_l1_active_minus1); + O(VASliceParameterBufferHEVC, slice_qp_delta); + O(VASliceParameterBufferHEVC, slice_cb_qp_offset); + O(VASliceParameterBufferHEVC, slice_cr_qp_offset); + O(VASliceParameterBufferHEVC, slice_beta_offset_div2); + O(VASliceParameterBufferHEVC, slice_tc_offset_div2); + O(VASliceParameterBufferHEVC, luma_log2_weight_denom); + O(VASliceParameterBufferHEVC, delta_chroma_log2_weight_denom); + O(VASliceParameterBufferHEVC, delta_luma_weight_l0); + O(VASliceParameterBufferHEVC, luma_offset_l0); + O(VASliceParameterBufferHEVC, delta_chroma_weight_l0); + O(VASliceParameterBufferHEVC, ChromaOffsetL0); + O(VASliceParameterBufferHEVC, delta_luma_weight_l1); + O(VASliceParameterBufferHEVC, luma_offset_l1); + O(VASliceParameterBufferHEVC, delta_chroma_weight_l1); + O(VASliceParameterBufferHEVC, ChromaOffsetL1); + O(VASliceParameterBufferHEVC, five_minus_max_num_merge_cand); + O(VASliceParameterBufferHEVC, num_entry_point_offsets); + O(VASliceParameterBufferHEVC, entry_offset_to_subset_array); + O(VASliceParameterBufferHEVC, slice_data_num_emu_prevn_bytes); + O(VASliceParameterBufferHEVC, va_reserved); + + S(VAIQMatrixBufferHEVC); + O(VAIQMatrixBufferHEVC, ScalingList4x4); + O(VAIQMatrixBufferHEVC, ScalingList8x8); + O(VAIQMatrixBufferHEVC, ScalingList16x16); + O(VAIQMatrixBufferHEVC, ScalingList32x32); + O(VAIQMatrixBufferHEVC, ScalingListDC16x16); + O(VAIQMatrixBufferHEVC, ScalingListDC32x32); + O(VAIQMatrixBufferHEVC, va_reserved); + + { + VAPictureParameterBufferHEVC h; + h.pic_fields.value = 0; + h.pic_fields.bits.chroma_format_idc = 3; + printf("bits hevc pic_fields.chroma_format_idc=3 -> 0x%08x\n", h.pic_fields.value); + h.pic_fields.value = 0; + h.pic_fields.bits.NoBiPredFlag = 1; + printf("bits hevc pic_fields.NoBiPredFlag=1 -> 0x%08x\n", h.pic_fields.value); + h.slice_parsing_fields.value = 0; + h.slice_parsing_fields.bits.IntraPicFlag = 1; + printf("bits hevc slice_parsing_fields.IntraPicFlag=1 -> 0x%08x\n", h.slice_parsing_fields.value); + VASliceParameterBufferHEVC s; + s.LongSliceFlags.value = 0; + s.LongSliceFlags.fields.slice_type = 3; + printf("bits hevc LongSliceFlags.slice_type=3 -> 0x%08x\n", s.LongSliceFlags.value); + s.LongSliceFlags.value = 0; + s.LongSliceFlags.fields.slice_loop_filter_across_slices_enabled_flag = 1; + printf("bits hevc LongSliceFlags.slice_loop_filter_across=1 -> 0x%08x\n", s.LongSliceFlags.value); + } + + /* ---- AV1 (va_dec_av1.h) ---- + * + * Three things make this codec's layout worth measuring rather than counting: + * a POINTER member (`anchor_frames_list`) that forces eight-byte alignment and + * therefore padding nothing in the field list suggests; two bit-field unions + * that are NOT 32 bits wide (`loop_filter_info_fields` is a uint8_t, + * `qmatrix_fields` and `loop_restoration_fields` are uint16_t), so a u32 `pack` + * would write over the neighbouring field; and three nested structs whose own + * VA_PADDING_LOW tails sit inside the picture-parameter buffer. + */ + S(VASegmentationStructAV1); + O(VASegmentationStructAV1, segment_info_fields); + O(VASegmentationStructAV1, feature_data); + O(VASegmentationStructAV1, feature_mask); + O(VASegmentationStructAV1, va_reserved); + + S(VAFilmGrainStructAV1); + O(VAFilmGrainStructAV1, film_grain_info_fields); + O(VAFilmGrainStructAV1, grain_seed); + O(VAFilmGrainStructAV1, num_y_points); + O(VAFilmGrainStructAV1, point_y_value); + O(VAFilmGrainStructAV1, point_y_scaling); + O(VAFilmGrainStructAV1, num_cb_points); + O(VAFilmGrainStructAV1, point_cb_value); + O(VAFilmGrainStructAV1, point_cb_scaling); + O(VAFilmGrainStructAV1, num_cr_points); + O(VAFilmGrainStructAV1, point_cr_value); + O(VAFilmGrainStructAV1, point_cr_scaling); + O(VAFilmGrainStructAV1, ar_coeffs_y); + O(VAFilmGrainStructAV1, ar_coeffs_cb); + O(VAFilmGrainStructAV1, ar_coeffs_cr); + O(VAFilmGrainStructAV1, cb_mult); + O(VAFilmGrainStructAV1, cb_luma_mult); + O(VAFilmGrainStructAV1, cb_offset); + O(VAFilmGrainStructAV1, cr_mult); + O(VAFilmGrainStructAV1, cr_luma_mult); + O(VAFilmGrainStructAV1, cr_offset); + O(VAFilmGrainStructAV1, va_reserved); + + S(VAWarpedMotionParamsAV1); + O(VAWarpedMotionParamsAV1, wmtype); + O(VAWarpedMotionParamsAV1, wmmat); + O(VAWarpedMotionParamsAV1, invalid); + O(VAWarpedMotionParamsAV1, va_reserved); + + S(VADecPictureParameterBufferAV1); + O(VADecPictureParameterBufferAV1, profile); + O(VADecPictureParameterBufferAV1, order_hint_bits_minus_1); + O(VADecPictureParameterBufferAV1, bit_depth_idx); + O(VADecPictureParameterBufferAV1, matrix_coefficients); + O(VADecPictureParameterBufferAV1, seq_info_fields); + O(VADecPictureParameterBufferAV1, current_frame); + O(VADecPictureParameterBufferAV1, current_display_picture); + O(VADecPictureParameterBufferAV1, anchor_frames_num); + O(VADecPictureParameterBufferAV1, anchor_frames_list); + O(VADecPictureParameterBufferAV1, frame_width_minus1); + O(VADecPictureParameterBufferAV1, frame_height_minus1); + O(VADecPictureParameterBufferAV1, output_frame_width_in_tiles_minus_1); + O(VADecPictureParameterBufferAV1, output_frame_height_in_tiles_minus_1); + O(VADecPictureParameterBufferAV1, ref_frame_map); + O(VADecPictureParameterBufferAV1, ref_frame_idx); + O(VADecPictureParameterBufferAV1, primary_ref_frame); + O(VADecPictureParameterBufferAV1, order_hint); + O(VADecPictureParameterBufferAV1, seg_info); + O(VADecPictureParameterBufferAV1, film_grain_info); + O(VADecPictureParameterBufferAV1, tile_cols); + O(VADecPictureParameterBufferAV1, tile_rows); + O(VADecPictureParameterBufferAV1, width_in_sbs_minus_1); + O(VADecPictureParameterBufferAV1, height_in_sbs_minus_1); + O(VADecPictureParameterBufferAV1, tile_count_minus_1); + O(VADecPictureParameterBufferAV1, context_update_tile_id); + O(VADecPictureParameterBufferAV1, pic_info_fields); + O(VADecPictureParameterBufferAV1, superres_scale_denominator); + O(VADecPictureParameterBufferAV1, interp_filter); + O(VADecPictureParameterBufferAV1, filter_level); + O(VADecPictureParameterBufferAV1, filter_level_u); + O(VADecPictureParameterBufferAV1, filter_level_v); + O(VADecPictureParameterBufferAV1, loop_filter_info_fields); + O(VADecPictureParameterBufferAV1, ref_deltas); + O(VADecPictureParameterBufferAV1, mode_deltas); + O(VADecPictureParameterBufferAV1, base_qindex); + O(VADecPictureParameterBufferAV1, y_dc_delta_q); + O(VADecPictureParameterBufferAV1, u_dc_delta_q); + O(VADecPictureParameterBufferAV1, u_ac_delta_q); + O(VADecPictureParameterBufferAV1, v_dc_delta_q); + O(VADecPictureParameterBufferAV1, v_ac_delta_q); + O(VADecPictureParameterBufferAV1, qmatrix_fields); + O(VADecPictureParameterBufferAV1, mode_control_fields); + O(VADecPictureParameterBufferAV1, cdef_damping_minus_3); + O(VADecPictureParameterBufferAV1, cdef_bits); + O(VADecPictureParameterBufferAV1, cdef_y_strengths); + O(VADecPictureParameterBufferAV1, cdef_uv_strengths); + O(VADecPictureParameterBufferAV1, loop_restoration_fields); + O(VADecPictureParameterBufferAV1, wm); + O(VADecPictureParameterBufferAV1, va_reserved); + printf("count VADecPictureParameterBufferAV1 wm %zu\n", + sizeof(((VADecPictureParameterBufferAV1 *)0)->wm) / + sizeof(((VADecPictureParameterBufferAV1 *)0)->wm[0])); + printf("size union seq_info_fields %zu\n", + sizeof(((VADecPictureParameterBufferAV1 *)0)->seq_info_fields)); + printf("size union pic_info_fields %zu\n", + sizeof(((VADecPictureParameterBufferAV1 *)0)->pic_info_fields)); + printf("size union loop_filter_info_fields %zu\n", + sizeof(((VADecPictureParameterBufferAV1 *)0)->loop_filter_info_fields)); + printf("size union qmatrix_fields %zu\n", + sizeof(((VADecPictureParameterBufferAV1 *)0)->qmatrix_fields)); + printf("size union mode_control_fields %zu\n", + sizeof(((VADecPictureParameterBufferAV1 *)0)->mode_control_fields)); + printf("size union loop_restoration_fields %zu\n", + sizeof(((VADecPictureParameterBufferAV1 *)0)->loop_restoration_fields)); + + S(VASliceParameterBufferAV1); + O(VASliceParameterBufferAV1, slice_data_size); + O(VASliceParameterBufferAV1, slice_data_offset); + O(VASliceParameterBufferAV1, slice_data_flag); + O(VASliceParameterBufferAV1, tile_row); + O(VASliceParameterBufferAV1, tile_column); + O(VASliceParameterBufferAV1, tg_start); + O(VASliceParameterBufferAV1, tg_end); + O(VASliceParameterBufferAV1, anchor_frame_idx); + O(VASliceParameterBufferAV1, tile_idx_in_tile_list); + O(VASliceParameterBufferAV1, va_reserved); + + /* + * Bit-field allocation order for AV1's six unions — one field at a time, the + * same proof the H.264 block makes, repeated here because three of these + * unions are NARROWER than a word and a mistake there is invisible in a u32. + */ + { + VADecPictureParameterBufferAV1 a; + a.seq_info_fields.value = 0; + a.seq_info_fields.fields.still_picture = 1; + printf("bits av1 seq_info.still_picture=1 -> 0x%08x\n", a.seq_info_fields.value); + a.seq_info_fields.value = 0; + a.seq_info_fields.fields.film_grain_params_present = 1; + printf("bits av1 seq_info.film_grain_params_present=1 -> 0x%08x\n", + a.seq_info_fields.value); + a.seq_info_fields.value = 0; + a.seq_info_fields.fields.mono_chrome = 1; + printf("bits av1 seq_info.mono_chrome=1 -> 0x%08x\n", a.seq_info_fields.value); + + a.pic_info_fields.value = 0; + a.pic_info_fields.bits.frame_type = 3; + printf("bits av1 pic_info.frame_type=3 -> 0x%08x\n", a.pic_info_fields.value); + a.pic_info_fields.value = 0; + a.pic_info_fields.bits.large_scale_tile = 1; + printf("bits av1 pic_info.large_scale_tile=1 -> 0x%08x\n", a.pic_info_fields.value); + a.pic_info_fields.value = 0; + a.pic_info_fields.bits.use_ref_frame_mvs = 1; + printf("bits av1 pic_info.use_ref_frame_mvs=1 -> 0x%08x\n", a.pic_info_fields.value); + + a.loop_filter_info_fields.value = 0; + a.loop_filter_info_fields.bits.sharpness_level = 7; + printf("bits av1 loop_filter_info.sharpness_level=7 -> 0x%02x\n", + a.loop_filter_info_fields.value); + a.loop_filter_info_fields.value = 0; + a.loop_filter_info_fields.bits.mode_ref_delta_update = 1; + printf("bits av1 loop_filter_info.mode_ref_delta_update=1 -> 0x%02x\n", + a.loop_filter_info_fields.value); + + a.qmatrix_fields.value = 0; + a.qmatrix_fields.bits.using_qmatrix = 1; + printf("bits av1 qmatrix.using_qmatrix=1 -> 0x%04x\n", a.qmatrix_fields.value); + a.qmatrix_fields.value = 0; + a.qmatrix_fields.bits.qm_v = 0xf; + printf("bits av1 qmatrix.qm_v=0xf -> 0x%04x\n", a.qmatrix_fields.value); + + a.mode_control_fields.value = 0; + a.mode_control_fields.bits.delta_q_present_flag = 1; + printf("bits av1 mode_control.delta_q_present_flag=1 -> 0x%08x\n", + a.mode_control_fields.value); + a.mode_control_fields.value = 0; + a.mode_control_fields.bits.skip_mode_present = 1; + printf("bits av1 mode_control.skip_mode_present=1 -> 0x%08x\n", + a.mode_control_fields.value); + a.mode_control_fields.value = 0; + a.mode_control_fields.bits.tx_mode = 3; + printf("bits av1 mode_control.tx_mode=3 -> 0x%08x\n", a.mode_control_fields.value); + + a.loop_restoration_fields.value = 0; + a.loop_restoration_fields.bits.yframe_restoration_type = 3; + printf("bits av1 loop_restoration.yframe_restoration_type=3 -> 0x%04x\n", + a.loop_restoration_fields.value); + a.loop_restoration_fields.value = 0; + a.loop_restoration_fields.bits.lr_uv_shift = 1; + printf("bits av1 loop_restoration.lr_uv_shift=1 -> 0x%04x\n", + a.loop_restoration_fields.value); + + VASegmentationStructAV1 s; + s.segment_info_fields.value = 0; + s.segment_info_fields.bits.enabled = 1; + printf("bits av1 segment_info.enabled=1 -> 0x%08x\n", s.segment_info_fields.value); + s.segment_info_fields.value = 0; + s.segment_info_fields.bits.update_data = 1; + printf("bits av1 segment_info.update_data=1 -> 0x%08x\n", s.segment_info_fields.value); + + VAFilmGrainStructAV1 g; + g.film_grain_info_fields.value = 0; + g.film_grain_info_fields.bits.apply_grain = 1; + printf("bits av1 film_grain.apply_grain=1 -> 0x%08x\n", g.film_grain_info_fields.value); + g.film_grain_info_fields.value = 0; + g.film_grain_info_fields.bits.clip_to_restricted_range = 1; + printf("bits av1 film_grain.clip_to_restricted_range=1 -> 0x%08x\n", + g.film_grain_info_fields.value); + g.film_grain_info_fields.value = 0; + g.film_grain_info_fields.bits.grain_scale_shift = 3; + printf("bits av1 film_grain.grain_scale_shift=3 -> 0x%08x\n", + g.film_grain_info_fields.value); + } + + printf("enum VAProfileAV1Profile0 %d\n", VAProfileAV1Profile0); + printf("enum VAProfileAV1Profile1 %d\n", VAProfileAV1Profile1); + printf("enum VAAV1TransformationIdentity %d\n", VAAV1TransformationIdentity); + printf("enum VAAV1TransformationTranslation %d\n", VAAV1TransformationTranslation); + printf("enum VAAV1TransformationRotzoom %d\n", VAAV1TransformationRotzoom); + printf("enum VAAV1TransformationAffine %d\n", VAAV1TransformationAffine); + printf("enum VA_RT_FORMAT_YUV420_10 0x%08x\n", VA_RT_FORMAT_YUV420_10); + printf("enum VA_RT_FORMAT_YUV420 0x%08x\n", VA_RT_FORMAT_YUV420); + + printf("VA_PADDING_LOW=%d VA_PADDING_MEDIUM=%d\n", VA_PADDING_LOW, VA_PADDING_MEDIUM); + + /* + * The export descriptor. This one is not a buffer we FILL — it is a struct the + * driver WRITES, so a wrong layout is read as plausible garbage (an fd from the + * middle of a pitch, a plane count from a modifier's high word) rather than + * refused. It carries fixed-size arrays whose bounds the flattening walk trusts, + * which is exactly the shape that turned into the green-screen bug once already. + */ + S(VADRMPRIMESurfaceDescriptor); + O(VADRMPRIMESurfaceDescriptor, fourcc); + O(VADRMPRIMESurfaceDescriptor, width); + O(VADRMPRIMESurfaceDescriptor, height); + O(VADRMPRIMESurfaceDescriptor, num_objects); + O(VADRMPRIMESurfaceDescriptor, objects); + O(VADRMPRIMESurfaceDescriptor, num_layers); + O(VADRMPRIMESurfaceDescriptor, layers); + printf("count VADRMPRIMESurfaceDescriptor objects %zu\n", + sizeof(((VADRMPRIMESurfaceDescriptor *)0)->objects) / + sizeof(((VADRMPRIMESurfaceDescriptor *)0)->objects[0])); + printf("count VADRMPRIMESurfaceDescriptor layers %zu\n", + sizeof(((VADRMPRIMESurfaceDescriptor *)0)->layers) / + sizeof(((VADRMPRIMESurfaceDescriptor *)0)->layers[0])); + printf("count layer.object_index %zu\n", + sizeof(((VADRMPRIMESurfaceDescriptor *)0)->layers[0].object_index) / + sizeof(((VADRMPRIMESurfaceDescriptor *)0)->layers[0].object_index[0])); + + /* + * The enumerators the runtime calls pass by value. Printed rather than + * transcribed because two of them are the exact pair this program has already + * been warned about: VASliceParameterBufferType and VASliceDataBufferType are + * 4 and 5, not the 3 and 4 that counting the enum from the top suggests. + */ + printf("enum VAEntrypointVLD %d\n", VAEntrypointVLD); + printf("enum VAConfigAttribRTFormat %d\n", VAConfigAttribRTFormat); + printf("enum VAPictureParameterBufferType %d\n", VAPictureParameterBufferType); + printf("enum VAIQMatrixBufferType %d\n", VAIQMatrixBufferType); + printf("enum VASliceParameterBufferType %d\n", VASliceParameterBufferType); + printf("enum VASliceDataBufferType %d\n", VASliceDataBufferType); + printf("enum VA_EXPORT_SURFACE_READ_ONLY 0x%04x\n", VA_EXPORT_SURFACE_READ_ONLY); + printf("enum VA_EXPORT_SURFACE_SEPARATE_LAYERS 0x%04x\n", + VA_EXPORT_SURFACE_SEPARATE_LAYERS); + printf("enum VA_SURFACE_ATTRIB_SETTABLE 0x%04x\n", VA_SURFACE_ATTRIB_SETTABLE); + printf("enum VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 0x%08x\n", + VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2); + printf("enum VASurfaceAttribPixelFormat %d\n", VASurfaceAttribPixelFormat); + printf("enum VAGenericValueTypeInteger %d\n", VAGenericValueTypeInteger); + printf("enum VA_STATUS_SUCCESS %d\n", VA_STATUS_SUCCESS); + printf("enum VA_INVALID_ID 0x%08x\n", VA_INVALID_ID); + printf("enum VA_FOURCC_NV12 0x%08x\n", VA_FOURCC_NV12); + printf("enum VA_FOURCC_P010 0x%08x\n", VA_FOURCC_P010); + printf("enum VA_PROGRESSIVE 0x%04x\n", VA_PROGRESSIVE); + + S(VASurfaceAttrib); + O(VASurfaceAttrib, type); + O(VASurfaceAttrib, flags); + O(VASurfaceAttrib, value); + S(VAGenericValue); + O(VAGenericValue, type); + O(VAGenericValue, value); + S(VAConfigAttrib); + O(VAConfigAttrib, type); + O(VAConfigAttrib, value); + return 0; +} diff --git a/crates/pf-vaadec/src/config.rs b/crates/pf-vaadec/src/config.rs new file mode 100644 index 00000000..615670d2 --- /dev/null +++ b/crates/pf-vaadec/src/config.rs @@ -0,0 +1,273 @@ +//! Decoder-creation decisions: which `VAProfile` a stream needs, which render-target +//! format its surfaces must carry, and how many of them to allocate. +//! +//! The same job `pf-dxvadec`'s `config` module does for DXVA, and split out for the same +//! reason: these are pure functions of the stream's shape, so they belong where the +//! ordinary gates run them rather than inside `cfg(target_os = "linux")` FFI that +//! only a box can compile. +//! +//! Constant values are the libva 2.23.0 enumerators. + +/// `VAEntrypointVLD` — full bitstream decode, the only entry point this rung uses. +pub const VA_ENTRYPOINT_VLD: u32 = 1; + +/// `VAProfile` enumerators (`va.h`). +pub const VA_PROFILE_H264_MAIN: i32 = 6; +pub const VA_PROFILE_H264_HIGH: i32 = 7; +pub const VA_PROFILE_H264_CONSTRAINED_BASELINE: i32 = 13; +pub const VA_PROFILE_HEVC_MAIN: i32 = 17; +pub const VA_PROFILE_HEVC_MAIN10: i32 = 18; +/// Measured, not counted from the top of the enum: `VAProfileAV1Profile0` is **32** +/// and `VAProfileAV1Profile1` is 33, with ten VP9/HEVC enumerators in between. +pub const VA_PROFILE_AV1_PROFILE0: i32 = 32; +pub const VA_PROFILE_AV1_PROFILE1: i32 = 33; + +/// `VA_RT_FORMAT_*` — the surface render-target format. +pub const VA_RT_FORMAT_YUV420: u32 = 0x0000_0001; +pub const VA_RT_FORMAT_YUV444: u32 = 0x0000_0004; +pub const VA_RT_FORMAT_YUV420_10: u32 = 0x0000_0100; + +/// Which codec a session decodes. Mirrors `pf-dxvadec`'s `Codec` rather than +/// re-exporting it: this crate must not depend on the Windows-facing one. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Codec { + H264, + H265, + Av1, +} + +/// A profile choice, with the name the logs print. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaProfile { + pub value: i32, + pub name: &'static str, +} + +/// Why a stream cannot be decoded by this rung at all. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConfigError { + /// A (chroma, depth) pair with no profile — 4:4:4, or a depth outside 8/10. + UnsupportedShape { chroma_format_idc: u8, depth: u8 }, +} + +impl std::fmt::Display for ConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConfigError::UnsupportedShape { + chroma_format_idc, + depth, + } => write!( + f, + "no VAAPI decode profile for chroma_format_idc {chroma_format_idc} at {depth} bits" + ), + } + } +} + +impl std::error::Error for ConfigError {} + +/// The profile a stream of this shape decodes under. +/// +/// H.264 resolves to **High** for every 8-bit 4:2:0 stream rather than reading the +/// SPS's `profile_idc`. That is deliberate and is what VAAPI clients do: High is a +/// superset of Main and Constrained Baseline for the tools our hosts emit, every +/// driver advertising H.264 decode advertises High, and picking Main for a stream +/// that turns out to use 8x8 transforms is a mid-stream failure where picking High +/// is not. The narrower enumerators are exported for the capability probe, which +/// reports what the DEVICE offers. +/// +/// 4:4:4 is refused rather than mapped: `VAProfileH264High444` exists in the header +/// but no driver in this fleet advertises it, and the Vulkan rung is where this +/// program's 4:4:4 support actually lives. +/// +/// **AV1 Profile 0 covers 8 AND 10 bits under one enumerator**, so the pair differs +/// only in the render-target format — which is the one thing that must not be shared, +/// since it is what the surface pool is allocated with. Profile 1 (4:4:4) and +/// Profile 2 (4:2:2 / 12-bit) are refused: `va_dec_av1.h` opens by saying *"This AV1 +/// decoding API supports 8-bit/10bit 420 format only"*, so this is the API's +/// envelope and not merely ours. Monochrome reaches here as `chroma_format_idc` 0 +/// and lands in the same refusal rather than being mistaken for 4:2:0. +pub fn profile_for( + codec: Codec, + chroma_format_idc: u8, + depth: u8, +) -> Result { + match (codec, chroma_format_idc, depth) { + (Codec::H264, 1, 8) => Ok(VaProfile { + value: VA_PROFILE_H264_HIGH, + name: "H.264 High", + }), + (Codec::Av1, 1, 8) => Ok(VaProfile { + value: VA_PROFILE_AV1_PROFILE0, + name: "AV1 Profile 0", + }), + (Codec::Av1, 1, 10) => Ok(VaProfile { + value: VA_PROFILE_AV1_PROFILE0, + name: "AV1 Profile 0 (10-bit)", + }), + (Codec::H265, 1, 8) => Ok(VaProfile { + value: VA_PROFILE_HEVC_MAIN, + name: "HEVC Main", + }), + (Codec::H265, 1, 10) => Ok(VaProfile { + value: VA_PROFILE_HEVC_MAIN10, + name: "HEVC Main 10", + }), + _ => Err(ConfigError::UnsupportedShape { + chroma_format_idc, + depth, + }), + } +} + +/// The surface render-target format for a stream shape. +pub fn rt_format(chroma_format_idc: u8, depth: u8) -> Result { + match (chroma_format_idc, depth) { + (1, 8) => Ok(VA_RT_FORMAT_YUV420), + (1, 10) => Ok(VA_RT_FORMAT_YUV420_10), + (3, 8) => Ok(VA_RT_FORMAT_YUV444), + _ => Err(ConfigError::UnsupportedShape { + chroma_format_idc, + depth, + }), + } +} + +/// Headroom over the DPB for pictures the CONSUMER still holds. +/// +/// A surface handed to the presenter is not free to decode into — that is what +/// zero-copy costs — and a pool sized exactly to the DPB stalls the decoder behind +/// the display. +/// +/// **8, matching `pf_vkdecode::images::HOLD_HEADROOM`, and for its measurement**: +/// the real client pipeline holds roughly four to seven frames at steady state (two +/// bounded(2) channels, the frame store's 1..=3 preroll, the in-flight present and +/// the retired-frame slot), so eight leaves a frame of slack and a consumer holding +/// more than that has earned an honest "pool exhausted" rather than a silent stall. +/// The number was 4 when this module was written against no consumer; the native +/// Vulkan rung had already measured the pipeline by then, and 4 would have run the +/// pool dry on an ordinary stream. +/// +/// (The FFmpeg VAAPI rung asks libavcodec for `extra_hw_frames = 4` and survives on +/// it, but its pool is not this pool: `av_hwframe_get_buffer` BLOCKS until a surface +/// frees, so its headroom buys latency where ours buys correctness.) +pub const PRESENTER_HEADROOM: usize = 8; + +/// How many decode surfaces a session allocates: the DPB, plus the picture being +/// decoded, plus [`PRESENTER_HEADROOM`]. +/// +/// VAAPI reports no driver minimum to honour (DXVA's +/// `ConfigMinRenderTargetBuffCount` has no counterpart), so this is the whole rule. +/// +/// AV1 passes [`AV1_MAX_DPB_FRAMES`] here — the codec's constant, not a stream +/// property. +pub fn surface_count(max_dpb_frames: usize) -> usize { + max_dpb_frames + 1 + PRESENTER_HEADROOM +} + +/// AV1's DPB depth: `NUM_REF_FRAMES`, and a constant of the codec rather than +/// anything a sequence header says. +/// +/// The slot ledger adds the picture being decoded, so an AV1 session's ledger holds +/// nine — libavcodec's own `num_surfaces = 1 + 8` for this codec, before the +/// presenter headroom this rung adds on top. +pub const AV1_MAX_DPB_FRAMES: usize = 8; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn h264_8bit_420_is_high() { + let p = profile_for(Codec::H264, 1, 8).expect("the envelope's only H.264 shape"); + assert_eq!(p.value, VA_PROFILE_H264_HIGH); + } + + #[test] + fn hevc_picks_main_or_main10_by_depth() { + assert_eq!( + profile_for(Codec::H265, 1, 8).unwrap().value, + VA_PROFILE_HEVC_MAIN + ); + assert_eq!( + profile_for(Codec::H265, 1, 10).unwrap().value, + VA_PROFILE_HEVC_MAIN10 + ); + } + + /// AV1 Profile 0 is one enumerator for two depths, and the depth still has to + /// reach the surface pool through `rt_format` rather than through the profile. + #[test] + fn av1_profile0_covers_both_depths_and_the_format_is_what_differs() { + assert_eq!( + profile_for(Codec::Av1, 1, 8).unwrap().value, + VA_PROFILE_AV1_PROFILE0 + ); + assert_eq!( + profile_for(Codec::Av1, 1, 10).unwrap().value, + VA_PROFILE_AV1_PROFILE0 + ); + assert_ne!( + profile_for(Codec::Av1, 1, 8).unwrap().name, + profile_for(Codec::Av1, 1, 10).unwrap().name, + "the log must still say which depth the session was built for" + ); + assert_eq!(rt_format(1, 8).unwrap(), VA_RT_FORMAT_YUV420); + assert_eq!(rt_format(1, 10).unwrap(), VA_RT_FORMAT_YUV420_10); + } + + #[test] + fn shapes_outside_the_envelope_are_refused_not_guessed() { + // 10-bit H.264 (High10) and 4:4:4 both have header enumerators; neither is + // in this rung's envelope, and silently narrowing to an 8-bit profile is the + // class of bug that decodes to garbage instead of failing. + assert!(profile_for(Codec::H264, 1, 10).is_err()); + assert!(profile_for(Codec::H264, 3, 8).is_err()); + assert!(profile_for(Codec::H265, 3, 10).is_err()); + assert!(rt_format(1, 12).is_err()); + // AV1 Profile 1 (4:4:4) and Profile 2 (4:2:2 / 12-bit) have no rung here — + // `va_dec_av1.h` says the API itself is 8/10-bit 4:2:0 only — and + // monochrome, which the AV1 planner reports as chroma_format_idc 0, is + // refused rather than treated as 4:2:0's neighbour. + assert!(profile_for(Codec::Av1, 3, 8).is_err()); + assert!(profile_for(Codec::Av1, 3, 10).is_err()); + assert!(profile_for(Codec::Av1, 1, 12).is_err()); + assert!(profile_for(Codec::Av1, 0, 8).is_err()); + assert!(profile_for(Codec::Av1, 2, 8).is_err()); + } + + /// AV1's pool is sized from the codec's constant, and the ledger it implies is + /// the nine slots [`crate::pic_av1::plan_to_va_av1`] insists on. + #[test] + fn the_av1_pool_is_the_codecs_eight_slots_plus_the_current_picture() { + assert_eq!(AV1_MAX_DPB_FRAMES, pf_bitstream::av1::NUM_REF_SLOTS); + assert_eq!( + surface_count(AV1_MAX_DPB_FRAMES), + 8 + 1 + PRESENTER_HEADROOM + ); + } + + #[test] + fn rt_format_tracks_depth() { + assert_eq!(rt_format(1, 8).unwrap(), VA_RT_FORMAT_YUV420); + assert_eq!(rt_format(1, 10).unwrap(), VA_RT_FORMAT_YUV420_10); + } + + #[test] + fn the_surface_pool_covers_dpb_plus_current_plus_headroom() { + assert_eq!(surface_count(4), 4 + 1 + PRESENTER_HEADROOM); + assert_eq!(surface_count(16), 16 + 1 + PRESENTER_HEADROOM); + } + + /// The headroom must cover what the client pipeline actually holds, which the + /// native Vulkan rung measured before this crate existed. Pinning it to that + /// crate's constant means a future re-measurement moves both rungs together + /// instead of leaving this one quietly short. + #[test] + fn the_headroom_matches_the_pipeline_depth_the_vulkan_rung_measured() { + assert_eq!( + PRESENTER_HEADROOM, + pf_vkdecode::images::HOLD_HEADROOM as usize + ); + } +} diff --git a/crates/pf-vaadec/src/drm.rs b/crates/pf-vaadec/src/drm.rs new file mode 100644 index 00000000..3f83d6ae --- /dev/null +++ b/crates/pf-vaadec/src/drm.rs @@ -0,0 +1,466 @@ +//! The export descriptor — `vaExportSurfaceHandle`'s answer — and the walk that +//! turns it into the plane list a dmabuf import consumes. +//! +//! This is the one structure in the rung that the DRIVER writes and we read. Every +//! other buffer here is one we fill, where a wrong field is at worst refused; a +//! misread descriptor is plausible garbage — an fd taken from the middle of a +//! pitch, a plane count read out of a modifier's high word — and it imports +//! successfully into a texture of nonsense. So the layout is measured by +//! `layout-probe.c` like everything else, and the walk lives here, pure, where +//! macOS and the container run its tests. +//! +//! # The bug this walk exists to not repeat +//! +//! With `VA_EXPORT_SURFACE_SEPARATE_LAYERS` an NV12 surface comes back as **two +//! layers** — an `R8` luma layer and a `GR88` chroma layer, one plane each — not as +//! one two-plane `NV12` layer. Taking `layers[0]` and calling it the surface is how +//! this project once painted the screen green: the importer saw a single-plane R8 +//! texture and the chroma was simply gone. Hence [`flatten`]: every plane of every +//! layer, in declared order, and the surface's format comes from the descriptor's +//! own top-level `fourcc` rather than from any layer's component format. +//! +//! (The alternative, `VA_EXPORT_SURFACE_COMPOSED_LAYERS`, asks the driver for one +//! layer describing the whole surface. It is not universally implemented, and the +//! separate-layers form is what the FFmpeg VAAPI path this rung replaces has always +//! used — so it is the form the fleet's drivers are exercised on.) + +use std::os::raw::c_int; + +/// `VA_EXPORT_SURFACE_READ_ONLY` — the decoder keeps writing this surface's future +/// siblings; the consumer only samples. +pub const VA_EXPORT_SURFACE_READ_ONLY: u32 = 0x0001; + +/// `VA_EXPORT_SURFACE_SEPARATE_LAYERS` — one layer per plane (module docs). +pub const VA_EXPORT_SURFACE_SEPARATE_LAYERS: u32 = 0x0004; + +/// `VA_FOURCC_NV12` — identical to `DRM_FORMAT_NV12`; the two namespaces agree on +/// the packed-fourcc value, which is why the descriptor's `fourcc` can be handed +/// to a DRM importer unchanged. +pub const VA_FOURCC_NV12: u32 = 0x3231_564e; + +/// `VA_FOURCC_P010` — identical to `DRM_FORMAT_P010`. +pub const VA_FOURCC_P010: u32 = 0x3031_3050; + +/// Fixed array bounds in the descriptor, measured (`layout-probe.c`). +pub const MAX_OBJECTS: usize = 4; +pub const MAX_LAYERS: usize = 4; +pub const MAX_PLANES_PER_LAYER: usize = 4; + +/// One buffer object backing the surface: an fd we OWN and must close. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct VaDrmPrimeObject { + /// DRM PRIME fd. `c_int` because that is what the header says; the caller + /// wraps it in an `OwnedFd` the moment the export succeeds. + pub fd: c_int, + pub size: u32, + pub drm_format_modifier: u64, +} + +/// One layer: under `SEPARATE_LAYERS` this is a single plane with its own +/// component format. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct VaDrmPrimeLayer { + /// The LAYER's DRM format (`R8`, `GR88`, …) — a component format, never the + /// surface's. See the module docs. + pub drm_format: u32, + pub num_planes: u32, + pub object_index: [u32; MAX_PLANES_PER_LAYER], + pub offset: [u32; MAX_PLANES_PER_LAYER], + pub pitch: [u32; MAX_PLANES_PER_LAYER], +} + +/// `VADRMPRIMESurfaceDescriptor` (`va_drmcommon.h`). +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct VaDrmPrimeSurfaceDescriptor { + /// The SURFACE's fourcc (`VA_FOURCC_NV12`, `VA_FOURCC_P010`, …) — the combined + /// format, and the one a DRM importer wants. + pub fourcc: u32, + pub width: u32, + pub height: u32, + pub num_objects: u32, + pub objects: [VaDrmPrimeObject; MAX_OBJECTS], + pub num_layers: u32, + pub layers: [VaDrmPrimeLayer; MAX_LAYERS], +} + +impl VaDrmPrimeSurfaceDescriptor { + /// A zeroed descriptor for the driver to fill. + /// + /// Zero is not a valid `num_objects`/`num_layers`, so a driver that returns + /// success without writing anything is caught by [`flatten`] rather than read + /// as a surface with no planes. + pub fn zeroed() -> Self { + Self { + fourcc: 0, + width: 0, + height: 0, + num_objects: 0, + objects: [VaDrmPrimeObject { + fd: -1, + size: 0, + drm_format_modifier: 0, + }; MAX_OBJECTS], + num_layers: 0, + layers: [VaDrmPrimeLayer { + drm_format: 0, + num_planes: 0, + object_index: [0; MAX_PLANES_PER_LAYER], + offset: [0; MAX_PLANES_PER_LAYER], + pitch: [0; MAX_PLANES_PER_LAYER], + }; MAX_LAYERS], + } + } +} + +/// One plane of the flattened surface. `fd` is BORROWED from the descriptor's +/// object list — several planes routinely name the same object — so the caller +/// owns the objects and the planes reference them. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ExportedPlane { + pub fd: c_int, + pub offset: u32, + pub stride: u32, +} + +/// The flattened surface: what an importer needs, in plane order. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExportedSurface { + /// The combined DRM fourcc, from the descriptor's own top-level field. + pub fourcc: u32, + pub width: u32, + pub height: u32, + /// The tiling modifier. Every object must agree on it — see [`flatten`]. + pub modifier: u64, + /// Every plane of every layer, in declared order. + pub planes: Vec, + /// The fds the caller OWNS and must close, one per object. + pub object_fds: Vec, +} + +/// Why a descriptor cannot be read as a surface. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExportError { + /// Zero (a driver that "succeeded" without writing) or more than the arrays hold. + ObjectCount(u32), + LayerCount(u32), + PlaneCount { + layer: usize, + planes: u32, + }, + /// A plane named an object outside `num_objects` — reading it would take an fd + /// from uninitialised descriptor memory. + ObjectIndex { + layer: usize, + plane: usize, + index: u32, + }, + /// An object came back without a usable fd. + BadFd { + object: usize, + fd: c_int, + }, + /// The objects disagree on the tiling modifier. A dmabuf import takes ONE + /// modifier for the whole image, so importing plane 1 under plane 0's tiling + /// would decode the chroma as if it were laid out some other way. Every fleet + /// driver puts the whole surface in one BO; a driver that does not needs code + /// that does not exist yet, and must say so rather than guess. + MixedModifiers { + first: u64, + other: u64, + }, +} + +impl std::fmt::Display for ExportError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ExportError::ObjectCount(n) => { + write!( + f, + "descriptor declares {n} objects (want 1..={MAX_OBJECTS})" + ) + } + ExportError::LayerCount(n) => { + write!(f, "descriptor declares {n} layers (want 1..={MAX_LAYERS})") + } + ExportError::PlaneCount { layer, planes } => write!( + f, + "layer {layer} declares {planes} planes (want 1..={MAX_PLANES_PER_LAYER})" + ), + ExportError::ObjectIndex { + layer, + plane, + index, + } => write!( + f, + "layer {layer} plane {plane} names object {index}, which the descriptor \ + does not have" + ), + ExportError::BadFd { object, fd } => { + write!(f, "object {object} exported fd {fd}") + } + ExportError::MixedModifiers { first, other } => write!( + f, + "the surface's objects disagree on tiling ({first:#018x} vs {other:#018x}) — \ + a single-modifier import cannot express it" + ), + } + } +} + +impl std::error::Error for ExportError {} + +/// Flatten a descriptor into an importable surface: **every plane of every layer, +/// in declared order** (module docs). +/// +/// Validates before it walks, so a malformed descriptor is a typed refusal and +/// never an out-of-bounds read of the fixed arrays. The caller owns +/// [`ExportedSurface::object_fds`] on success; on failure it owns the descriptor's +/// fds and must close them itself — this function takes no ownership either way, +/// because it cannot know whether the export call succeeded. +pub fn flatten(desc: &VaDrmPrimeSurfaceDescriptor) -> Result { + let objects = desc.num_objects as usize; + if objects == 0 || objects > MAX_OBJECTS { + return Err(ExportError::ObjectCount(desc.num_objects)); + } + let layers = desc.num_layers as usize; + if layers == 0 || layers > MAX_LAYERS { + return Err(ExportError::LayerCount(desc.num_layers)); + } + for (i, o) in desc.objects[..objects].iter().enumerate() { + if o.fd < 0 { + return Err(ExportError::BadFd { + object: i, + fd: o.fd, + }); + } + } + let modifier = desc.objects[0].drm_format_modifier; + if let Some(o) = desc.objects[1..objects] + .iter() + .find(|o| o.drm_format_modifier != modifier) + { + return Err(ExportError::MixedModifiers { + first: modifier, + other: o.drm_format_modifier, + }); + } + + let mut planes = Vec::with_capacity(layers * 2); + for (l, layer) in desc.layers[..layers].iter().enumerate() { + let n = layer.num_planes as usize; + if n == 0 || n > MAX_PLANES_PER_LAYER { + return Err(ExportError::PlaneCount { + layer: l, + planes: layer.num_planes, + }); + } + for p in 0..n { + let index = layer.object_index[p]; + if index as usize >= objects { + return Err(ExportError::ObjectIndex { + layer: l, + plane: p, + index, + }); + } + planes.push(ExportedPlane { + fd: desc.objects[index as usize].fd, + offset: layer.offset[p], + stride: layer.pitch[p], + }); + } + } + + Ok(ExportedSurface { + fourcc: desc.fourcc, + width: desc.width, + height: desc.height, + modifier, + planes, + object_fds: desc.objects[..objects].iter().map(|o| o.fd).collect(), + }) +} + +// Measured by `layout-probe.c` against libva 2.23.0 headers, not transcribed. +const _: () = { + use std::mem::align_of; + use std::mem::offset_of; + use std::mem::size_of; + assert!(size_of::() == 16); + assert!(size_of::() == 56); + assert!(size_of::() == 312); + assert!(align_of::() == 8); + assert!(offset_of!(VaDrmPrimeSurfaceDescriptor, fourcc) == 0); + assert!(offset_of!(VaDrmPrimeSurfaceDescriptor, width) == 4); + assert!(offset_of!(VaDrmPrimeSurfaceDescriptor, height) == 8); + assert!(offset_of!(VaDrmPrimeSurfaceDescriptor, num_objects) == 12); + assert!(offset_of!(VaDrmPrimeSurfaceDescriptor, objects) == 16); + assert!(offset_of!(VaDrmPrimeSurfaceDescriptor, num_layers) == 80); + assert!(offset_of!(VaDrmPrimeSurfaceDescriptor, layers) == 84); +}; + +#[cfg(test)] +mod tests { + use super::*; + + /// `DRM_FORMAT_R8` / `DRM_FORMAT_GR88` — the component formats a driver reports + /// per layer for NV12 under `SEPARATE_LAYERS`. Present only to build the + /// realistic fixture; nothing in the walk reads them, which is the point. + const DRM_FORMAT_R8: u32 = 0x2038_5220; + const DRM_FORMAT_GR88: u32 = 0x3838_5247; + const MOD: u64 = 0x0200_0000_0180_1002; + + /// What radeonsi/iHD actually hand back for an NV12 decode surface: ONE object, + /// TWO layers of one plane each, chroma at a non-zero offset in the same buffer. + fn nv12_two_layers() -> VaDrmPrimeSurfaceDescriptor { + let mut d = VaDrmPrimeSurfaceDescriptor::zeroed(); + d.fourcc = VA_FOURCC_NV12; + d.width = 1920; + d.height = 1080; + d.num_objects = 1; + d.objects[0] = VaDrmPrimeObject { + fd: 7, + size: 1920 * 1088 * 3 / 2, + drm_format_modifier: MOD, + }; + d.num_layers = 2; + d.layers[0] = VaDrmPrimeLayer { + drm_format: DRM_FORMAT_R8, + num_planes: 1, + object_index: [0; 4], + offset: [0; 4], + pitch: [1920, 0, 0, 0], + }; + d.layers[1] = VaDrmPrimeLayer { + drm_format: DRM_FORMAT_GR88, + num_planes: 1, + object_index: [0; 4], + offset: [1920 * 1088, 0, 0, 0], + pitch: [1920, 0, 0, 0], + }; + d + } + + #[test] + fn both_layers_become_planes_and_the_surface_keeps_its_own_fourcc() { + let out = flatten(&nv12_two_layers()).expect("a well-formed NV12 export"); + // The green-screen regression, as an assertion: two planes, not one. + assert_eq!(out.planes.len(), 2, "chroma was dropped"); + assert_eq!( + out.fourcc, VA_FOURCC_NV12, + "the surface fourcc must come from the descriptor, not from layers[0].drm_format \ + ({DRM_FORMAT_R8:#010x})" + ); + assert_ne!(out.fourcc, DRM_FORMAT_R8); + assert_eq!(out.planes[0].offset, 0); + assert_eq!(out.planes[1].offset, 1920 * 1088); + // Both planes live in the SAME object, so both carry the same fd — and the + // caller must close it exactly once. + assert_eq!(out.planes[0].fd, 7); + assert_eq!(out.planes[1].fd, 7); + assert_eq!(out.object_fds, vec![7]); + assert_eq!(out.modifier, MOD); + } + + #[test] + fn a_multi_plane_layer_flattens_in_declared_order() { + // The COMPOSED-ish shape: one layer that declares both planes itself. The + // walk must handle it without caring which shape the driver chose. + let mut d = VaDrmPrimeSurfaceDescriptor::zeroed(); + d.fourcc = VA_FOURCC_P010; + d.num_objects = 2; + d.objects[0] = VaDrmPrimeObject { + fd: 11, + size: 64, + drm_format_modifier: MOD, + }; + d.objects[1] = VaDrmPrimeObject { + fd: 12, + size: 32, + drm_format_modifier: MOD, + }; + d.num_layers = 1; + d.layers[0] = VaDrmPrimeLayer { + drm_format: VA_FOURCC_P010, + num_planes: 2, + object_index: [0, 1, 0, 0], + offset: [0, 0, 0, 0], + pitch: [3840, 3840, 0, 0], + }; + let out = flatten(&d).expect("a well-formed two-object export"); + assert_eq!(out.planes.len(), 2); + assert_eq!(out.planes[0].fd, 11); + assert_eq!(out.planes[1].fd, 12); + assert_eq!(out.object_fds, vec![11, 12], "both objects must be closed"); + } + + #[test] + fn a_driver_that_wrote_nothing_is_refused_rather_than_read() { + let d = VaDrmPrimeSurfaceDescriptor::zeroed(); + assert_eq!(flatten(&d), Err(ExportError::ObjectCount(0))); + } + + #[test] + fn counts_past_the_arrays_are_refused_before_the_walk() { + let mut d = nv12_two_layers(); + d.num_objects = 5; + assert_eq!(flatten(&d), Err(ExportError::ObjectCount(5))); + let mut d = nv12_two_layers(); + d.num_layers = 9; + assert_eq!(flatten(&d), Err(ExportError::LayerCount(9))); + let mut d = nv12_two_layers(); + d.layers[1].num_planes = 5; + assert_eq!( + flatten(&d), + Err(ExportError::PlaneCount { + layer: 1, + planes: 5 + }) + ); + } + + #[test] + fn a_plane_naming_an_object_that_does_not_exist_is_refused() { + // `num_objects` is 1, so object_index 1 addresses descriptor memory the + // driver never wrote — an fd of -1 or worse, a stale one. + let mut d = nv12_two_layers(); + d.layers[1].object_index[0] = 1; + assert_eq!( + flatten(&d), + Err(ExportError::ObjectIndex { + layer: 1, + plane: 0, + index: 1 + }) + ); + } + + #[test] + fn objects_that_disagree_on_tiling_are_refused_not_averaged() { + let mut d = nv12_two_layers(); + d.num_objects = 2; + d.objects[1] = VaDrmPrimeObject { + fd: 8, + size: 16, + drm_format_modifier: 0, + }; + d.layers[1].object_index[0] = 1; + assert_eq!( + flatten(&d), + Err(ExportError::MixedModifiers { + first: MOD, + other: 0 + }) + ); + } + + #[test] + fn an_object_without_an_fd_is_refused() { + let mut d = nv12_two_layers(); + d.objects[0].fd = -1; + assert_eq!(flatten(&d), Err(ExportError::BadFd { object: 0, fd: -1 })); + } +} diff --git a/crates/pf-vaadec/src/lib.rs b/crates/pf-vaadec/src/lib.rs new file mode 100644 index 00000000..10e424b5 --- /dev/null +++ b/crates/pf-vaadec/src/lib.rs @@ -0,0 +1,152 @@ +//! Native VAAPI decode for the Linux clients — M6 (H.264/HEVC) and M7 (AV1) of the +//! native-decode program, and the VAAPI counterpart of [`pf_vkdecode`] and +//! `pf-dxvadec`. +//! +//! Like `pf-dxvadec`, this crate is the **CPU-testable half**: everything between +//! pf-bitstream's per-AU plan and the buffers a `vaRenderPicture` call delivers. It +//! links no libva, names no `VA*` handle type, and compiles on macOS and in the Linux +//! container — which is the point. The VAAPI rung itself is +//! `cfg(target_os = "linux")` code that only a box can build, so anything left inside +//! that boundary is verified by a remote `cargo check` and nothing more. +//! +//! - [`va`] / [`va_h265`] / [`va_av1`]: the libva decode buffer layouts, +//! **hand-declared**, with every size and offset measured off the real headers and +//! pinned as compile-time assertions. +//! - [`config`]: profile, render-target format and surface-count decisions. +//! - [`pic`] / [`pic_h265`] / [`pic_av1`]: one `AuPlan` into picture parameters, IQ +//! matrices and slice records (AV1: tile records, and no IQ matrix at all). +//! +//! # Status +//! +//! **All three codecs converted, and the rung is wired.** `pf-client-core`'s +//! `video_vaapi_native` dlopens libva and drives these buffers; this crate holds +//! everything decidable without a device — including [`drm`], the export +//! descriptor the driver writes back and the plane walk that reads it. +//! +//! ⚠ **Nothing here has decoded a frame.** The rung is pin-only +//! (`PUNKTFUNK_DECODER=native-vaapi`) and no VAAPI hardware has been reachable +//! during M7, so everything below is a CPU-side conversion checked against +//! libavcodec and against measured layouts, not against a picture. +//! +//! Five things this crate settled that a reader would otherwise have to re-derive: +//! +//! * **`slice_data_bit_offset` costs no new parsing.** VAAPI is the only one of the +//! three backends that wants a bit position — DXVA takes a byte offset, Vulkan +//! takes none — and the vendored parser already records exactly it as +//! `SliceHeader::header_bit_size`, because cros-codecs' own production backend is +//! VAAPI. Its definition matches field for field: computed as +//! `(nalu.size - emulation_prevention_bytes) * 8 - bits_left`, it counts from and +//! including the NAL header byte with emulation-prevention bytes removed, which is +//! what `VASliceParameterBufferH264` documents. +//! * **The slice data buffer starts at the NAL header byte**, so the start code is +//! skipped — `SlicePlan::data` is start-code-inclusive, and the prefix is three +//! OR four bytes (the real host emits four on 100% of access units), so it is +//! measured per slice rather than assumed. +//! * **`VAPictureParameterBufferH264::reference_frames` is the MARKED DPB**, not the +//! access unit's own lists — the same statement DXVA's `RefFrameList` makes, so it +//! is filled from pf-bitstream's per-AU `dpb_refs` snapshot. Vulkan's +//! `pReferenceSlots` is the opposite and takes the AU's own set; all three +//! conventions now have a written home. +//! * **Unlike DXVA's short-format slice control, VAAPI wants the per-slice reference +//! lists themselves** (`RefPicList0`/`RefPicList1`, 32 entries each, in 8.2.4.2 +//! order) and the prediction weight tables. One wrinkle handled in [`pic`]: the +//! vendored `PredWeightTable` stores `luma_offset_l0` as `[i8; 32]` but +//! `luma_offset_l1` as `[i16; 32]`, an upstream inconsistency, while libva wants +//! `i16` for both. +//! * **AV1's reference plumbing is a FIFTH convention**, and libva's AV1 buffers +//! break three of this rung's other habits: the "slice" parameter buffer is a TILE +//! parameter buffer, several of its records share ONE data buffer (the only place +//! `vaCreateBuffer`'s `num_elements` is not 1), and there is no IQ matrix buffer at +//! all. [`va_av1`] states the convention and what it was established from; +//! [`pic_av1`] is where it is applied. +//! +//! # Why the slot ledger is borrowed +//! +//! [`SlotMap`] comes from [`pf_vkdecode`] for the reason `pf-dxvadec`'s docs give at +//! length: it is not a Vulkan object but a ledger from +//! [`pf_bitstream::h264::PicId`] to hardware DPB slot indices, and it is as +//! API-agnostic as it is codec-agnostic. VAAPI's own indirection is one step longer — +//! a slot indexes the caller's surface table, because `VAPictureH264::picture_id` is +//! a `VASurfaceID` rather than an index — so the conversion will take that table as +//! a parameter and stay pure. + +pub mod config; +pub mod drm; +pub mod pic; +pub mod pic_av1; +pub mod pic_h265; +pub mod va; +pub mod va_av1; +pub mod va_h265; + +/// The DPB slot ledger — borrowed, not redefined (crate docs). +pub use pf_vkdecode::SlotError; +pub use pf_vkdecode::SlotMap; + +/// The AV1 planner and its plan. ⚠ Its `plan_au` returns a **`Vec`**: an AV1 access +/// unit is a TEMPORAL UNIT and may carry several frames, of which at most one +/// displays. +pub use pf_bitstream::av1::AuPlan as AuPlanAv1; +pub use pf_bitstream::av1::Av1Planner; +pub use pf_bitstream::av1::DpbUpdate as DpbUpdateAv1; +pub use pf_bitstream::av1::FrameType as FrameTypeAv1; +pub use pf_bitstream::av1::ParsedFrameHeader as ParsedFrameHeaderAv1; +pub use pf_bitstream::av1::ParsedSequenceHeader as ParsedSequenceHeaderAv1; +pub use pf_bitstream::av1::PicId as PicIdAv1; +pub use pf_bitstream::av1::PicturePlan as PicturePlanAv1; +pub use pf_bitstream::av1::PlanError as PlanErrorAv1; +pub use pf_bitstream::av1::PlanWarning as PlanWarningAv1; +pub use pf_bitstream::av1::NUM_REF_SLOTS; +/// The planners and plans this crate converts, re-exported so the Linux layer names +/// every type it touches through `pf_vaadec` — the same courtesy `pf-dxvadec` does +/// for the Windows layer. +pub use pf_bitstream::h264::AuPlan; +pub use pf_bitstream::h264::ColourDescription; +pub use pf_bitstream::h264::DisplayCrop; +pub use pf_bitstream::h264::H264Planner; +pub use pf_bitstream::h264::PlanError; +pub use pf_bitstream::h264::PlanWarning; +pub use pf_bitstream::h265::AuPlan as AuPlanH265; +pub use pf_bitstream::h265::H265Planner; +pub use pf_bitstream::h265::PlanError as PlanErrorH265; +pub use pf_bitstream::h265::PlanWarning as PlanWarningH265; +/// Which warnings mean the PICTURE is damaged — pf-vkdecode's one list, so all three +/// native rungs conceal on exactly the same predicate. +pub use pf_vkdecode::is_integrity_warning; +pub use pf_vkdecode::is_integrity_warning_av1; +pub use pf_vkdecode::is_integrity_warning_h265; + +pub use drm::flatten; +pub use drm::ExportError; +pub use drm::ExportedPlane; +pub use drm::ExportedSurface; +pub use drm::VaDrmPrimeSurfaceDescriptor; +pub use drm::VA_EXPORT_SURFACE_READ_ONLY; +pub use drm::VA_EXPORT_SURFACE_SEPARATE_LAYERS; +pub use drm::VA_FOURCC_NV12; +pub use drm::VA_FOURCC_P010; + +pub use config::profile_for; +pub use config::rt_format; +pub use config::surface_count; +pub use config::Codec; +pub use config::ConfigError; +pub use config::VaProfile; +pub use config::AV1_MAX_DPB_FRAMES; +pub use config::VA_ENTRYPOINT_VLD; +pub use pic::plan_to_va; +pub use pic::DecodePlanVa; +pub use pic::PlanToVaError; +pub use pic_av1::plan_to_va_av1; +pub use pic_av1::DecodePlanVaAv1; +pub use pic_av1::PlanToVaAv1Error; +pub use pic_av1::TileGroupVa; +pub use pic_h265::plan_to_va_h265; +pub use pic_h265::DecodePlanVaH265; +pub use pic_h265::PlanToVaH265Error; +pub use va::PicFieldsH264; +pub use va::SeqFieldsH264; +pub use va::VaIqMatrixBufferH264; +pub use va::VaPictureH264; +pub use va::VaPictureParameterBufferH264; +pub use va::VaSliceParameterBufferH264; diff --git a/crates/pf-vaadec/src/pic.rs b/crates/pf-vaadec/src/pic.rs new file mode 100644 index 00000000..a66d3825 --- /dev/null +++ b/crates/pf-vaadec/src/pic.rs @@ -0,0 +1,819 @@ +//! One [`AuPlan`] into the libva buffers a `vaRenderPicture` call carries: the +//! picture parameters, the inverse-quantization matrices and one slice-parameter +//! record per slice. +//! +//! The counterpart of `pf-dxvadec`'s `pic` module, and it follows the same +//! transaction discipline for the same reason — a half-applied DPB update is the +//! shape of a corrupt reference: +//! +//! 1. envelope and capacity are validated (read-only); +//! 2. references resolve against the PRE-removal state (read-only) — this access +//! unit's own end-of-picture marking can evict a picture its slices legitimately +//! reference; +//! 3. `removed` is applied, then the setup slot is assigned last. +//! +//! # Three things VAAPI wants that the other two backends do not +//! +//! **A bit offset.** `slice_data_bit_offset` is the position where `slice_data()` +//! begins, counted from and including the NAL header byte with emulation-prevention +//! bytes removed. DXVA takes a byte offset and Vulkan takes nothing. It costs no new +//! parsing: the vendored parser records exactly this as +//! `SliceHeader::header_bit_size` — `(nalu.size - epb) * 8 - bits_left` — because +//! cros-codecs' own production backend is VAAPI. +//! +//! **The slice data without its start code.** That bit offset is relative to the NAL +//! header byte, so the buffer must begin there. `SlicePlan::data` is +//! start-code-INCLUSIVE and the prefix is three OR four bytes (the real host emits +//! four on every access unit), so the prefix is measured per slice rather than +//! assumed — the same normalisation the Vulkan ring layer performs, and the same +//! defect class that made HEVC unplayable when it was skipped. +//! +//! **The per-slice reference lists.** DXVA's short-format slice control expresses no +//! lists at all — the hardware re-parses the slice header — but VAAPI wants +//! `RefPicList0`/`RefPicList1` in 8.2.4.2 order, which is precisely what +//! `SlicePlan::ref_list0`/`ref_list1` already carry. +//! +//! # Two reference sets, and they are not the same set +//! +//! `VAPictureParameterBufferH264::reference_frames` is documented as "in DPB": a +//! statement about the decoded picture buffer, exactly like DXVA's `RefFrameList` +//! and exactly UNLIKE Vulkan's `pReferenceSlots` (the slots THIS operation uses). +//! It is therefore filled from the planner's per-AU `dpb_refs` snapshot — the marked +//! DPB — while the per-slice lists come from the slice's own derived lists. Getting +//! that backwards loses a long-term reference no slice happens to name, which is the +//! failure this program has already paid for on the DXVA side. + +use std::ops::Range; + +use pf_bitstream::h264::AuPlan; +use pf_bitstream::h264::PicId; +use pf_bitstream::h264::RefPic; + +use crate::va::PicFieldsH264; +use crate::va::SeqFieldsH264; +use crate::va::VaIqMatrixBufferH264; +use crate::va::VaPictureH264; +use crate::va::VaPictureParameterBufferH264; +use crate::va::VaSliceParameterBufferH264; +use crate::va::VA_PICTURE_H264_LONG_TERM_REFERENCE; +use crate::va::VA_PICTURE_H264_SHORT_TERM_REFERENCE; +use crate::va::VA_SLICE_DATA_FLAG_ALL; +use crate::SlotError; +use crate::SlotMap; + +/// `VAPictureParameterBufferH264::reference_frames` length, and the H.264 DPB +/// ceiling — the two coincide, which is why an overflow here means a malformed plan +/// rather than an expressiveness limit. +pub const REFERENCE_FRAMES_LEN: usize = 16; + +/// `RefPicList0`/`RefPicList1` length. +pub const REF_PIC_LIST_LEN: usize = 32; + +/// Everything one `vaBeginPicture`/`vaRenderPicture`/`vaEndPicture` sequence needs. +#[derive(Debug, Clone)] +pub struct DecodePlanVa { + pub pic_params: VaPictureParameterBufferH264, + pub iq_matrix: VaIqMatrixBufferH264, + /// One record per slice, in bitstream order. + pub slices: Vec, + /// Each slice's data range in the access unit, **start code excluded** — what + /// the matching `VASliceDataBuffer` carries. Parallel to [`Self::slices`]. + pub slice_data: Vec>, + /// The DPB slot this picture decodes into; index it into the caller's surface + /// table to get the `VASurfaceID`. + pub setup_slot: u8, +} + +/// Why a plan cannot be expressed as VAAPI buffers. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanToVaError { + NoSlices, + NoStoredId, + /// FMO. Refused rather than ignored: the deprecated fields exist in the struct + /// but no driver implements slice groups. + SliceGroups { + count: u32, + }, + SeparateColourPlanes, + CapacityMismatch { + required: usize, + capacity: usize, + }, + /// A slice named a reference the slot map does not hold. + UnresolvedReference(PicId), + /// More marked references than `reference_frames` can express. + TooManyReferences(usize), + /// A slice's derived list is longer than `RefPicList0`/`1`. + RefListTooLong { + slice: usize, + len: usize, + }, + /// The slot map's slot has no entry in the caller's surface table. + SurfaceOutOfRange { + slot: u8, + surfaces: usize, + }, + /// The picture is larger than the macroblock counters can express. + DimensionOverflow { + width_mbs: u32, + height_mbs: u32, + }, + /// A slice's byte range is not inside the access unit, or carries no Annex-B + /// start code where one is required. + SliceRange { + slice: usize, + }, + /// `header_bit_size` does not fit `slice_data_bit_offset`'s 16 bits. Only + /// reachable from an absurd slice header, and an error rather than a truncation + /// because a wrong bit offset decodes garbage. + SliceBitOffsetOverflow { + slice: usize, + bits: usize, + }, + Slot(SlotError), +} + +impl From for PlanToVaError { + fn from(e: SlotError) -> Self { + PlanToVaError::Slot(e) + } +} + +impl std::fmt::Display for PlanToVaError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlanToVaError::NoSlices => write!(f, "the access unit planned no slices"), + PlanToVaError::NoStoredId => write!(f, "the plan stored no picture id"), + PlanToVaError::SliceGroups { count } => { + write!(f, "slice groups (FMO) are outside the envelope: {count}") + } + PlanToVaError::SeparateColourPlanes => { + write!(f, "separate colour planes are outside the envelope") + } + PlanToVaError::CapacityMismatch { required, capacity } => write!( + f, + "the slot map holds {capacity} slots, this stream needs {required}" + ), + PlanToVaError::UnresolvedReference(id) => { + write!(f, "reference picture {id} holds no DPB slot") + } + PlanToVaError::TooManyReferences(n) => { + write!(f, "{n} marked references exceed reference_frames[16]") + } + PlanToVaError::RefListTooLong { slice, len } => { + write!(f, "slice {slice}: reference list of {len} exceeds 32") + } + PlanToVaError::SurfaceOutOfRange { slot, surfaces } => { + write!(f, "DPB slot {slot} has no surface in a table of {surfaces}") + } + PlanToVaError::DimensionOverflow { + width_mbs, + height_mbs, + } => write!( + f, + "picture of {width_mbs}x{height_mbs} macroblocks is too large" + ), + PlanToVaError::SliceRange { slice } => { + write!( + f, + "slice {slice}: byte range is not a start-code-prefixed NAL" + ) + } + PlanToVaError::SliceBitOffsetOverflow { slice, bits } => { + write!( + f, + "slice {slice}: header of {bits} bits exceeds 16-bit offset" + ) + } + PlanToVaError::Slot(e) => write!(f, "DPB slot map: {e:?}"), + } + } +} + +impl std::error::Error for PlanToVaError {} + +/// The Annex-B start-code length at the front of `bytes` (3 or 4), or `None`. +pub(crate) fn start_code_len(bytes: &[u8]) -> Option { + if bytes.starts_with(&[0x00, 0x00, 0x00, 0x01]) { + Some(4) + } else if bytes.starts_with(&[0x00, 0x00, 0x01]) { + Some(3) + } else { + None + } +} + +/// One `VAPictureH264` for a reference picture already resolved to a slot. +fn va_ref(rp: &RefPic, surface: u32) -> VaPictureH264 { + VaPictureH264 { + picture_id: surface, + frame_idx: u32::from(rp.frame_num_or_lt_idx), + flags: if rp.is_long_term { + VA_PICTURE_H264_LONG_TERM_REFERENCE + } else { + VA_PICTURE_H264_SHORT_TERM_REFERENCE + }, + top_field_order_cnt: rp.top_field_order_cnt, + bottom_field_order_cnt: rp.bottom_field_order_cnt, + va_reserved: [0; 4], + } +} + +/// Resolve `id` to its surface, or say which id could not be resolved. +fn surface_of(slots: &SlotMap, surfaces: &[u32], id: PicId) -> Result<(u8, u32), PlanToVaError> { + let slot = slots + .slot_of(id) + .ok_or(PlanToVaError::UnresolvedReference(id))?; + let surface = *surfaces + .get(usize::from(slot)) + .ok_or(PlanToVaError::SurfaceOutOfRange { + slot, + surfaces: surfaces.len(), + })?; + Ok((slot, surface)) +} + +/// Convert one planned access unit. +/// +/// `au` is the access unit the plan was built from — needed because the slice data +/// buffer must start at the NAL header byte, and the start-code prefix is three or +/// four bytes depending on the encoder. `surfaces` maps DPB slot to `VASurfaceID` +/// for the pictures the DPB already holds; this crate never allocates one. +/// +/// # Why the decode target is a parameter and not `surfaces[setup_slot]` +/// +/// The caller binds the target surface, at activation time, exactly as +/// `pf-vkdecode` binds a pool image when a DPB slot is activated. A slot ledger +/// is not a surface allocator: [`SlotMap::assign`] takes the lowest free slot, and +/// a slot freed by this AU's own removals is free by the time the setup picture +/// takes it — measured at **225 of the vendored vector's 250 access units** +/// (`the_setup_picture_routinely_inherits_a_just_freed_slot`). Reading the target +/// out of a slot-indexed table would therefore decode, on nine frames in ten, +/// into the surface holding the picture that was just displayed — which the +/// consumer may still be sampling. Zero-copy means the decoder cannot have that +/// surface back until the consumer says so, and only the caller knows. +/// +/// `setup_surface` must be free in that sense: bound to no live picture and held +/// by no consumer. After a successful call the caller binds it to +/// [`DecodePlanVa::setup_slot`], so later access units resolve references to this +/// picture through `surfaces`. +/// +/// Nothing mutates `slots` until every fallible step has passed. +pub fn plan_to_va( + plan: &AuPlan, + au: &[u8], + slots: &mut SlotMap, + surfaces: &[u32], + setup_surface: u32, +) -> Result { + if plan.slices.is_empty() { + return Err(PlanToVaError::NoSlices); + } + let setup_id = plan.dpb.stored.ok_or(PlanToVaError::NoStoredId)?; + let sps = &plan.sps; + let pps = &plan.pps; + let pic = &plan.picture; + + if pps.num_slice_groups_minus1 != 0 { + return Err(PlanToVaError::SliceGroups { + count: pps.num_slice_groups_minus1 + 1, + }); + } + if sps.separate_colour_plane_flag { + return Err(PlanToVaError::SeparateColourPlanes); + } + + let required = pic.max_dpb_frames + 1; + if slots.capacity() != required { + return Err(PlanToVaError::CapacityMismatch { + required, + capacity: slots.capacity(), + }); + } + // The caller binds `setup_surface` to the returned slot, so a table that cannot + // express every slot is caught HERE — before anything mutates the ledger — + // rather than as an out-of-range bind after a successful conversion. Checked + // against the capacity, not the chosen slot, precisely so it stays a pre-check. + if surfaces.len() < slots.capacity() { + return Err(PlanToVaError::SurfaceOutOfRange { + slot: (slots.capacity() - 1) as u8, + surfaces: surfaces.len(), + }); + } + + // Height is expressed in FRAME macroblocks, so the map-units count doubles for a + // non-frame-only SPS — unreachable inside pf-bitstream's progressive envelope, + // written out so the expression says what the spec says. + let width_mbs = u32::from(sps.pic_width_in_mbs_minus1) + 1; + let height_mbs = (u32::from(sps.pic_height_in_map_units_minus1) + 1) + * (2 - u32::from(sps.frame_mbs_only_flag)); + let (Ok(width_minus1), Ok(height_minus1)) = ( + u16::try_from(width_mbs.saturating_sub(1)), + u16::try_from(height_mbs.saturating_sub(1)), + ) else { + return Err(PlanToVaError::DimensionOverflow { + width_mbs, + height_mbs, + }); + }; + + // --- read-only resolution, against the PRE-removal slot map ------------- + + // The marked DPB, in the planner's order — `reference_frames` is a statement + // about the DPB, not about this access unit (module docs). + if plan.dpb_refs.len() > REFERENCE_FRAMES_LEN { + return Err(PlanToVaError::TooManyReferences(plan.dpb_refs.len())); + } + let mut reference_frames = [VaPictureH264::invalid(); REFERENCE_FRAMES_LEN]; + for (slot_out, rp) in reference_frames.iter_mut().zip(&plan.dpb_refs) { + let (_, surface) = surface_of(slots, surfaces, rp.id)?; + *slot_out = va_ref(rp, surface); + } + + // Per-slice derived lists, in 8.2.4.2 order. + let mut slices = Vec::with_capacity(plan.slices.len()); + let mut slice_data = Vec::with_capacity(plan.slices.len()); + for (index, sp) in plan.slices.iter().enumerate() { + let hdr = &sp.header; + let mut rec = VaSliceParameterBufferH264::zeroed(); + + let bytes = au + .get(sp.data.clone()) + .ok_or(PlanToVaError::SliceRange { slice: index })?; + let prefix = start_code_len(bytes).ok_or(PlanToVaError::SliceRange { slice: index })?; + let payload = sp.data.start + prefix..sp.data.end; + rec.slice_data_size = (payload.end - payload.start) as u32; + rec.slice_data_offset = 0; + rec.slice_data_flag = VA_SLICE_DATA_FLAG_ALL; + rec.slice_data_bit_offset = u16::try_from(hdr.header_bit_size).map_err(|_| { + PlanToVaError::SliceBitOffsetOverflow { + slice: index, + bits: hdr.header_bit_size, + } + })?; + slice_data.push(payload); + + rec.first_mb_in_slice = hdr.first_mb_in_slice as u16; + rec.slice_type = hdr.slice_type as u8; + rec.direct_spatial_mv_pred_flag = u8::from(hdr.direct_spatial_mv_pred_flag); + rec.num_ref_idx_l0_active_minus1 = hdr.num_ref_idx_l0_active_minus1; + rec.num_ref_idx_l1_active_minus1 = hdr.num_ref_idx_l1_active_minus1; + rec.cabac_init_idc = hdr.cabac_init_idc; + rec.slice_qp_delta = hdr.slice_qp_delta; + rec.disable_deblocking_filter_idc = hdr.disable_deblocking_filter_idc; + rec.slice_alpha_c0_offset_div2 = hdr.slice_alpha_c0_offset_div2; + rec.slice_beta_offset_div2 = hdr.slice_beta_offset_div2; + + for (list_out, list_in) in [ + (&mut rec.ref_pic_list0, &sp.ref_list0), + (&mut rec.ref_pic_list1, &sp.ref_list1), + ] { + if list_in.len() > REF_PIC_LIST_LEN { + return Err(PlanToVaError::RefListTooLong { + slice: index, + len: list_in.len(), + }); + } + for (entry, rp) in list_out.iter_mut().zip(list_in) { + // The marked snapshot is the authority for the marking and the + // pair-key: a list entry may be a concealment substitute relabelled + // short-term. Falling back to the entry's own copy is honest if the + // DPB does not hold it. + let marked = plan.dpb_refs.iter().find(|d| d.id == rp.id); + let (_, surface) = surface_of(slots, surfaces, rp.id)?; + *entry = va_ref(marked.unwrap_or(rp), surface); + } + } + + // 7.3.3: an explicit weight table is parsed for L0 when the PPS enables + // weighted P prediction on a P/SP slice, and for both lists when + // weighted_bipred_idc == 1 on a B slice. Anywhere else the arrays are not + // meaningful, and flagging them would hand the driver defaults as if the + // stream had coded them. + let pwt = &hdr.pred_weight_table; + let explicit_l0 = (pps.weighted_pred_flag + && (hdr.slice_type.is_p() || hdr.slice_type.is_sp())) + || (pps.weighted_bipred_idc == 1 && hdr.slice_type.is_b()); + let explicit_l1 = pps.weighted_bipred_idc == 1 && hdr.slice_type.is_b(); + if explicit_l0 || explicit_l1 { + rec.luma_log2_weight_denom = pwt.luma_log2_weight_denom; + rec.chroma_log2_weight_denom = pwt.chroma_log2_weight_denom; + } + if explicit_l0 { + rec.luma_weight_l0_flag = 1; + rec.chroma_weight_l0_flag = 1; + rec.luma_weight_l0 = pwt.luma_weight_l0; + // The vendored table stores L0 offsets as i8 and L1 offsets as i16 — an + // upstream inconsistency, not a semantic difference; libva wants i16 for + // both, so the narrow side widens. + for (out, v) in rec.luma_offset_l0.iter_mut().zip(pwt.luma_offset_l0) { + *out = i16::from(v); + } + rec.chroma_weight_l0 = pwt.chroma_weight_l0; + for (out, v) in rec.chroma_offset_l0.iter_mut().zip(pwt.chroma_offset_l0) { + *out = [i16::from(v[0]), i16::from(v[1])]; + } + } + if explicit_l1 { + rec.luma_weight_l1_flag = 1; + rec.chroma_weight_l1_flag = 1; + rec.luma_weight_l1 = pwt.luma_weight_l1; + rec.luma_offset_l1 = pwt.luma_offset_l1; + rec.chroma_weight_l1 = pwt.chroma_weight_l1; + for (out, v) in rec.chroma_offset_l1.iter_mut().zip(pwt.chroma_offset_l1) { + *out = [i16::from(v[0]), i16::from(v[1])]; + } + } + + slices.push(rec); + } + + // --- mutations, after every fallible step ------------------------------- + + // The AU's own picture can appear in `removed`: a non-reference picture with no + // free frame buffer is stored and evicted within one plan. Its surface must + // still exist for the decode, so it is assigned here and released right after. + let setup_evicted = plan.dpb.removed.contains(&setup_id); + for &id in &plan.dpb.removed { + if id == setup_id { + continue; + } + let _ = slots.release(id); + } + let setup_slot = slots.assign(setup_id)?; + if setup_evicted { + slots.release(setup_id); + } + + let curr_pic = VaPictureH264 { + picture_id: setup_surface, + // For the current picture this is `frame_num`, not a long-term index. + frame_idx: u32::from(pic.frame_num), + flags: if pic.is_reference { + VA_PICTURE_H264_SHORT_TERM_REFERENCE + } else { + 0 + }, + top_field_order_cnt: pic.top_field_order_cnt, + bottom_field_order_cnt: pic.bottom_field_order_cnt, + va_reserved: [0; 4], + }; + + let seq_fields = SeqFieldsH264 { + chroma_format_idc: sps.chroma_format_idc, + separate_colour_plane_flag: sps.separate_colour_plane_flag, + gaps_in_frame_num_value_allowed_flag: sps.gaps_in_frame_num_value_allowed_flag, + frame_mbs_only_flag: sps.frame_mbs_only_flag, + mb_adaptive_frame_field_flag: sps.mb_adaptive_frame_field_flag, + direct_8x8_inference_flag: sps.direct_8x8_inference_flag, + // A.3.3.2 is a level-derived constraint, and libavcodec's VAAPI backend + // leaves it 0 for every stream it sends; matching the path drivers are + // validated against beats deriving a value nobody consumes. + min_luma_bi_pred_size8x8: false, + log2_max_frame_num_minus4: sps.log2_max_frame_num_minus4, + pic_order_cnt_type: sps.pic_order_cnt_type, + log2_max_pic_order_cnt_lsb_minus4: sps.log2_max_pic_order_cnt_lsb_minus4, + delta_pic_order_always_zero_flag: sps.delta_pic_order_always_zero_flag, + }; + let pic_fields = PicFieldsH264 { + entropy_coding_mode_flag: pps.entropy_coding_mode_flag, + weighted_pred_flag: pps.weighted_pred_flag, + weighted_bipred_idc: pps.weighted_bipred_idc, + transform_8x8_mode_flag: pps.transform_8x8_mode_flag, + // Progressive envelope: pf-bitstream rejects field coding before a plan + // exists, so this is a constant rather than a read. + field_pic_flag: false, + constrained_intra_pred_flag: pps.constrained_intra_pred_flag, + pic_order_present_flag: pps.bottom_field_pic_order_in_frame_present_flag, + deblocking_filter_control_present_flag: pps.deblocking_filter_control_present_flag, + redundant_pic_cnt_present_flag: pps.redundant_pic_cnt_present_flag, + reference_pic_flag: pic.is_reference, + }; + + let pic_params = VaPictureParameterBufferH264 { + curr_pic, + reference_frames, + picture_width_in_mbs_minus1: width_minus1, + picture_height_in_mbs_minus1: height_minus1, + bit_depth_luma_minus8: pic.bit_depth_luma_minus8, + bit_depth_chroma_minus8: pic.bit_depth_chroma_minus8, + num_ref_frames: sps.max_num_ref_frames, + seq_fields: seq_fields.pack(), + num_slice_groups_minus1: 0, + slice_group_map_type: 0, + slice_group_change_rate_minus1: 0, + pic_init_qp_minus26: pps.pic_init_qp_minus26, + pic_init_qs_minus26: pps.pic_init_qs_minus26, + chroma_qp_index_offset: pps.chroma_qp_index_offset, + second_chroma_qp_index_offset: pps.second_chroma_qp_index_offset, + pic_fields: pic_fields.pack(), + frame_num: pic.frame_num, + va_reserved: [0; 8], + }; + + // The PPS lists are the EFFECTIVE ones: the parser has already applied Table 7-2's + // fallback rules, so no SPS/PPS merge happens here. + let iq_matrix = VaIqMatrixBufferH264 { + scaling_list4x4: pps.scaling_lists_4x4, + // libva carries only the two 8x8 lists a 4:2:0 stream uses; the parser keeps + // six (the 4:4:4 set). + scaling_list8x8: [pps.scaling_lists_8x8[0], pps.scaling_lists_8x8[1]], + va_reserved: [0; 4], + }; + + Ok(DecodePlanVa { + pic_params, + iq_matrix, + slices, + slice_data, + setup_slot, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::va::VA_INVALID_SURFACE; + + /// Surface ids for the walks below: `SURFACE_BASE + access-unit index`, so every + /// picture gets its own and none is ever reused. Well away from slot indices, so + /// a mix-up shows as a value rather than a plausible off-by-one — and unique, so + /// a stale or aliased reference cannot hide behind a surface that happens to be + /// right again. + const SURFACE_BASE: u32 = 0x9000; + use crate::va::VA_PICTURE_H264_INVALID; + + /// The vendored conformance vector every other rung's parity legs decode: 250 + /// access units, two slice NALUs per picture, four IDRs, real reordering. + const TEST_25FPS_H264: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264" + ); + + /// Minimal H.264 access-unit splitter. The production wire delivers whole access + /// units, so pf-bitstream keeps its splitter test-only; this is the same rule — + /// a new AU begins at a non-VCL NALU following slices, or at a slice declaring + /// itself first-in-picture — and the access-unit count asserted below is what + /// keeps it honest. + fn split_aus(stream: &[u8]) -> Vec<&[u8]> { + let mut aus = Vec::new(); + let (mut au_start, mut au_has_slice) = (0usize, false); + let mut i = 0usize; + while i + 3 <= stream.len() { + if stream[i..i + 3] != [0x00, 0x00, 0x01] { + i += 1; + continue; + } + let header = i + 3; + let mut start = i; + if start > 0 && stream[start - 1] == 0x00 { + start -= 1; + } + let is_slice = matches!(stream[header] & 0x1f, 1 | 5); + let first = is_slice && stream.get(header + 1).is_some_and(|b| b & 0x80 != 0); + if au_has_slice && (!is_slice || first) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + i += 3; + } + aus.push(&stream[au_start..]); + aus + } + + /// Every access unit of a real stream converts, and the parts a driver reads are + /// self-consistent. + /// + /// The unit tests above check one field at a time; this is the one that would + /// notice a transaction ordering mistake, a slot exhausted mid-stream, or a + /// slice range that walked off its access unit — none of which a synthetic + /// single-picture case reaches. + #[test] + fn the_whole_vendored_vector_converts() { + use pf_bitstream::h264::H264Planner; + + let aus = split_aus(TEST_25FPS_H264); + assert_eq!(aus.len(), 250, "the vendored vector is 250 access units"); + + let mut planner = H264Planner::new(); + // The caller's binding, modelled the way the rung does it: every picture is + // given its OWN never-reused surface id, and the slot table is updated after + // the conversion returns. Ids start well away from slot indices so a mix-up + // shows up as a value rather than as a plausible-looking off-by-one, and + // never reusing one means a stale or aliased reference cannot hide behind a + // surface that happens to be right again. + let mut surfaces: Vec = Vec::new(); + let mut slots: Option = None; + let mut converted = 0usize; + let mut saw_multi_slice = false; + let mut saw_references = false; + + for (index, au) in aus.iter().enumerate() { + let plan = planner + .plan_au(au) + .unwrap_or_else(|e| panic!("AU {index}: the clean vector must plan, got {e:?}")); + let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames)); + surfaces.resize(map.capacity(), VA_INVALID_SURFACE); + let setup_surface = SURFACE_BASE + index as u32; + let out = plan_to_va(&plan, au, map, &surfaces, setup_surface) + .unwrap_or_else(|e| panic!("AU {index}: conversion failed: {e}")); + surfaces[usize::from(out.setup_slot)] = setup_surface; + + assert_eq!( + out.slices.len(), + plan.slices.len(), + "AU {index}: one record per slice" + ); + assert_eq!(out.slice_data.len(), out.slices.len()); + saw_multi_slice |= out.slices.len() > 1; + + for (n, (rec, range)) in out.slices.iter().zip(&out.slice_data).enumerate() { + assert!( + range.end <= au.len() && range.start < range.end, + "AU {index} slice {n}: range {range:?} is not inside a {}-byte AU", + au.len() + ); + assert_eq!( + rec.slice_data_size as usize, + range.end - range.start, + "AU {index} slice {n}: declared size must match the range" + ); + // The payload begins at the NAL header byte: no start code left. + assert_ne!( + &au[range.start..range.start + 3.min(range.end - range.start)], + &[0x00, 0x00, 0x01][..], + "AU {index} slice {n}: the start code was not trimmed" + ); + assert!( + rec.slice_data_bit_offset > 0, + "AU {index} slice {n}: a slice header cannot be zero bits" + ); + assert!( + usize::from(rec.slice_data_bit_offset) < (range.end - range.start) * 8, + "AU {index} slice {n}: the header cannot outrun the slice" + ); + } + + // `reference_frames` mirrors the marked DPB exactly: as many valid + // entries as the snapshot has, and every entry past it invalidated. + let valid = out + .pic_params + .reference_frames + .iter() + .filter(|e| e.flags & VA_PICTURE_H264_INVALID == 0) + .count(); + assert_eq!( + valid, + plan.dpb_refs.len(), + "AU {index}: reference_frames must carry the marked DPB and nothing else" + ); + saw_references |= valid > 0; + for e in out.pic_params.reference_frames.iter().take(valid) { + assert!( + surfaces.contains(&e.picture_id), + "AU {index}: a reference names a surface outside the table" + ); + } + + assert_eq!(out.pic_params.frame_num, plan.picture.frame_num); + assert!(usize::from(out.setup_slot) < surfaces.len()); + converted += 1; + } + + assert_eq!(converted, 250); + assert!( + saw_multi_slice, + "this vector is two slices per picture — a run that never saw one is \ + splitting access units wrong" + ); + assert!( + saw_references, + "a 250-frame vector must reference something" + ); + } + + /// The decode target must never be a surface this same access unit READS. + /// + /// This is the question a slot ledger cannot answer, and it is why the caller + /// binds the setup surface instead of the conversion reading one out of a + /// slot-indexed table. + /// + /// `SlotMap::assign` takes the LOWEST free slot, and a slot freed by this + /// access unit's own removals is free by the time the setup picture is + /// assigned. Measured on the vendored vector, that is not an edge case: the + /// setup picture inherits a just-freed slot on **225 of 250** access units. + /// A surface bound BY SLOT would therefore decode, on nine frames in ten, + /// into the surface still holding the picture that was just displayed — which + /// under zero-copy the consumer may still be sampling. Hence the pool model + /// this crate's callers use, and hence `setup_surface`. + /// + /// The second half of the test is the reassurance that comes with it: given + /// the caller's contract (a surface bound to no live picture), the decode + /// target is never a surface the same access unit READS. That is checked + /// against both readable sets, which are not the same snapshot — `dpb_refs` is + /// taken after this AU's marking process, the per-slice lists before it. + #[test] + fn the_setup_picture_routinely_inherits_a_just_freed_slot() { + use pf_bitstream::h264::H264Planner; + + let aus = split_aus(TEST_25FPS_H264); + let mut planner = H264Planner::new(); + let mut surfaces: Vec = Vec::new(); + let mut slots: Option = None; + let mut collisions = 0usize; + let mut first: Option = None; + let mut inherited = 0usize; + + for (index, au) in aus.iter().enumerate() { + let plan = planner.plan_au(au).expect("the clean vector plans"); + let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames)); + surfaces.resize(map.capacity(), VA_INVALID_SURFACE); + // Which slots this AU's own removals will free — read BEFORE the + // conversion applies them, because afterwards the ledger has forgotten. + let freed: Vec = plan + .dpb + .removed + .iter() + .filter_map(|id| map.slot_of(*id)) + .collect(); + let setup_surface = SURFACE_BASE + index as u32; + let out = plan_to_va(&plan, au, map, &surfaces, setup_surface) + .expect("the clean vector converts"); + surfaces[usize::from(out.setup_slot)] = setup_surface; + if freed.contains(&out.setup_slot) { + inherited += 1; + } + let curr = out.pic_params.curr_pic.picture_id; + let names = + |e: &VaPictureH264| e.flags & VA_PICTURE_H264_INVALID == 0 && e.picture_id == curr; + let read_by_this_au = out.pic_params.reference_frames.iter().any(names) + || out.slices.iter().any(|s| { + s.ref_pic_list0.iter().any(names) || s.ref_pic_list1.iter().any(names) + }); + if read_by_this_au { + collisions += 1; + first.get_or_insert(index); + } + } + // The measurement this design rests on. A floor rather than the exact + // count, so a planner change that shifts it by a frame does not fail — + // but one that made slot reuse RARE would, and would mean the doc above + // has stopped being true. + assert!( + inherited > 200, + "the setup picture inherited a just-freed slot on only {inherited} of 250 access \ + units — the reason `setup_surface` is a parameter no longer holds, and the \ + documentation that cites it needs re-measuring" + ); + assert_eq!( + collisions, 0, + "the decode target collided with a picture this access unit reads, on \ + {collisions} of 250 (first at AU {first:?})" + ); + } + + #[test] + fn start_code_len_reads_both_prefix_forms() { + assert_eq!(start_code_len(&[0, 0, 1, 0x65]), Some(3)); + assert_eq!(start_code_len(&[0, 0, 0, 1, 0x65]), Some(4)); + // A NAL handed over WITHOUT its prefix must not be mistaken for one: the + // bit offset is relative to the header byte, so trimming the wrong number + // of bytes shifts every slice. + assert_eq!(start_code_len(&[0x65, 0x88]), None); + assert_eq!(start_code_len(&[0, 0, 2, 1]), None); + assert_eq!(start_code_len(&[0, 0]), None); + } + + #[test] + fn a_long_term_reference_is_flagged_long_term() { + let rp = RefPic { + id: 7, + top_field_order_cnt: 4, + bottom_field_order_cnt: 4, + is_long_term: true, + frame_num_or_lt_idx: 2, + }; + let e = va_ref(&rp, 0x1234); + assert_eq!(e.flags, VA_PICTURE_H264_LONG_TERM_REFERENCE); + assert_eq!(e.picture_id, 0x1234); + // For a long-term picture this field carries LongTermFrameIdx, not frame_num. + assert_eq!(e.frame_idx, 2); + } + + #[test] + fn a_short_term_reference_carries_its_frame_num() { + let rp = RefPic { + id: 3, + top_field_order_cnt: -2, + bottom_field_order_cnt: -2, + is_long_term: false, + frame_num_or_lt_idx: 9, + }; + let e = va_ref(&rp, 5); + assert_eq!(e.flags, VA_PICTURE_H264_SHORT_TERM_REFERENCE); + assert_eq!(e.frame_idx, 9); + assert_eq!(e.top_field_order_cnt, -2); + assert_ne!(e.flags & VA_PICTURE_H264_INVALID, VA_PICTURE_H264_INVALID); + } +} diff --git a/crates/pf-vaadec/src/pic_av1.rs b/crates/pf-vaadec/src/pic_av1.rs new file mode 100644 index 00000000..d86a2cd3 --- /dev/null +++ b/crates/pf-vaadec/src/pic_av1.rs @@ -0,0 +1,1591 @@ +//! One AV1 [`AuPlanAv1`] into libva's buffers — M7's VAAPI conversion, and the +//! third and last hardware rung for this codec. +//! +//! The layouts it fills are measured against the real `va_dec_av1.h` +//! ([`crate::va_av1`]); this module is where the AV1 frame header's meaning is +//! mapped onto them, and where the places VAAPI disagrees with the other two +//! backends are handled. +//! +//! # The reference convention, and what it is established from +//! +//! [`crate::va_av1`]'s module docs state it in full. In one line: **`ref_frame_map` +//! is indexed by AV1 reference SLOT and holds a `VASurfaceID`; `ref_frame_idx` is +//! indexed by reference NAME and holds a SLOT (an index into `ref_frame_map`); +//! global motion is a picture-level array indexed by NAME with `wm[0]` = +//! `LAST_FRAME`; and there is no per-reference size field at all.** That comes from +//! `va_dec_av1.h`'s own comments and from libavcodec's `vaapi_av1.c`, which is what +//! every VAAPI driver is validated against. +//! +//! The last clause is the one that differs from DXVA and is easy to get backwards. +//! `DXVA_PicEntry_AV1` carries each reference's own `width`/`height` because a +//! decoder scales motion out of a differently-sized reference (7.11.3.3 derives +//! `xStep` from `RefUpscaledWidth[refIdx]`). libva 2.23.0 has **no** +//! `ref_frame_width`/`ref_frame_height` — measured, `grep -c` is 0 — so a VAAPI +//! driver reads each reference's dimensions off the SURFACE. Nothing here needs +//! `RefState::upscaled_width`, and looking for a field to put it in would end in +//! writing it somewhere it does not belong. +//! +//! # What this conversion refuses, and why refusing is the honest answer +//! +//! **Film grain synthesis.** libva's picture buffer carries two surfaces — +//! `current_frame` (the decode target, which is also what later frames PREDICT +//! from) and `current_display_picture` (the grained output) — and libavcodec +//! allocates a second frame (`ctx->tmp_frame`) precisely so the two can differ. +//! With one surface there are only wrong answers: grain in the reference chain, +//! which drifts every later frame, or an ungrained picture on screen, and +//! `va_dec_av1.h` does not say which a driver would pick. So a frame with +//! `apply_grain` set is [`PlanToVaAv1Error::FilmGrain`] rather than a submission +//! that decodes to something. The film-grain STRUCTURE is declared and its layout +//! pinned ([`crate::va_av1::VaFilmGrainStructAV1`]); it is left zero, which libva +//! documents as "ignore all of this" when `apply_grain` is 0. The fill and the +//! second surface belong to the same future change and neither is written here. +//! +//! No punktfunk host emits film grain (no AV1 hardware encoder in the fleet does) +//! and neither vendored conformance vector codes it, so this refusal is reachable +//! only by a stream from elsewhere — where it costs the session this rung and gets +//! the FFmpeg rung, which synthesises grain correctly. +//! +//! The gate is per FRAME rather than per SEQUENCE on purpose: `film_grain_params_present` +//! only says the tool is coded, and a sequence that declares it while every frame +//! leaves `apply_grain` at 0 decodes here perfectly. Refusing on the sequence flag +//! would be a whole-session demotion bought with nothing. What the per-frame gate must +//! NOT do is poison the ledger on its way out, which is why it sits after the mutation +//! block — see "A refusal after the mutations is deliberate" below. +//! +//! # A lost reference gets a LIVE surface, not `VA_INVALID_ID` +//! +//! A slot the planner reports empty, and a slot whose picture this rung never decoded +//! into a surface, both arrive here as [`VA_INVALID_SURFACE`] in `ref_frame_map`. +//! Sending that is what `va_dec_av1.h:352` warns about — *"Driver is not responsible +//! to validate reference frames' id"* — and the sentence CONTINUES: *"If missing frame +//! is identified, application may choose to perform error recovery by pointing +//! problematic index to an alternative frame buffer."* That is what +//! [`DecodePlanVaAv1::substituted_refs`] records: every empty entry is pointed at a +//! live surface (a resolved reference where there is one, the decode target otherwise) +//! so a concealed frame is a driver predicting from the WRONG picture rather than a +//! driver dereferencing a handle that names nothing. +//! +//! ⚠ Only where the store is PUBLISHED. A shown key frame publishes an all-invalid map +//! deliberately (libavcodec does the same) and substituting there would depart from the +//! one path every driver is exercised on, for a frame that reads no references at all. +//! +//! # A refusal after the mutations is deliberate +//! +//! [`Av1Planner::plan_au`](pf_bitstream::av1::Av1Planner::plan_au) has already stored +//! this picture in its own reference store by the time the plan arrives, so a refusal +//! that skipped this rung's `slots.assign` would leave the ledger one picture short of +//! the planner's store FOREVER: the next access unit's `dpb_refs` names the picture, +//! [`SlotMap::slot_of`] answers `None`, and [`PlanToVaAv1Error::UnresolvedReference`] +//! fires — which is itself a refusal, so it never repairs. One lost tile group would +//! cost every frame until the next shown key frame. +//! +//! So the removals and the assignment run BEFORE the tile walk and before the film +//! grain gate, and every refusal past that point leaves the ledger in step with the +//! planner. The caller's side of the contract is in [`plan_to_va_av1`]'s docs: on a +//! refusal it must bind NOTHING to the assigned slot, which is what turns the next +//! frame's reference to this picture into the substitution above. +//! +//! # Tiles: one record per TILE, several records per BUFFER +//! +//! `VASliceParameterBufferAV1` is a tile parameter buffer under a misleading name +//! (the header says so). libavcodec sends, per tile-group OBU, **one parameter +//! buffer holding that group's records** beside **one data buffer holding the +//! group's whole `tile_data` region** — `tile_size_minus_1` fields and all — with +//! each record's `slice_data_offset` relative to that buffer. That is the DXVA +//! upload layout, not the Vulkan one, so [`Av1Bitstream::groups`] is the half of the +//! shared walk this rung reads, and [`DecodePlanVaAv1::tile_groups`] is grouped +//! accordingly rather than being a flat list. + +use std::ops::Range; + +use pf_bitstream::av1::coded_cdef_sec_strength; +use pf_bitstream::av1::AuPlan as AuPlanAv1; +use pf_bitstream::av1::FrameType; +use pf_bitstream::av1::PicId; +use pf_bitstream::av1::NUM_REF_SLOTS; +use pf_bitstream::av1::REFS_PER_FRAME; + +use crate::va::VA_INVALID_SURFACE; +use crate::va::VA_SLICE_DATA_FLAG_ALL; +use crate::va_av1::FilmGrainInfoFieldsAV1; +use crate::va_av1::LoopFilterInfoFieldsAV1; +use crate::va_av1::LoopRestorationFieldsAV1; +use crate::va_av1::ModeControlFieldsAV1; +use crate::va_av1::PicInfoFieldsAV1; +use crate::va_av1::QmatrixFieldsAV1; +use crate::va_av1::SegmentInfoFieldsAV1; +use crate::va_av1::SeqInfoFieldsAV1; +use crate::va_av1::VaDecPictureParameterBufferAV1; +use crate::va_av1::VaSegmentationStructAV1; +use crate::va_av1::VaSliceParameterBufferAV1; +use crate::va_av1::VaWarpedMotionParamsAV1; +use crate::va_av1::ANCHOR_FRAME_UNUSED; +use crate::va_av1::LAST_FRAME; +use crate::va_av1::SUPERRES_NUM; +use crate::va_av1::TILE_SBS_LEN; +use crate::SlotError; +use crate::SlotMap; +use pf_vkdecode::plan_bitstream; +use pf_vkdecode::Av1Bitstream; +use pf_vkdecode::Av1TileError; + +/// AV1's tile ceiling in one frame — `MAX_TILE_COLS` × `MAX_TILE_ROWS` is 4096, +/// which no AV1 level defines; libavcodec refuses past 256 ("exceeding all defined +/// levels in the AV1 spec") and so does the shared walk. +pub const MAX_TILES: usize = 256; + +/// AV1's `MAX_TILE_COLS` / `MAX_TILE_ROWS`, and the bound the parser's own +/// `TileInfo` arrays are sized to. +pub const MAX_TILE_DIM: usize = 64; + +/// One tile-group OBU's submission: the records for its tiles, and the byte range +/// (ACCESS-UNIT coordinates) of the `tile_data` region they address. +/// +/// The pairing is the point. `vaRenderPicture` establishes which data buffer a +/// parameter buffer's `slice_data_offset` is relative to by being handed the two +/// together, so the records and their region must travel as one thing. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TileGroupVa { + pub tiles: Vec, + pub data: Range, +} + +/// Everything one AV1 `vaRenderPicture` sequence needs. +#[derive(Debug, Clone)] +pub struct DecodePlanVaAv1 { + pub pic_params: VaDecPictureParameterBufferAV1, + /// One entry per tile-group (or frame) OBU, in decode order. + pub tile_groups: Vec, + /// The ledger slot this picture took — or `None` when the picture refreshes no + /// reference slot and the conversion gave the slot straight back (see the + /// `refresh_frame_flags == 0` note in [`plan_to_va_av1`]). + pub setup_slot: Option, + pub setup_id: PicId, + /// Which `ref_frame_map` entries were empty and got a live surface instead — bit + /// `i` for AV1 reference slot `i` (module docs, "A lost reference gets a LIVE + /// surface"). + /// + /// Non-zero means this frame is being concealed: it decodes from at least one + /// substitute. Reported rather than silent because it is the one thing about a + /// submission that a log cannot otherwise tell from a clean decode, and because a + /// clean stream must never produce it — `pf-vaadec`'s vector test asserts 0 across + /// all 274 frames. + pub substituted_refs: u8, +} + +/// Why an AV1 plan cannot be expressed as VAAPI buffers. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanToVaAv1Error { + /// A `show_existing_frame` plan decodes nothing and has no submission. Not a + /// failure: the caller displays a surface it already holds. + NoDecode, + NoTiles, + /// The access unit's tile OBUs could not be walked into per-tile payloads. + Tiles(Av1TileError), + /// The frame header's tile GRID and the tiles the access unit actually carried + /// disagree — a dropped tile group, most likely, which nothing else reports. + TileCountMismatch { + records: usize, + walked: usize, + grid: usize, + }, + /// More tile columns or rows than AV1 defines. + TooManyTiles { + cols: u32, + rows: u32, + }, + /// A tile's payload is not inside the tile-group region the records address. + TileOutsideGroup { + tile: usize, + }, + /// The frame applies film grain, which needs a second surface this rung does not + /// allocate (module docs). + FilmGrain, + CapacityMismatch { + required: usize, + capacity: usize, + }, + /// A picture the marked store holds has no ledger slot, so no surface can be put + /// in `ref_frame_map` for it. + UnresolvedReference(PicId), + SurfaceOutOfRange { + slot: u8, + surfaces: usize, + }, + /// A header value wider than the libva field that carries it. + FieldOverflow { + field: &'static str, + value: u32, + }, + Slot(SlotError), +} + +impl From for PlanToVaAv1Error { + fn from(e: SlotError) -> Self { + PlanToVaAv1Error::Slot(e) + } +} + +impl PlanToVaAv1Error { + /// This refusal is the shape a LOST TILE GROUP makes. + /// + /// The distinction the caller needs, and the reason it is decided here rather than + /// by matching an enum at the call site: on a plan that already carries an + /// integrity warning these five are damage, not a defect — the access unit simply + /// did not carry the tiles its frame header announced — and the rung's answer to + /// damage is concealment, exactly as it is for every warning the planner raises. + /// On an UNDAMAGED plan the same five mean this conversion or the shared tile walk + /// disagrees with a stream that arrived whole, which is a defect and must surface + /// as one. + /// + /// Everything else stays a refusal either way: a capacity mismatch, an unresolved + /// reference or a field overflow says something about this rung's own state that + /// concealing would bury. + pub fn lost_tiles(&self) -> bool { + matches!( + self, + PlanToVaAv1Error::NoTiles + | PlanToVaAv1Error::Tiles(_) + | PlanToVaAv1Error::TileCountMismatch { .. } + | PlanToVaAv1Error::TooManyTiles { .. } + | PlanToVaAv1Error::TileOutsideGroup { .. } + ) + } +} + +impl std::fmt::Display for PlanToVaAv1Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlanToVaAv1Error::NoDecode => { + write!(f, "a show_existing_frame access unit decodes nothing") + } + PlanToVaAv1Error::NoTiles => write!(f, "the access unit planned no tiles"), + PlanToVaAv1Error::Tiles(e) => write!(f, "tile walk: {e}"), + PlanToVaAv1Error::TileCountMismatch { + records, + walked, + grid, + } => write!( + f, + "{records} tile records and {walked} walked tiles for a {grid}-tile \ + grid — a tile group was lost" + ), + PlanToVaAv1Error::TooManyTiles { cols, rows } => { + write!(f, "a {cols}x{rows} tile grid is outside AV1's limits") + } + PlanToVaAv1Error::TileOutsideGroup { tile } => write!( + f, + "tile {tile}'s payload is not inside its tile group's data region" + ), + PlanToVaAv1Error::FilmGrain => write!( + f, + "this frame applies film grain, which needs a separate display \ + surface this rung does not allocate" + ), + PlanToVaAv1Error::CapacityMismatch { required, capacity } => write!( + f, + "the slot map holds {capacity} slots, AV1 needs {required}" + ), + PlanToVaAv1Error::UnresolvedReference(id) => { + write!(f, "picture {id} holds a reference slot but no surface") + } + PlanToVaAv1Error::SurfaceOutOfRange { slot, surfaces } => { + write!( + f, + "ledger slot {slot} has no surface in a table of {surfaces}" + ) + } + PlanToVaAv1Error::FieldOverflow { field, value } => { + write!(f, "{field} = {value} does not fit its libva field") + } + PlanToVaAv1Error::Slot(e) => write!(f, "DPB slot map: {e:?}"), + } + } +} + +impl std::error::Error for PlanToVaAv1Error {} + +fn narrow(field: &'static str, value: u32) -> Result { + u8::try_from(value).map_err(|_| PlanToVaAv1Error::FieldOverflow { field, value }) +} + +fn narrow16(field: &'static str, value: u32) -> Result { + u16::try_from(value).map_err(|_| PlanToVaAv1Error::FieldOverflow { field, value }) +} + +fn narrow32(field: &'static str, value: usize) -> Result { + u32::try_from(value).map_err(|_| PlanToVaAv1Error::FieldOverflow { + field, + value: u32::MAX, + }) +} + +/// Convert one planned AV1 frame. +/// +/// `au` is the access unit `plan` was planned from: the tile records need per-TILE +/// byte ranges, and finding those means walking each tile group's header and its +/// `tile_size_minus_1` fields — a walk over the bitstream, not over the plan. It is +/// [`plan_bitstream`], shared with the Vulkan and DXVA rungs. +/// +/// `surfaces` is the caller's ledger-slot → `VASurfaceID` table and `setup_surface` +/// is the surface this picture decodes INTO — the same parameter contract +/// [`crate::pic::plan_to_va`] documents, and for the same reason: the decode target +/// comes off the caller's free list at activation time and is bound to its slot +/// afterwards, because a slot freed by this access unit's own removals is free +/// again by the time the ledger is asked. +/// +/// # The frame that refreshes nothing +/// +/// A frame with `refresh_frame_flags == 0` is legal AV1 — shown once, referenced +/// never — and it enters the planner's store NOWHERE, so the planner can never +/// report it removed. It still needs a ledger slot while it is converted (that is +/// how a later frame would resolve its surface), so this function assigns one and +/// gives it straight back, exactly as [`crate::pic_h265::plan_to_va_h265`] does for +/// a picture its own access unit evicts. [`DecodePlanVaAv1::setup_slot`] is then +/// `None`, which is the caller's signal that nothing binds the surface and only its +/// pending-output claim keeps it off the free list. +/// +/// That the release can happen HERE rather than in the caller is a property of this +/// backend: a VAAPI ledger slot is not a surface (the DXVA rung's `setup_slot` IS +/// its surface index, which is why `pf_dxvadec` has to hold the slot until the frame +/// has been read). Nine such frames would otherwise exhaust a nine-slot ledger and +/// kill a session on correct streams. +/// +/// # What a refusal leaves behind, and what the caller owes it +/// +/// ⚠ `slots` is mutated BEFORE the tile walk and before the film grain gate, so a +/// refusal from either of those has already applied this access unit's removals and +/// assigned the setup picture its slot. That is deliberate and the module docs say +/// why: the planner stored the picture before this function was called, and a refusal +/// that skipped the assignment would desynchronise the ledger from the planner's store +/// permanently. +/// +/// What the caller owes in return is that on a refusal it binds **nothing** to the +/// assigned slot — no surface was written, and leaving the slot's PREVIOUS binding in +/// place would make the next frame predict from a picture that is not the one the +/// bitstream named. An unbound slot reads back as `VA_INVALID_SURFACE` in +/// `surfaces` and is then substituted (module docs), which is the concealment libva +/// documents. +/// +/// The refusals that can still fire before any mutation — [`PlanToVaAv1Error::NoDecode`], +/// [`PlanToVaAv1Error::CapacityMismatch`], [`PlanToVaAv1Error::SurfaceOutOfRange`], +/// [`PlanToVaAv1Error::UnresolvedReference`] and the `RefPic::slot` overflow — leave +/// `slots` untouched, so the same "bind nothing" answer is correct for them too. +pub fn plan_to_va_av1( + plan: &AuPlanAv1, + au: &[u8], + slots: &mut SlotMap, + surfaces: &[u32], + setup_surface: u32, +) -> Result { + let setup_id = plan.dpb.stored.ok_or(PlanToVaAv1Error::NoDecode)?; + let h = &*plan.header; + let seq = &*plan.sequence; + let color = &seq.color_config; + + // AV1's DPB depth is a constant of the codec: eight reference slots plus the + // picture being decoded. + let required = NUM_REF_SLOTS + 1; + if slots.capacity() != required { + return Err(PlanToVaAv1Error::CapacityMismatch { + required, + capacity: slots.capacity(), + }); + } + // A pre-check, so the caller's post-call bind of `setup_surface` to the returned + // slot is always in range. + if surfaces.len() < slots.capacity() { + return Err(PlanToVaAv1Error::SurfaceOutOfRange { + slot: (slots.capacity() - 1) as u8, + surfaces: surfaces.len(), + }); + } + + // --- the reference store, by AV1 SLOT, holding SURFACES ------------------- + // + // ⚠ Indexed by `RefPic::slot` (the bitstream's own 0..8 reference slot), NOT by + // the ledger slot — the ledger is only how a PicId finds its surface. Writing + // the ledger slot's number here would name a different reference on every frame + // whose store is not in ledger order, which is every frame after the first + // eviction. + let mut ref_frame_map = [VA_INVALID_SURFACE; NUM_REF_SLOTS]; + // ⚠ A SHOWN KEY FRAME publishes an empty store. libavcodec: + // `if (frame_type == AV1_FRAME_KEY && frame_header->show_frame) + // pic_param.ref_frame_map[i] = VA_INVALID_ID;` + // — the frame decodes from nothing and refreshes every slot, so the surfaces the + // store held a moment ago are not references for it. Ours would still list them + // (the plan's `dpb_refs` is the store BEFORE this frame's refresh), and the + // difference is exactly the one place drivers have been exercised. + let publishes_store = !(h.frame_type == FrameType::KeyFrame && h.show_frame); + if publishes_store { + for r in &plan.dpb_refs { + let ledger = slots + .slot_of(r.id) + .ok_or(PlanToVaAv1Error::UnresolvedReference(r.id))?; + let surface = + *surfaces + .get(usize::from(ledger)) + .ok_or(PlanToVaAv1Error::SurfaceOutOfRange { + slot: ledger, + surfaces: surfaces.len(), + })?; + let slot = usize::from(r.slot); + if slot >= NUM_REF_SLOTS { + return Err(PlanToVaAv1Error::FieldOverflow { + field: "RefPic::slot", + value: u32::from(r.slot), + }); + } + ref_frame_map[slot] = surface; + } + } + + // ⚠ An empty entry is pointed at a LIVE surface — the header's own prescription + // for a missing reference, quoted in the module docs. Two different losses land + // here and both need it: a slot the planner reports empty (its picture never + // arrived) and a slot whose picture this rung refused to convert, which the caller + // signals by binding no surface to it. + // + // ⚠ A resolved reference is preferred over `setup_surface`. Both are live and + // correctly sized, but the decode target is the surface the driver is about to + // WRITE, and naming it as its own reference is a shape some drivers validate + // against; a picture that actually decoded is the better substitute and is the + // "alternative frame buffer" the header means. The target is the fallback for the + // one case with nothing else to reach for — a store that resolved nothing at all. + let mut substituted_refs = 0u8; + if publishes_store { + let alternative = ref_frame_map + .iter() + .copied() + .find(|&s| s != VA_INVALID_SURFACE) + .unwrap_or(setup_surface); + for (slot, entry) in ref_frame_map.iter_mut().enumerate() { + if *entry == VA_INVALID_SURFACE { + *entry = alternative; + substituted_refs |= 1 << slot; + } + } + } + + // The seven reference NAMES, each holding the SLOT it reads — which is + // `ref_frame_idx[name]` verbatim, and libavcodec copies it unconditionally + // (a key or intra-only frame reads no references and the driver ignores it). + // + // ⚠ Deliberately NOT taken from `plan.refs`: a lost reference leaves a hole + // there, and a hole is not a slot. The name still points at the slot the + // bitstream coded, and the concealment is done one level down — that slot's + // `ref_frame_map` entry is the substituted surface above, so the name resolves to a + // live picture rather than to nothing. + let ref_frame_idx = h.ref_frame_idx; + + // --- mutations, once the references have resolved ------------------------ + // + // ⚠ HERE, and not after the tile walk. Every fallible step below leaves the ledger + // in step with the planner's store, which is what a refusal needs (fn docs); doing + // it the other way round turns one lost tile group into a hard `Err` on every + // frame until the next shown key frame. + // + // ⚠ But not before the loop above either: a reference resolves against the store as + // it stood BEFORE this access unit's removals, and releasing first would lose the + // picture a name still points at. + + for &id in &plan.dpb.removed { + if id == setup_id { + continue; + } + let _ = slots.release(id); + } + let assigned = slots.assign(setup_id)?; + // The frame that refreshes nothing never enters the store, so nothing will ever + // ask the ledger for it again (fn docs). + let setup_slot = if h.refresh_frame_flags == 0 { + slots.release(setup_id); + None + } else { + Some(assigned) + }; + + // Film grain: refused, not approximated (module docs). After the mutations, so the + // refusal costs this frame and not the rest of the GOP. + if seq.film_grain_params_present && h.film_grain_params.apply_grain { + return Err(PlanToVaAv1Error::FilmGrain); + } + + // --- tiles --------------------------------------------------------------- + // + // A frame header whose tile groups did not arrive is the everyday shape of a lost + // packet: `PlanWarning::TruncatedAu`, a plan that still stores its picture, and an + // empty or short tile list. It refuses here — there is nothing to submit — and + // [`PlanToVaAv1Error::lost_tiles`] is how the caller tells that damage apart from a + // defect. + if plan.tiles.is_empty() { + return Err(PlanToVaAv1Error::NoTiles); + } + let t = &h.tile_info; + if t.tile_cols == 0 || t.tile_rows == 0 { + return Err(PlanToVaAv1Error::NoTiles); + } + let grid = (t.tile_cols as usize).saturating_mul(t.tile_rows as usize); + if t.tile_cols as usize > MAX_TILE_DIM || t.tile_rows as usize > MAX_TILE_DIM { + return Err(PlanToVaAv1Error::TooManyTiles { + cols: t.tile_cols, + rows: t.tile_rows, + }); + } + if grid > MAX_TILES { + return Err(PlanToVaAv1Error::Tiles(Av1TileError::TooManyTiles { + tiles: grid, + })); + } + let bitstream: Av1Bitstream = + plan_bitstream(au, &plan.tiles, h).map_err(PlanToVaAv1Error::Tiles)?; + + let mut tile_groups: Vec = Vec::with_capacity(plan.tiles.len()); + let mut walked = 0usize; + // `plan_bitstream` pushes one region per plan tile group, in order, so `index` + // addresses this group's region — `get` rather than `[]` because a panic in a + // decode thread is a worse answer than a refusal even for a case the walk cannot + // produce. + for (index, tg) in plan.tiles.iter().enumerate() { + let region = bitstream + .groups + .get(index) + .ok_or(PlanToVaAv1Error::TileCountMismatch { + records: walked, + walked: bitstream.tiles.len(), + grid, + })? + .clone(); + // A group whose end precedes its start is malformed; the walk refuses it + // too, so this saturates rather than growing a second refusal path. + let count = tg.tg_end.saturating_sub(tg.tg_start).saturating_add(1); + let mut records = Vec::with_capacity(count as usize); + for step in 0..count { + let tile_num = tg.tg_start.saturating_add(step); + let payload = bitstream.tiles.get(walked).ok_or({ + PlanToVaAv1Error::TileCountMismatch { + records: walked, + walked: bitstream.tiles.len(), + grid, + } + })?; + walked += 1; + // The offset libva wants is relative to the DATA BUFFER, which is this + // group's whole `tile_data` region — not to the access unit, and not to + // the tile. Rebased here rather than by a packer, because unlike DXVA + // this rung uploads the region itself and has nothing to rebase against + // later. + if payload.start < region.start || payload.end > region.end { + return Err(PlanToVaAv1Error::TileOutsideGroup { tile: walked - 1 }); + } + records.push(VaSliceParameterBufferAV1 { + slice_data_size: narrow32("slice_data_size", payload.end - payload.start)?, + slice_data_offset: narrow32("slice_data_offset", payload.start - region.start)?, + slice_data_flag: VA_SLICE_DATA_FLAG_ALL, + // Tile numbering is libavcodec's: `tile_row = tile_num / tile_cols`, + // `tile_column = tile_num % tile_cols`, with `tile_num` running + // `tg_start..=tg_end` across the frame's groups. + tile_row: narrow16("tile_row", tile_num / t.tile_cols)?, + tile_column: narrow16("tile_column", tile_num % t.tile_cols)?, + // `va_deprecated`, and libavcodec fills both anyway. + tg_start: narrow16("tg_start", tg.tg_start)?, + tg_end: narrow16("tg_end", tg.tg_end)?, + anchor_frame_idx: ANCHOR_FRAME_UNUSED, + tile_idx_in_tile_list: 0, + va_reserved: [0; 4], + }); + } + tile_groups.push(TileGroupVa { + tiles: records, + data: region, + }); + } + // The independent cross-check, and the same one the DXVA rung makes: the tile + // GRID comes from the frame header and is what `tile_cols`/`tile_rows` announce + // to the driver, while the record count comes from the tile groups the access + // unit actually carried. A dropped tile group raises no warning anywhere else — + // the OBU walk simply never sees it — and submitting anyway declares a grid the + // tile buffers are short for. + if walked != grid || bitstream.tiles.len() != grid { + return Err(PlanToVaAv1Error::TileCountMismatch { + records: walked, + walked: bitstream.tiles.len(), + grid, + }); + } + + // --- the picture parameter blocks ---------------------------------------- + + let lf = &h.loop_filter_params; + let q = &h.quantization_params; + let c = &h.cdef_params; + let lr = &h.loop_restoration_params; + let sp = &h.segmentation_params; + let gm = &h.global_motion_params; + + let mut seg_info = VaSegmentationStructAV1::zeroed(); + seg_info.segment_info_fields = SegmentInfoFieldsAV1 { + enabled: sp.segmentation_enabled, + update_map: sp.segmentation_update_map, + temporal_update: sp.segmentation_temporal_update, + update_data: sp.segmentation_update_data, + } + .pack(); + for segment in 0..8 { + let mut mask = 0u8; + for (feature, enabled) in sp.feature_enabled[segment].iter().enumerate() { + if *enabled { + mask |= 1 << feature; + } + } + seg_info.feature_mask[segment] = mask; + // No clipping here: libva wants `FeatureData` AFTER 5.9.14's Clip3, and the + // vendored parser clips as it reads (`helpers::clip3` against the spec's + // `FEATURE_MAX`). Clipping again would be a no-op; not clipping at all would + // have been the bug, which is why this says which side did it. + seg_info.feature_data[segment] = sp.feature_data[segment]; + } + + let mut cdef_y_strengths = [0u8; crate::va_av1::CDEF_MAX]; + let mut cdef_uv_strengths = [0u8; crate::va_av1::CDEF_MAX]; + for i in 0..crate::va_av1::CDEF_MAX { + // The header's own formula: `(pri << 2) | (sec & 0x03)`. + // + // ⚠ `sec` must be the CODED two-bit read. AV1 5.9.19 rewrites the syntax + // element in place (a coded 3 becomes 4) and cros-codecs follows the spec, so + // masking the parser's value with 3 would turn the STRONGEST secondary filter + // into NO filter — on 68 of the vendored vector's 274 frames, including + // frame 0. `coded_cdef_sec_strength` is the inverse; its docs carry the + // evidence. + let pri_y = narrow("cdef_y_pri_strength", c.cdef_y_pri_strength[i])?; + let pri_uv = narrow("cdef_uv_pri_strength", c.cdef_uv_pri_strength[i])?; + cdef_y_strengths[i] = (pri_y << 2) | coded_cdef_sec_strength(c.cdef_y_sec_strength[i]); + cdef_uv_strengths[i] = (pri_uv << 2) | coded_cdef_sec_strength(c.cdef_uv_sec_strength[i]); + } + + let mut width_in_sbs_minus_1 = [0u16; TILE_SBS_LEN]; + let mut height_in_sbs_minus_1 = [0u16; TILE_SBS_LEN]; + // ⚠ Clamped to 63 entries. The arrays ARE 63 long and the header says why — the + // last tile's size is derived from the others and the frame size — but + // libavcodec loops to `tile_cols`, which writes index 63 on a 64-column frame. + // That is a one-element overrun in libavcodec, not a layout we should reproduce. + for (out, coded) in width_in_sbs_minus_1 + .iter_mut() + .zip(&t.width_in_sbs_minus_1[..(t.tile_cols as usize).min(TILE_SBS_LEN)]) + { + *out = narrow16("width_in_sbs_minus_1", *coded)?; + } + for (out, coded) in height_in_sbs_minus_1 + .iter_mut() + .zip(&t.height_in_sbs_minus_1[..(t.tile_rows as usize).min(TILE_SBS_LEN)]) + { + *out = narrow16("height_in_sbs_minus_1", *coded)?; + } + + let mut wm = [VaWarpedMotionParamsAV1::zeroed(); REFS_PER_FRAME]; + for (name, entry) in wm.iter_mut().enumerate() { + // ⚠ Global motion is indexed by reference NAME, never by DPB slot. AV1's + // `global_motion_params()` loops `ref = LAST_FRAME..ALTREF_FRAME` and the + // vendored parser stores it that way; libavcodec's `vaapi_av1.c` writes + // `pic_param.wm[i - 1]` for `i = LAST_FRAME..=ALTREF_FRAME`. Reading by slot + // agrees with the truth only while reference `i` happens to sit in slot + // `i + 1`, and silently hands every warped reference somebody else's warp + // the moment it does not. + let gm_name = LAST_FRAME + name; + entry.wmtype = gm.gm_type[gm_name] as u32; + // Six warp parameters, not eight: 5.9.24 codes six and libavcodec copies + // `for (j = 0; j < 6; j++)`. `wmmat[6]`/`wmmat[7]` stay zero. + entry.wmmat[..6].copy_from_slice(&gm.gm_params[gm_name]); + // `warp_valid` is the parser's `setup_shear` verdict — a warp whose shear + // parameters are out of range is unusable — and libva's flag is its inverse. + entry.invalid = u8::from(!gm.warp_valid[gm_name]); + } + + let bit_depth_idx = if color.high_bitdepth { + if color.twelve_bit { + 2 + } else { + 1 + } + } else { + 0 + }; + + let mut pic_params = VaDecPictureParameterBufferAV1::zeroed(); + pic_params.profile = seq.seq_profile as u8; + // ⚠ The parser types this `i32` and leaves it **-1** when `enable_order_hint` is + // 0 (`parser.rs`: `s.order_hint_bits_minus_1 = -1`). `as u8` on that is 255 — a + // decoder told the order hints are 256 bits wide — so the disabled case sends 0, + // which is what libavcodec's CBS holds for a field it never read. + pic_params.order_hint_bits_minus_1 = if seq.enable_order_hint { + narrow( + "order_hint_bits_minus_1", + u32::try_from(seq.order_hint_bits_minus_1).map_err(|_| { + PlanToVaAv1Error::FieldOverflow { + field: "order_hint_bits_minus_1", + value: 0, + } + })?, + )? + } else { + 0 + }; + pic_params.bit_depth_idx = bit_depth_idx; + pic_params.matrix_coefficients = color.matrix_coefficients as u8; + pic_params.seq_info_fields = SeqInfoFieldsAV1 { + still_picture: seq.still_picture, + use_128x128_superblock: seq.use_128x128_superblock, + enable_filter_intra: seq.enable_filter_intra, + enable_intra_edge_filter: seq.enable_intra_edge_filter, + enable_interintra_compound: seq.enable_interintra_compound, + enable_masked_compound: seq.enable_masked_compound, + enable_dual_filter: seq.enable_dual_filter, + enable_order_hint: seq.enable_order_hint, + enable_jnt_comp: seq.enable_jnt_comp, + enable_cdef: seq.enable_cdef, + mono_chrome: color.mono_chrome, + color_range: color.color_range, + subsampling_x: color.subsampling_x, + subsampling_y: color.subsampling_y, + chroma_sample_position: color.chroma_sample_position as u8, + film_grain_params_present: seq.film_grain_params_present, + } + .pack(); + pic_params.current_frame = setup_surface; + // Equal to `current_frame` because `apply_grain` is 0 on every frame that + // reaches here (module docs); libva then ignores this field entirely. + pic_params.current_display_picture = setup_surface; + pic_params.anchor_frames_num = 0; + pic_params.anchor_frames_list = std::ptr::null_mut(); + // The UPSCALED width — the same quantity libavcodec sends as the coded + // `frame_width_minus_1`, which AV1 5.9.8 reads into `UpscaledWidth` before + // superres divides it down into `FrameWidth`. + pic_params.frame_width_minus1 = narrow16( + "frame_width_minus1", + h.upscaled_width + .checked_sub(1) + .ok_or(PlanToVaAv1Error::FieldOverflow { + field: "upscaled_width", + value: 0, + })?, + )?; + pic_params.frame_height_minus1 = narrow16( + "frame_height_minus1", + h.frame_height + .checked_sub(1) + .ok_or(PlanToVaAv1Error::FieldOverflow { + field: "frame_height", + value: 0, + })?, + )?; + pic_params.ref_frame_map = ref_frame_map; + pic_params.ref_frame_idx = ref_frame_idx; + pic_params.primary_ref_frame = narrow("primary_ref_frame", h.primary_ref_frame)?; + pic_params.order_hint = narrow("order_hint", h.order_hint)?; + pic_params.seg_info = seg_info; + pic_params.tile_cols = narrow("tile_cols", t.tile_cols)?; + pic_params.tile_rows = narrow("tile_rows", t.tile_rows)?; + pic_params.width_in_sbs_minus_1 = width_in_sbs_minus_1; + pic_params.height_in_sbs_minus_1 = height_in_sbs_minus_1; + pic_params.context_update_tile_id = + narrow16("context_update_tile_id", t.context_update_tile_id)?; + pic_params.pic_info_fields = PicInfoFieldsAV1 { + frame_type: h.frame_type as u8, + show_frame: h.show_frame, + showable_frame: h.showable_frame, + error_resilient_mode: h.error_resilient_mode, + disable_cdf_update: h.disable_cdf_update, + allow_screen_content_tools: h.allow_screen_content_tools != 0, + force_integer_mv: h.force_integer_mv != 0, + allow_intrabc: h.allow_intrabc, + use_superres: h.use_superres, + allow_high_precision_mv: h.allow_high_precision_mv, + is_motion_mode_switchable: h.is_motion_mode_switchable, + use_ref_frame_mvs: h.use_ref_frame_mvs, + disable_frame_end_update_cdf: h.disable_frame_end_update_cdf, + uniform_tile_spacing_flag: t.uniform_tile_spacing_flag, + allow_warped_motion: h.allow_warped_motion, + large_scale_tile: false, + } + .pack(); + // The REAL denominator, not the coded one, and `SUPERRES_NUM` when superres is + // off — libva documents 8 there and 9..=16 otherwise, so a 0 would be outside + // the field's stated range. + pic_params.superres_scale_denominator = if h.use_superres { + narrow("superres_denom", h.superres_denom)? + } else { + SUPERRES_NUM + }; + pic_params.interp_filter = h.interpolation_filter as u8; + pic_params.filter_level = [lf.loop_filter_level[0], lf.loop_filter_level[1]]; + pic_params.filter_level_u = lf.loop_filter_level[2]; + pic_params.filter_level_v = lf.loop_filter_level[3]; + pic_params.loop_filter_info_fields = LoopFilterInfoFieldsAV1 { + sharpness_level: lf.loop_filter_sharpness, + mode_ref_delta_enabled: lf.loop_filter_delta_enabled, + mode_ref_delta_update: lf.loop_filter_delta_update, + } + .pack(); + pic_params.ref_deltas = lf.loop_filter_ref_deltas; + pic_params.mode_deltas = lf.loop_filter_mode_deltas; + pic_params.base_qindex = narrow("base_qindex", q.base_q_idx)?; + // The five deltas are `su(1+6)` reads, so the parser cannot hand out anything + // outside -63..=63 and the narrowing cannot truncate. + pic_params.y_dc_delta_q = q.delta_q_y_dc as i8; + pic_params.u_dc_delta_q = q.delta_q_u_dc as i8; + pic_params.u_ac_delta_q = q.delta_q_u_ac as i8; + pic_params.v_dc_delta_q = q.delta_q_v_dc as i8; + pic_params.v_ac_delta_q = q.delta_q_v_ac as i8; + pic_params.qmatrix_fields = QmatrixFieldsAV1 { + using_qmatrix: q.using_qmatrix, + // No 0xFF sentinel here, unlike DXVA: libva carries `using_qmatrix` itself, + // so a frame without a matrix simply leaves these ignored. + qm_y: narrow("qm_y", q.qm_y)?, + qm_u: narrow("qm_u", q.qm_u)?, + qm_v: narrow("qm_v", q.qm_v)?, + } + .pack(); + pic_params.mode_control_fields = ModeControlFieldsAV1 { + delta_q_present_flag: q.delta_q_present, + log2_delta_q_res: narrow("delta_q_res", q.delta_q_res)?, + delta_lf_present_flag: lf.delta_lf_present, + log2_delta_lf_res: lf.delta_lf_res, + delta_lf_multi: lf.delta_lf_multi, + tx_mode: h.tx_mode as u8, + reference_select: h.reference_select, + reduced_tx_set_used: h.reduced_tx_set, + skip_mode_present: h.skip_mode_present, + } + .pack(); + // The parser holds `CdefDamping` (coded + 3); libva wants the coded value. + pic_params.cdef_damping_minus_3 = + narrow("cdef_damping_minus_3", c.cdef_damping.saturating_sub(3))?; + pic_params.cdef_bits = narrow("cdef_bits", c.cdef_bits)?; + pic_params.cdef_y_strengths = cdef_y_strengths; + pic_params.cdef_uv_strengths = cdef_uv_strengths; + pic_params.loop_restoration_fields = LoopRestorationFieldsAV1 { + // The parser's `FrameRestorationType` IS the spec's, so no remap — see + // [`LoopRestorationFieldsAV1`]'s docs for why libavcodec appears to remap + // and this does not. + yframe_restoration_type: lr.frame_restoration_type[0] as u8, + cbframe_restoration_type: lr.frame_restoration_type[1] as u8, + crframe_restoration_type: lr.frame_restoration_type[2] as u8, + lr_unit_shift: lr.lr_unit_shift, + lr_uv_shift: lr.lr_uv_shift, + } + .pack(); + pic_params.wm = wm; + // Left zero, and deliberately: `apply_grain` is 0 on every frame that reaches + // here, which libva documents as "all the rest parameters should be set to zero + // and ignored". + pic_params.film_grain_info.film_grain_info_fields = FilmGrainInfoFieldsAV1::default().pack(); + + Ok(DecodePlanVaAv1 { + pic_params, + tile_groups, + setup_slot, + setup_id, + substituted_refs, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use cros_codecs::bitstream_utils::IvfIterator; + use pf_bitstream::av1::Av1Planner; + use std::collections::HashMap; + + const AV1_25FPS: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1" + ); + + /// A surface table with a recognisable value per ledger slot, so a wrong index + /// is a wrong NUMBER rather than a plausible one. + fn surface_table() -> Vec { + (0..NUM_REF_SLOTS as u32 + 1).map(|i| 0x1000 + i).collect() + } + + /// The caller's binding step, reproduced: re-derive the ledger-slot → surface + /// table from the ledger (a slot the conversion released binds nothing), then + /// bind the picture just converted. + /// + /// This is `video_vaapi_native`'s `bind_setup` + `sync_slot_bindings` field for + /// field, down to asking the LEDGER where the picture landed rather than being told + /// — and the test has to do it because the surface table the NEXT frame resolves + /// its references through is exactly this table. A fixed table would let the + /// reference checks below pass while reading somebody else's surface. + /// + /// `surface` is `None` for the refusal path, where the conversion assigned the slot + /// but nothing was decoded into a surface for it. Binding nothing is the caller's + /// half of [`plan_to_va_av1`]'s contract. + fn bind( + slot_surface: &mut [u32], + slots: &SlotMap, + stored: Option, + surface: Option, + ) { + let live: std::collections::HashSet = slots.held().map(|(slot, _)| slot).collect(); + for (index, bound) in slot_surface.iter_mut().enumerate() { + if !live.contains(&(index as u8)) { + *bound = VA_INVALID_SURFACE; + } + } + if let Some(slot) = stored.and_then(|id| slots.slot_of(id)) { + slot_surface[usize::from(slot)] = surface.unwrap_or(VA_INVALID_SURFACE); + } + } + + /// The whole vendored vector, converted — and every statement that could be + /// transposed checked against an independently kept shadow of the truth. + /// + /// The load-bearing assertions are the two indexings this API gets wrong most + /// easily: `ref_frame_map` is by AV1 SLOT (checked against a `PicId → surface` + /// map this test keeps itself, so a ledger-slot index would read a different + /// surface), and `wm[]` is by reference NAME (checked against + /// `gm_params[name + 1]`, so the off-by-one that hands every reference its + /// neighbour's warp fails here). + #[test] + fn the_whole_vendored_vector_converts_and_the_indexings_hold() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + // The caller's ledger-slot → surface bindings, maintained exactly as the + // client maintains them. + let mut slot_surface = vec![VA_INVALID_SURFACE; NUM_REF_SLOTS + 1]; + // Our own PicId → surface record, kept INDEPENDENTLY of the ledger — so a + // conversion that indexed the store by the wrong thing reads a surface this + // map disagrees with. + let mut surface_of: HashMap = HashMap::new(); + + let (mut frames, mut inter, mut warped, mut cdef_fixups) = (0u32, 0u32, 0u32, 0u32); + let mut multi_ref_slot_pictures = 0u32; + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("the clean vector plans") { + let Some(id) = plan.dpb.stored else { + continue; + }; + frames += 1; + // The caller's free-list choice, faked deterministically: a surface + // number that is unique per picture, so a stale binding is visible. + let setup_surface = 0x9000 + frames; + // The table is snapshotted BEFORE the conversion, as the client does + // — references resolve against the pre-removal bindings. + let surfaces = slot_surface.clone(); + let va = plan_to_va_av1(&plan, packet, &mut slots, &surfaces, setup_surface) + .unwrap_or_else(|e| panic!("frame {frames}: {e}")); + surface_of.insert(id, setup_surface); + bind(&mut slot_surface, &slots, Some(id), Some(setup_surface)); + + assert_eq!(va.pic_params.current_frame, setup_surface); + assert_eq!( + va.pic_params.current_display_picture, setup_surface, + "no film grain means the display surface IS the decode target" + ); + + let h = &*plan.header; + let shown_key = h.frame_type == FrameType::KeyFrame && h.show_frame; + + // --- ref_frame_map is by AV1 SLOT and holds SURFACES --------- + let mut expected = [VA_INVALID_SURFACE; NUM_REF_SLOTS]; + if !shown_key { + for r in &plan.dpb_refs { + expected[usize::from(r.slot)] = *surface_of + .get(&r.id) + .unwrap_or_else(|| panic!("frame {frames}: no surface for {}", r.id)); + } + } + assert_eq!( + va.pic_params.ref_frame_map, expected, + "frame {frames}: the store must be indexed by AV1 slot and hold \ + the surface each slot's picture decoded into" + ); + assert_eq!( + va.substituted_refs, 0, + "frame {frames}: a stream that lost nothing must conceal nothing — \ + a substitution here means the reference plumbing is inventing \ + surfaces on a clean vector" + ); + if shown_key { + assert!( + va.pic_params + .ref_frame_map + .iter() + .all(|&s| s == VA_INVALID_SURFACE), + "frame {frames}: a shown key frame publishes an empty store" + ); + } + // One picture in SEVERAL AV1 slots is what makes the indexing above + // falsifiable: while every picture holds exactly one slot, an + // AV1-slot index and a per-picture index cannot be told apart. + let distinct_slots: std::collections::HashSet = + plan.dpb_refs.iter().map(|r| r.slot).collect(); + assert_eq!( + distinct_slots.len(), + plan.dpb_refs.len(), + "the marked store lists each slot once" + ); + let distinct_ids: std::collections::HashSet = + plan.dpb_refs.iter().map(|r| r.id).collect(); + if distinct_ids.len() < plan.dpb_refs.len() { + multi_ref_slot_pictures += 1; + } + + // --- ref_frame_idx is by NAME and holds a SLOT --------------- + assert_eq!( + va.pic_params.ref_frame_idx, h.ref_frame_idx, + "frame {frames}: the name table is the header's own slot list" + ); + if !h.frame_is_intra { + inter += 1; + for (name, r) in plan.refs.iter().enumerate() { + let r = r.expect("the clean vector loses no reference"); + assert_eq!( + va.pic_params.ref_frame_idx[name], r.slot, + "frame {frames}: name {name} must carry its SLOT" + ); + assert_eq!( + va.pic_params.ref_frame_map[usize::from(r.slot)], + surface_of[&r.id], + "frame {frames}: following name {name} through the slot \ + table must reach that reference's own surface" + ); + } + } + + // --- global motion is by NAME, one step off the parser ------- + for name in 0..REFS_PER_FRAME { + let gm = &h.global_motion_params; + assert_eq!( + va.pic_params.wm[name].wmmat[..6], + gm.gm_params[name + 1], + "frame {frames}: wm[{name}] must be reference name \ + {}'s warp, not slot {name}'s", + name + 1 + ); + assert_eq!(va.pic_params.wm[name].wmmat[6..], [0, 0]); + assert_eq!(va.pic_params.wm[name].wmtype, gm.gm_type[name + 1] as u32); + assert_eq!( + va.pic_params.wm[name].invalid, + u8::from(!gm.warp_valid[name + 1]) + ); + if va.pic_params.wm[name].wmtype != 0 { + warped += 1; + } + } + + // --- the tile records address the tile PAYLOADS -------------- + let grid = (h.tile_info.tile_cols * h.tile_info.tile_rows) as usize; + let records: usize = va.tile_groups.iter().map(|g| g.tiles.len()).sum(); + assert_eq!(records, grid, "frame {frames}: one record per tile"); + for group in &va.tile_groups { + let region = &packet[group.data.clone()]; + for tile in &group.tiles { + let start = tile.slice_data_offset as usize; + let end = start + tile.slice_data_size as usize; + assert!( + end <= region.len(), + "frame {frames}: a record runs past its data buffer" + ); + // The bytes the record addresses must BE a tile payload — + // and specifically not the group's own header, which is + // where the region starts and the payload does not. + assert!( + !region[start..end].is_empty(), + "frame {frames}: an empty tile" + ); + } + // The whole region must be accounted for: every tile's payload + // plus one `TileSizeBytes` field per tile EXCEPT the last. This + // is `tile_group_obu()`'s own arithmetic and is a fact about the + // bitstream rather than about this conversion, which is what + // makes it independent of the offsets it checks. + let size_bytes = if grid > 1 { + h.tile_info.tile_size_bytes as usize + } else { + 0 + }; + let payloads: usize = + group.tiles.iter().map(|t| t.slice_data_size as usize).sum(); + assert_eq!( + payloads + (group.tiles.len() - 1) * size_bytes, + group.data.end - group.data.start, + "frame {frames}: the group's tiles and its size fields must \ + account for the region exactly" + ); + assert_eq!( + group.tiles[0].slice_data_offset, 0, + "the first tile's payload starts the tile_data region" + ); + } + + // --- the scalar traps ---------------------------------------- + assert_eq!( + va.pic_params.frame_width_minus1 as u32, + h.upscaled_width - 1, + "frame {frames}: the width field is the UPSCALED width" + ); + assert_eq!(va.pic_params.frame_height_minus1 as u32, h.frame_height - 1); + assert_eq!( + va.pic_params.superres_scale_denominator, SUPERRES_NUM, + "this vector uses no superres, so the denominator is 8 — never 0" + ); + assert_eq!( + va.pic_params.order_hint_bits_minus_1 as i32, + plan.sequence.order_hint_bits_minus_1, + "the vector enables order hints, so the field is the parser's" + ); + let coded = 1usize << h.cdef_params.cdef_bits; + for i in 0..coded { + let sec = va.pic_params.cdef_y_strengths[i] & 0x3; + let pri = va.pic_params.cdef_y_strengths[i] >> 2; + assert_eq!(pri as u32, h.cdef_params.cdef_y_pri_strength[i]); + if h.cdef_params.cdef_y_sec_strength[i] == 4 { + cdef_fixups += 1; + assert_eq!( + sec, 3, + "frame {frames}: the spec's in-place 4 is the coded 3, \ + and masking it with 3 would send 0" + ); + } else { + assert_eq!(sec as u32, h.cdef_params.cdef_y_sec_strength[i]); + } + } + } + } + + assert_eq!(frames, 274, "every frame of the vector converted"); + assert!( + inter > 0, + "no inter frame: the name-table checks were vacuous" + ); + assert!( + multi_ref_slot_pictures > 0, + "no picture ever occupied two reference slots at once, so nothing here \ + could tell an AV1-slot index from a per-picture one" + ); + assert!( + cdef_fixups > 0, + "no frame coded a secondary strength needing the fixup, so the CDEF \ + packing above compared a correction against a stream that never needs it" + ); + // ⚠ Honest about what this vector does NOT cover: it codes no global motion + // at all, so the wm[] comparison above proves the INDEXING (each entry is + // read from `gm_params[name + 1]`) but every value compared is the identity + // warp. A transposition of two identical zeros is invisible. + eprintln!( + "frames {frames} · inter {inter} · non-identity warps {warped} \ + (0 means the warp VALUES are untested; the indexing is not) · \ + cdef fixups {cdef_fixups}" + ); + } + + /// A `show_existing_frame` plan has no submission — and the caller must be able + /// to tell that apart from a failure. + /// + /// ⚠ Built by hand, because the vendored vector uses `show_existing_frame` + /// **zero times** (pf-bitstream's own planner test says so and asserts it stays + /// 0). So what is exercised here is this function's `dpb.stored == None` arm and + /// nothing about the parser's display-only path. + #[test] + fn a_show_existing_frame_plan_is_not_a_decode() { + let mut planner = Av1Planner::new(); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let key = planner + .plan_au(first) + .expect("the key frame plans") + .first() + .expect("a frame") + .clone(); + let display_only = AuPlanAv1 { + dpb: pf_bitstream::av1::DpbUpdate { + stored: None, + outputs: vec![1], + removed: Vec::new(), + }, + tiles: Vec::new(), + ..key + }; + let mut slots = SlotMap::new(NUM_REF_SLOTS); + assert_eq!( + plan_to_va_av1(&display_only, first, &mut slots, &surface_table(), 7).err(), + Some(PlanToVaAv1Error::NoDecode) + ); + assert_eq!(slots.active(), 0, "a refusal must not touch the ledger"); + } + + /// Film grain is refused, not approximated (module docs) — and the refusal costs + /// this frame only. + /// + /// ⚠ The ledger assertion is the load-bearing half now. The gate sits AFTER the + /// mutation block precisely so a grained frame in the middle of a GOP does not + /// leave the ledger one picture short of the planner's store, which would turn + /// every later reference to it into a hard `UnresolvedReference` — a refusal that + /// can never repair itself. + #[test] + fn a_frame_that_applies_film_grain_is_refused() { + let mut planner = Av1Planner::new(); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let key = planner + .plan_au(first) + .expect("the key frame plans") + .first() + .expect("a frame") + .clone(); + + // The vector codes neither, so both halves of the gate are set by hand. + let mut seq = (*key.sequence).clone(); + seq.film_grain_params_present = true; + let mut header = (*key.header).clone(); + header.film_grain_params.apply_grain = true; + let grained = AuPlanAv1 { + sequence: std::rc::Rc::new(seq.clone()), + header: std::rc::Rc::new(header), + ..key.clone() + }; + let mut slots = SlotMap::new(NUM_REF_SLOTS); + assert_eq!( + plan_to_va_av1(&grained, first, &mut slots, &surface_table(), 7).err(), + Some(PlanToVaAv1Error::FilmGrain) + ); + let stored = grained.dpb.stored.expect("the key frame is stored"); + assert_eq!( + slots.slot_of(stored), + Some(0), + "the refusal must leave the ledger holding the picture the PLANNER stored \ + — the planner has no idea this rung said no, and every later frame that \ + names this picture resolves through this ledger" + ); + + // A sequence that DECLARES the tool but a frame that does not apply it is + // ordinary: the declaration alone must not cost the session this rung. + let declared_only = AuPlanAv1 { + sequence: std::rc::Rc::new(seq), + ..key + }; + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let va = plan_to_va_av1(&declared_only, first, &mut slots, &surface_table(), 7) + .expect("a declared-but-unused film grain tool still decodes"); + assert_eq!( + va.pic_params.film_grain_info, + crate::va_av1::VaFilmGrainStructAV1::zeroed(), + "apply_grain is 0, which libva documents as 'set the rest to zero'" + ); + assert_eq!( + va.pic_params.seq_info_fields & (1 << 15), + 1 << 15, + "the sequence's declaration is still reported" + ); + } + + /// `order_hint_bits_minus_1` must be 0 — not 255 — when order hints are off. + /// + /// The parser stores **-1** there, and `as u8` on that is 255: a decoder told + /// its order hints are 256 bits wide. Worth its own test because no vector here + /// disables order hints, so nothing else would ever exercise the branch. + #[test] + fn order_hints_off_sends_zero_not_the_parsers_minus_one() { + let mut planner = Av1Planner::new(); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let key = planner + .plan_au(first) + .expect("the key frame plans") + .first() + .expect("a frame") + .clone(); + assert!( + key.sequence.enable_order_hint, + "the vector enables order hints; this test is about the other branch" + ); + let mut seq = (*key.sequence).clone(); + seq.enable_order_hint = false; + seq.order_hint_bits_minus_1 = -1; + seq.order_hint_bits = 0; + let plan = AuPlanAv1 { + sequence: std::rc::Rc::new(seq), + ..key + }; + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let va = plan_to_va_av1(&plan, first, &mut slots, &surface_table(), 7).expect("converts"); + assert_eq!(va.pic_params.order_hint_bits_minus_1, 0); + assert_eq!( + va.pic_params.seq_info_fields & (1 << 7), + 0, + "and the sequence flag says so too" + ); + } + + /// A frame that refreshes no slot gives its ledger slot straight back. + /// + /// Nine such frames would otherwise fill a nine-slot ledger and kill the session + /// with `SlotError::Full` on a perfectly legal stream, which is the defect the + /// Vulkan and DXVA rungs each had to close separately. + #[test] + fn a_frame_that_refreshes_nothing_returns_its_slot() { + let mut planner = Av1Planner::new(); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let key = planner + .plan_au(first) + .expect("the key frame plans") + .first() + .expect("a frame") + .clone(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let surfaces = surface_table(); + + // The real key frame refreshes all eight slots and keeps its ledger slot. + let va = plan_to_va_av1(&key, first, &mut slots, &surfaces, 7).expect("converts"); + assert_eq!(va.setup_slot, Some(0)); + assert_eq!(slots.active(), 1); + + // The same frame with `refresh_frame_flags == 0`: converted, then released. + let mut header = (*key.header).clone(); + header.refresh_frame_flags = 0; + let ephemeral = AuPlanAv1 { + header: std::rc::Rc::new(header), + dpb: pf_bitstream::av1::DpbUpdate { + stored: Some(999), + outputs: vec![999], + removed: Vec::new(), + }, + ..key + }; + let va = plan_to_va_av1(&ephemeral, first, &mut slots, &surfaces, 8).expect("converts"); + assert_eq!( + va.setup_slot, None, + "nothing binds the surface — only the pending output claims it" + ); + assert_eq!( + slots.active(), + 1, + "the ledger is back where it was; a ninth such frame must still fit" + ); + assert_eq!(slots.slot_of(999), None); + } + + /// A ledger sized for another codec is refused rather than silently overflowed. + #[test] + fn a_ledger_of_the_wrong_capacity_is_refused() { + let mut planner = Av1Planner::new(); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let key = planner + .plan_au(first) + .expect("plans") + .first() + .expect("a frame") + .clone(); + // An H.264-shaped ledger (4-frame DPB) cannot hold AV1's eight slots. + let mut slots = SlotMap::new(4); + assert_eq!( + plan_to_va_av1(&key, first, &mut slots, &surface_table(), 7).err(), + Some(PlanToVaAv1Error::CapacityMismatch { + required: 9, + capacity: 5 + }) + ); + } + + /// A short surface table is refused BEFORE the ledger is touched, so the caller's + /// post-call bind is always in range. + #[test] + fn a_short_surface_table_is_refused_before_any_mutation() { + let mut planner = Av1Planner::new(); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let key = planner + .plan_au(first) + .expect("plans") + .first() + .expect("a frame") + .clone(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let short = vec![0u32; NUM_REF_SLOTS]; + assert!(matches!( + plan_to_va_av1(&key, first, &mut slots, &short, 7).err(), + Some(PlanToVaAv1Error::SurfaceOutOfRange { .. }) + )); + assert_eq!(slots.active(), 0); + } + + /// **The lost-packet regression.** A frame header whose tile groups did not arrive + /// refuses — and the GOP survives it. + /// + /// This is the shape one lost UDP packet makes, and pf-bitstream produces it + /// deliberately: `plan_au` pushes a plan whose picture IS stored and whose tile list + /// is short or empty, with a `TruncatedAu` warning saying so. The planner's own + /// reference store already holds that picture by then, so a refusal that skipped + /// this rung's `slots.assign` would leave the two permanently one picture apart — + /// and the very next frame that names it would refuse with `UnresolvedReference`, + /// which is ALSO before the assignment and so can never repair. Every frame to the + /// next shown key frame would hard-error: one lost packet, one lost GOP. + /// + /// So this test asserts the three things that stop that: the refusal is + /// recognisable as damage ([`PlanToVaAv1Error::lost_tiles`]), the ledger holds the + /// picture the planner stored, and the NEXT access unit converts — with the lost + /// picture's slot concealed by a live surface rather than resolved to the surface + /// of whatever picture held that slot before. + #[test] + fn a_truncated_access_unit_refuses_but_leaves_the_ledger_in_step() { + let packets: Vec<&[u8]> = IvfIterator::new(AV1_25FPS).take(3).collect(); + assert_eq!(packets.len(), 3, "the vector has at least three packets"); + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let mut slot_surface = vec![VA_INVALID_SURFACE; NUM_REF_SLOTS + 1]; + + // --- packet 0: an ordinary shown key frame ----------------------------- + let key = planner.plan_au(packets[0]).expect("plans").remove(0); + let key_id = key.dpb.stored.expect("the key frame decodes"); + plan_to_va_av1(&key, packets[0], &mut slots, &slot_surface.clone(), 0x9001) + .expect("the key frame converts"); + bind(&mut slot_surface, &slots, Some(key_id), Some(0x9001)); + + // --- packet 1: two frames, and the FIRST loses its tile groups --------- + // + // Packet 1 of this vector is the standard AV1 shape: a hidden ALTREF that later + // frames predict from, then the frame that displays. Losing the hidden one is + // the worst case — nothing shows it, so nothing else would ever notice. + let mut unit = planner.plan_au(packets[1]).expect("plans"); + assert_eq!( + unit.len(), + 2, + "packet 1 is a hidden ALTREF plus a shown frame" + ); + let shown = unit.remove(1); + let mut lost = unit.remove(0); + let lost_id = lost.dpb.stored.expect("a truncated frame is still STORED"); + assert!( + !lost.header.show_frame, + "the frame this test loses is the hidden one" + ); + assert!( + !lost.tiles.is_empty(), + "the vector's own packet carries tiles; this test takes them away" + ); + // Exactly what pf-bitstream hands over when the tile-group OBUs are gone: the + // picture is stored, the tile list is empty, and the warning says damage. + lost.tiles.clear(); + lost.warnings + .push(pf_bitstream::av1::PlanWarning::TruncatedAu { offset: 0 }); + assert!( + lost.warnings.iter().any(crate::is_integrity_warning_av1), + "the plan this test drives must be one the rung CONCEALS" + ); + + let refusal = plan_to_va_av1(&lost, packets[1], &mut slots, &slot_surface.clone(), 0x9002) + .expect_err("no tiles, nothing to submit"); + assert_eq!(refusal, PlanToVaAv1Error::NoTiles); + assert!( + refusal.lost_tiles(), + "the caller tells damage from a defect through this predicate; a refusal \ + it does not recognise is a hard error and demotes the rung" + ); + let ledger_slot = slots.slot_of(lost_id).expect( + "THE REGRESSION: the planner stored this picture, so the ledger must hold \ + it too — without this every later reference to it is a hard Err that \ + never repairs", + ); + // The caller's half of the contract: the slot is live, but NOTHING is bound to + // it, because nothing was decoded. + bind(&mut slot_surface, &slots, Some(lost_id), None); + assert_eq!( + slot_surface[usize::from(ledger_slot)], + VA_INVALID_SURFACE, + "a picture that never decoded must not inherit the surface of whatever \ + held its ledger slot before" + ); + + // --- the rest of the unit, and the next one, must still convert -------- + let mut substituted_somewhere = false; + for (plan, packet, surface) in [ + (&shown, packets[1], 0x9003u32), + ( + &planner.plan_au(packets[2]).expect("plans").remove(0), + packets[2], + 0x9004, + ), + ] { + let id = plan.dpb.stored.expect("decodes"); + assert!( + plan.dpb_refs.iter().any(|r| r.id == lost_id), + "this frame must reference the truncated picture or it proves nothing" + ); + let va = plan_to_va_av1(plan, packet, &mut slots, &slot_surface.clone(), surface) + .expect("a frame after a lost one converts — it does not hard-error"); + + // Every slot the lost picture holds is concealed with a LIVE surface, and + // the surface chosen is a picture that really decoded (the key frame's), + // never the `VA_INVALID_SURFACE` a driver would dereference. + for r in &plan.dpb_refs { + let bit = 1u8 << r.slot; + if r.id == lost_id { + substituted_somewhere = true; + assert_eq!( + va.substituted_refs & bit, + bit, + "slot {} holds a picture with no surface and must be reported \ + substituted", + r.slot + ); + assert_eq!( + va.pic_params.ref_frame_map[usize::from(r.slot)], + 0x9001, + "and it must point at a picture that DECODED — the key \ + frame's surface — rather than at nothing" + ); + } else { + assert_eq!( + va.substituted_refs & bit, + 0, + "slot {} resolved; substituting it would hide a real reference", + r.slot + ); + } + } + bind(&mut slot_surface, &slots, Some(id), Some(surface)); + } + assert!( + substituted_somewhere, + "no frame ever named the lost picture, so nothing above was checked" + ); + } + + /// A store that resolved NOTHING still submits live surfaces. + /// + /// The fallback arm of the substitution, which the truncated-AU test above cannot + /// reach: with no decoded reference to reach for, the decode target itself is the + /// "alternative frame buffer" `va_dec_av1.h:352` prescribes. What must never + /// happen is `VA_INVALID_SURFACE` reaching a driver the same header says is *"not + /// responsible to validate reference frames' id"*. + #[test] + fn a_store_with_no_surfaces_at_all_falls_back_to_the_decode_target() { + let packets: Vec<&[u8]> = IvfIterator::new(AV1_25FPS).take(2).collect(); + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + + let key = planner.plan_au(packets[0]).expect("plans").remove(0); + let key_id = key.dpb.stored.expect("decodes"); + plan_to_va_av1(&key, packets[0], &mut slots, &surface_table(), 0x9001).expect("converts"); + assert!(slots.slot_of(key_id).is_some()); + + // The key frame's picture holds every slot, and the caller bound none of them — + // the state after a whole access unit was refused. + let unbound = vec![VA_INVALID_SURFACE; NUM_REF_SLOTS + 1]; + let next = planner.plan_au(packets[1]).expect("plans").remove(0); + assert_eq!(next.dpb_refs.len(), NUM_REF_SLOTS, "a full store"); + let va = plan_to_va_av1(&next, packets[1], &mut slots, &unbound, 0x9002).expect("converts"); + assert_eq!( + va.substituted_refs, 0xff, + "every slot of the store was concealed" + ); + assert_eq!( + va.pic_params.ref_frame_map, [0x9002; NUM_REF_SLOTS], + "with nothing else live, the decode target is the substitute" + ); + + // ⚠ And a SHOWN KEY FRAME is exempt: libavcodec publishes an all-invalid map + // there deliberately, and that is the one path every driver is exercised on. + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let va = plan_to_va_av1(&key, packets[0], &mut slots, &unbound, 0x9001).expect("converts"); + assert_eq!(va.substituted_refs, 0); + assert_eq!(va.pic_params.ref_frame_map, [VA_INVALID_SURFACE; 8]); + } +} diff --git a/crates/pf-vaadec/src/pic_h265.rs b/crates/pf-vaadec/src/pic_h265.rs new file mode 100644 index 00000000..1ff59c41 --- /dev/null +++ b/crates/pf-vaadec/src/pic_h265.rs @@ -0,0 +1,681 @@ +//! One HEVC [`AuPlanH265`] into libva's buffers — the H.265 twin of [`crate::pic`], +//! with the same transaction discipline and the same refusal-over-narrowing posture. +//! +//! # What differs from the H.264 conversion, and why each one bites +//! +//! * **`ReferenceFrames` is 15 entries, not 16.** +//! * **The reference sets are FLAGS, not arrays.** `RefPicSetStCurrBefore/After/LtCurr` +//! do not exist here: membership is ORed into each DPB entry's own `flags` as +//! `VA_PICTURE_HEVC_RPS_ST_CURR_BEFORE` / `_AFTER` / `_LT_CURR`. Vulkan wants slot +//! indices in identically named arrays and DXVA wants list positions in them — this +//! is the third convention, and confusing the first two is what made HEVC +//! unplayable on every driver. +//! * **The per-slice lists are INDICES into `ReferenceFrames`**, not pictures and not +//! surfaces, with `0xff` for an unused entry. So the DPB array must be built first +//! and every list entry resolved through it — a picture a slice names that is not in +//! the marked DPB is a refusal here rather than something to paper over. +//! * **The offset is in BYTES.** `slice_data()` is byte-aligned by `byte_alignment()`, +//! so `header_bit_size / 8` is exact — asserted, not assumed, because a rounded +//! offset would decode garbage from the first inter picture. +//! * **The IQ matrix is optional** and gated on `scaling_list_enabled_flag`, exactly +//! as the DXVA rung gates its `qmatrix`. Submitting one built from an all-zero +//! parser default is the defect review round 13 caught on the DXVA side: a driver +//! MUST apply what it is handed, so every residual would dequantise to zero. + +use std::ops::Range; + +use cros_codecs::codec::h265::parser::SliceType as SliceTypeH265; +use pf_bitstream::h265::AuPlan as AuPlanH265; +use pf_bitstream::h265::PicId; + +use crate::va::VA_SLICE_DATA_FLAG_ALL; +use crate::va_h265::LongSliceFlagsH265; +use crate::va_h265::PicFieldsH265; +use crate::va_h265::SliceParsingFieldsH265; +use crate::va_h265::VaIqMatrixBufferHEVC; +use crate::va_h265::VaPictureHEVC; +use crate::va_h265::VaPictureParameterBufferHEVC; +use crate::va_h265::VaSliceParameterBufferHEVC; +use crate::va_h265::REFERENCE_FRAMES_LEN_H265; +use crate::va_h265::REF_PIC_LIST_LEN_H265; +use crate::va_h265::VA_PICTURE_HEVC_LONG_TERM_REFERENCE; +use crate::va_h265::VA_PICTURE_HEVC_RPS_LT_CURR; +use crate::va_h265::VA_PICTURE_HEVC_RPS_ST_CURR_AFTER; +use crate::va_h265::VA_PICTURE_HEVC_RPS_ST_CURR_BEFORE; +use crate::SlotError; +use crate::SlotMap; + +/// Everything one HEVC `vaRenderPicture` sequence needs. +#[derive(Debug, Clone)] +pub struct DecodePlanVaH265 { + pub pic_params: VaPictureParameterBufferHEVC, + /// `None` unless the sequence enables scaling lists — the buffer is then not + /// submitted at all (module docs). + pub iq_matrix: Option, + pub slices: Vec, + /// Each slice's data range, start code excluded. Parallel to [`Self::slices`]. + pub slice_data: Vec>, + pub setup_slot: u8, +} + +/// Why an HEVC plan cannot be expressed as VAAPI buffers. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanToVaH265Error { + NoSlices, + NoStoredId, + SeparateColourPlanes, + CapacityMismatch { + required: usize, + capacity: usize, + }, + /// A slice list, or an RPS set, named a picture the marked DPB does not hold — + /// and HEVC's lists are indices INTO that array, so there is nothing to fall + /// back to. + UnresolvedReference(PicId), + /// More marked references than `ReferenceFrames[15]` can express. + TooManyReferences(usize), + RefListTooLong { + slice: usize, + len: usize, + }, + SurfaceOutOfRange { + slot: u8, + surfaces: usize, + }, + SliceRange { + slice: usize, + }, + /// `header_bit_size` is not a whole number of bytes. `slice_data()` is + /// byte-aligned, so this means the parser and this conversion disagree about + /// where the header ended — never something to round. + UnalignedSliceHeader { + slice: usize, + bits: u32, + }, + Slot(SlotError), +} + +impl From for PlanToVaH265Error { + fn from(e: SlotError) -> Self { + PlanToVaH265Error::Slot(e) + } +} + +impl std::fmt::Display for PlanToVaH265Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlanToVaH265Error::NoSlices => write!(f, "the access unit planned no slices"), + PlanToVaH265Error::NoStoredId => write!(f, "the plan stored no picture id"), + PlanToVaH265Error::SeparateColourPlanes => { + write!(f, "separate colour planes are outside the envelope") + } + PlanToVaH265Error::CapacityMismatch { required, capacity } => write!( + f, + "the slot map holds {capacity} slots, this stream needs {required}" + ), + PlanToVaH265Error::UnresolvedReference(id) => { + write!(f, "picture {id} is not in the marked DPB array") + } + PlanToVaH265Error::TooManyReferences(n) => { + write!(f, "{n} marked references exceed ReferenceFrames[15]") + } + PlanToVaH265Error::RefListTooLong { slice, len } => { + write!(f, "slice {slice}: reference list of {len} exceeds 15") + } + PlanToVaH265Error::SurfaceOutOfRange { slot, surfaces } => { + write!(f, "DPB slot {slot} has no surface in a table of {surfaces}") + } + PlanToVaH265Error::SliceRange { slice } => { + write!( + f, + "slice {slice}: byte range is not a start-code-prefixed NAL" + ) + } + PlanToVaH265Error::UnalignedSliceHeader { slice, bits } => write!( + f, + "slice {slice}: a {bits}-bit header is not byte-aligned, so \ + slice_data_byte_offset cannot be exact" + ), + PlanToVaH265Error::Slot(e) => write!(f, "DPB slot map: {e:?}"), + } + } +} + +impl std::error::Error for PlanToVaH265Error {} + +/// Convert one planned HEVC access unit. See [`crate::pic::plan_to_va`] for the +/// parameter contract — `au`, `surfaces` and `setup_surface` mean the same things, +/// including the reason the decode target is bound by the caller rather than read +/// out of a slot-indexed table. +pub fn plan_to_va_h265( + plan: &AuPlanH265, + au: &[u8], + slots: &mut SlotMap, + surfaces: &[u32], + setup_surface: u32, +) -> Result { + if plan.slices.is_empty() { + return Err(PlanToVaH265Error::NoSlices); + } + let setup_id = plan.dpb.stored.ok_or(PlanToVaH265Error::NoStoredId)?; + let sps = &plan.sps; + let pps = &plan.pps; + let pic = &plan.picture; + + if sps.separate_colour_plane_flag { + return Err(PlanToVaH265Error::SeparateColourPlanes); + } + let required = pic.max_dpb_frames + 1; + if slots.capacity() != required { + return Err(PlanToVaH265Error::CapacityMismatch { + required, + capacity: slots.capacity(), + }); + } + // See the H.264 twin: a pre-check, so the caller's post-call bind of + // `setup_surface` to the returned slot is always in range. + if surfaces.len() < slots.capacity() { + return Err(PlanToVaH265Error::SurfaceOutOfRange { + slot: (slots.capacity() - 1) as u8, + surfaces: surfaces.len(), + }); + } + if plan.dpb_refs.len() > REFERENCE_FRAMES_LEN_H265 { + return Err(PlanToVaH265Error::TooManyReferences(plan.dpb_refs.len())); + } + + // --- the DPB array, and the index every slice list will speak in --------- + // + // Built FIRST because the per-slice lists are indices into it. `dpb_refs` is + // the marked DPB — a superset of the three current sets, since RefPicSet*Foll + // pictures stay marked for later access units. + let mut reference_frames = [VaPictureHEVC::invalid(); REFERENCE_FRAMES_LEN_H265]; + let mut index_of: Vec<(PicId, u8)> = Vec::with_capacity(plan.dpb_refs.len()); + for (slot_out, rp) in reference_frames.iter_mut().zip(&plan.dpb_refs) { + let slot = slots + .slot_of(rp.id) + .ok_or(PlanToVaH265Error::UnresolvedReference(rp.id))?; + let surface = + *surfaces + .get(usize::from(slot)) + .ok_or(PlanToVaH265Error::SurfaceOutOfRange { + slot, + surfaces: surfaces.len(), + })?; + *slot_out = VaPictureHEVC { + picture_id: surface, + pic_order_cnt: rp.pic_order_cnt, + flags: if rp.is_long_term { + VA_PICTURE_HEVC_LONG_TERM_REFERENCE + } else { + 0 + }, + va_reserved: [0; 4], + }; + index_of.push((rp.id, index_of.len() as u8)); + } + + // Membership flags, ORed onto the entries the three current sets name. This is + // VAAPI's whole expression of the RPS — there is no array to fill. + for (set, flag) in [ + (&plan.rps.st_curr_before, VA_PICTURE_HEVC_RPS_ST_CURR_BEFORE), + (&plan.rps.st_curr_after, VA_PICTURE_HEVC_RPS_ST_CURR_AFTER), + (&plan.rps.lt_curr, VA_PICTURE_HEVC_RPS_LT_CURR), + ] { + for rp in set { + let idx = index_of + .iter() + .find(|(id, _)| *id == rp.id) + .map(|(_, i)| usize::from(*i)) + .ok_or(PlanToVaH265Error::UnresolvedReference(rp.id))?; + reference_frames[idx].flags |= flag; + } + } + + let find_index = |id: PicId| -> Result { + index_of + .iter() + .find(|(other, _)| *other == id) + .map(|(_, i)| *i) + .ok_or(PlanToVaH265Error::UnresolvedReference(id)) + }; + + // --- per-slice records --------------------------------------------------- + + let mut slices = Vec::with_capacity(plan.slices.len()); + let mut slice_data = Vec::with_capacity(plan.slices.len()); + for (index, sp) in plan.slices.iter().enumerate() { + let hdr = &sp.header; + let mut rec = VaSliceParameterBufferHEVC::zeroed(); + + let bytes = au + .get(sp.data.clone()) + .ok_or(PlanToVaH265Error::SliceRange { slice: index })?; + let prefix = crate::pic::start_code_len(bytes) + .ok_or(PlanToVaH265Error::SliceRange { slice: index })?; + let payload = sp.data.start + prefix..sp.data.end; + rec.slice_data_size = (payload.end - payload.start) as u32; + rec.slice_data_offset = 0; + rec.slice_data_flag = VA_SLICE_DATA_FLAG_ALL; + if hdr.header_bit_size % 8 != 0 { + return Err(PlanToVaH265Error::UnalignedSliceHeader { + slice: index, + bits: hdr.header_bit_size, + }); + } + rec.slice_data_byte_offset = hdr.header_bit_size / 8; + rec.slice_data_num_emu_prevn_bytes = hdr.n_emulation_prevention_bytes as u16; + slice_data.push(payload); + + rec.slice_segment_address = hdr.segment_address; + rec.collocated_ref_idx = hdr.collocated_ref_idx; + rec.num_ref_idx_l0_active_minus1 = hdr.num_ref_idx_l0_active_minus1; + rec.num_ref_idx_l1_active_minus1 = hdr.num_ref_idx_l1_active_minus1; + rec.slice_qp_delta = hdr.qp_delta; + rec.slice_cb_qp_offset = hdr.cb_qp_offset; + rec.slice_cr_qp_offset = hdr.cr_qp_offset; + rec.slice_beta_offset_div2 = hdr.beta_offset_div2; + rec.slice_tc_offset_div2 = hdr.tc_offset_div2; + rec.five_minus_max_num_merge_cand = hdr.five_minus_max_num_merge_cand; + rec.num_entry_point_offsets = hdr.num_entry_point_offsets as u16; + + rec.long_slice_flags = LongSliceFlagsH265 { + // The plan is one picture, so the last record IS the last slice of it. + last_slice_of_pic: index + 1 == plan.slices.len(), + dependent_slice_segment_flag: hdr.dependent_slice_segment_flag, + // H.265's own numbering (B=0, P=1, I=2), which is what libva's two bits + // take — no remap, unlike H.264's. + slice_type: hdr.type_ as u8, + color_plane_id: 0, + slice_sao_luma_flag: hdr.sao_luma_flag, + slice_sao_chroma_flag: hdr.sao_chroma_flag, + mvd_l1_zero_flag: hdr.mvd_l1_zero_flag, + cabac_init_flag: hdr.cabac_init_flag, + slice_temporal_mvp_enabled_flag: hdr.temporal_mvp_enabled_flag, + slice_deblocking_filter_disabled_flag: hdr.deblocking_filter_disabled_flag, + collocated_from_l0_flag: hdr.collocated_from_l0_flag, + slice_loop_filter_across_slices_enabled_flag: hdr + .loop_filter_across_slices_enabled_flag, + } + .pack(); + + for (list_out, list_in) in [(0usize, &sp.ref_list0), (1usize, &sp.ref_list1)] { + if list_in.len() > REF_PIC_LIST_LEN_H265 { + return Err(PlanToVaH265Error::RefListTooLong { + slice: index, + len: list_in.len(), + }); + } + for (n, rp) in list_in.iter().enumerate() { + rec.ref_pic_list[list_out][n] = find_index(rp.id)?; + } + } + + // 7.3.6.1: an explicit weight table is only coded for a P slice when the PPS + // enables weighted P prediction, or a B slice when it enables weighted + // bi-prediction. Copying it anywhere else hands the driver parser defaults as + // though the stream had coded them. + let weighted = (pps.weighted_pred_flag && hdr.type_ == SliceTypeH265::P) + || (pps.weighted_bipred_flag && hdr.type_ == SliceTypeH265::B); + if weighted { + let pwt = &hdr.pred_weight_table; + rec.luma_log2_weight_denom = pwt.luma_log2_weight_denom; + rec.delta_chroma_log2_weight_denom = pwt.delta_chroma_log2_weight_denom; + rec.delta_luma_weight_l0 = pwt.delta_luma_weight_l0; + rec.luma_offset_l0 = pwt.luma_offset_l0; + rec.delta_chroma_weight_l0 = pwt.delta_chroma_weight_l0; + rec.delta_luma_weight_l1 = pwt.delta_luma_weight_l1; + rec.luma_offset_l1 = pwt.luma_offset_l1; + rec.delta_chroma_weight_l1 = pwt.delta_chroma_weight_l1; + + // libva takes the DERIVED ChromaOffsetLX; the parser stores the coded + // delta. Clamped into a legal shift because a malformed denominator must + // not panic a decode thread. + let denom = (i32::from(pwt.luma_log2_weight_denom) + + i32::from(pwt.delta_chroma_log2_weight_denom)) + .clamp(0, 7); + let half_range = if sps.range_extension.high_precision_offsets_enabled_flag { + 1i32 << (i32::from(sps.bit_depth_chroma_minus8) + 8 - 1) + } else { + 128 + }; + rec.chroma_offset_l0 = chroma_offsets( + &pwt.delta_chroma_weight_l0, + &pwt.delta_chroma_offset_l0, + denom, + half_range, + ); + rec.chroma_offset_l1 = chroma_offsets( + &pwt.delta_chroma_weight_l1, + &pwt.delta_chroma_offset_l1, + denom, + half_range, + ); + } + + slices.push(rec); + } + + // --- mutations, after every fallible step -------------------------------- + + let setup_evicted = plan.dpb.removed.contains(&setup_id); + for &id in &plan.dpb.removed { + if id == setup_id { + continue; + } + let _ = slots.release(id); + } + let setup_slot = slots.assign(setup_id)?; + if setup_evicted { + slots.release(setup_id); + } + let pic_params = VaPictureParameterBufferHEVC { + curr_pic: VaPictureHEVC { + picture_id: setup_surface, + pic_order_cnt: pic.pic_order_cnt, + flags: 0, + va_reserved: [0; 4], + }, + reference_frames, + pic_width_in_luma_samples: sps.pic_width_in_luma_samples, + pic_height_in_luma_samples: sps.pic_height_in_luma_samples, + pic_fields: PicFieldsH265 { + chroma_format_idc: sps.chroma_format_idc, + separate_colour_plane_flag: sps.separate_colour_plane_flag, + pcm_enabled_flag: sps.pcm_enabled_flag, + scaling_list_enabled_flag: sps.scaling_list_enabled_flag, + transform_skip_enabled_flag: pps.transform_skip_enabled_flag, + amp_enabled_flag: sps.amp_enabled_flag, + strong_intra_smoothing_enabled_flag: sps.strong_intra_smoothing_enabled_flag, + sign_data_hiding_enabled_flag: pps.sign_data_hiding_enabled_flag, + constrained_intra_pred_flag: pps.constrained_intra_pred_flag, + cu_qp_delta_enabled_flag: pps.cu_qp_delta_enabled_flag, + weighted_pred_flag: pps.weighted_pred_flag, + weighted_bipred_flag: pps.weighted_bipred_flag, + transquant_bypass_enabled_flag: pps.transquant_bypass_enabled_flag, + tiles_enabled_flag: pps.tiles_enabled_flag, + entropy_coding_sync_enabled_flag: pps.entropy_coding_sync_enabled_flag, + pps_loop_filter_across_slices_enabled_flag: pps.loop_filter_across_slices_enabled_flag, + loop_filter_across_tiles_enabled_flag: pps.loop_filter_across_tiles_enabled_flag, + pcm_loop_filter_disabled_flag: sps.pcm_loop_filter_disabled_flag, + // Both are DERIVED hints a decoder may optimise on. libavcodec's VAAPI + // backend leaves them 0 for every stream, and a wrong "no reordering" + // claim is a correctness bug rather than a slow path — so 0 it is. + no_pic_reordering_flag: false, + no_bi_pred_flag: false, + } + .pack(), + sps_max_dec_pic_buffering_minus1: sps.max_dec_pic_buffering_minus1 + [usize::from(sps.max_sub_layers_minus1)], + bit_depth_luma_minus8: sps.bit_depth_luma_minus8, + bit_depth_chroma_minus8: sps.bit_depth_chroma_minus8, + pcm_sample_bit_depth_luma_minus1: sps.pcm_sample_bit_depth_luma_minus1, + pcm_sample_bit_depth_chroma_minus1: sps.pcm_sample_bit_depth_chroma_minus1, + log2_min_luma_coding_block_size_minus3: sps.log2_min_luma_coding_block_size_minus3, + log2_diff_max_min_luma_coding_block_size: sps.log2_diff_max_min_luma_coding_block_size, + log2_min_transform_block_size_minus2: sps.log2_min_luma_transform_block_size_minus2, + log2_diff_max_min_transform_block_size: sps.log2_diff_max_min_luma_transform_block_size, + log2_min_pcm_luma_coding_block_size_minus3: sps.log2_min_pcm_luma_coding_block_size_minus3, + log2_diff_max_min_pcm_luma_coding_block_size: sps + .log2_diff_max_min_pcm_luma_coding_block_size, + max_transform_hierarchy_depth_intra: sps.max_transform_hierarchy_depth_intra, + max_transform_hierarchy_depth_inter: sps.max_transform_hierarchy_depth_inter, + init_qp_minus26: pps.init_qp_minus26, + diff_cu_qp_delta_depth: pps.diff_cu_qp_delta_depth, + pps_cb_qp_offset: pps.cb_qp_offset, + pps_cr_qp_offset: pps.cr_qp_offset, + log2_parallel_merge_level_minus2: pps.log2_parallel_merge_level_minus2, + num_tile_columns_minus1: pps.num_tile_columns_minus1, + num_tile_rows_minus1: pps.num_tile_rows_minus1, + column_width_minus1: narrow_19(&pps.column_width_minus1), + row_height_minus1: narrow_21(&pps.row_height_minus1), + slice_parsing_fields: SliceParsingFieldsH265 { + lists_modification_present_flag: pps.lists_modification_present_flag, + long_term_ref_pics_present_flag: sps.long_term_ref_pics_present_flag, + sps_temporal_mvp_enabled_flag: sps.temporal_mvp_enabled_flag, + cabac_init_present_flag: pps.cabac_init_present_flag, + output_flag_present_flag: pps.output_flag_present_flag, + dependent_slice_segments_enabled_flag: pps.dependent_slice_segments_enabled_flag, + pps_slice_chroma_qp_offsets_present_flag: pps.slice_chroma_qp_offsets_present_flag, + sample_adaptive_offset_enabled_flag: sps.sample_adaptive_offset_enabled_flag, + deblocking_filter_override_enabled_flag: pps.deblocking_filter_override_enabled_flag, + pps_disable_deblocking_filter_flag: pps.deblocking_filter_disabled_flag, + slice_segment_header_extension_present_flag: pps + .slice_segment_header_extension_present_flag, + rap_pic_flag: pic.is_irap, + idr_pic_flag: pic.is_idr, + // An IRAP picture is intra by definition; nothing in our envelope codes + // an intra-only non-IRAP picture. + intra_pic_flag: pic.is_irap, + } + .pack(), + log2_max_pic_order_cnt_lsb_minus4: sps.log2_max_pic_order_cnt_lsb_minus4, + num_short_term_ref_pic_sets: sps.num_short_term_ref_pic_sets, + num_long_term_ref_pic_sps: sps.num_long_term_ref_pics_sps, + num_ref_idx_l0_default_active_minus1: pps.num_ref_idx_l0_default_active_minus1, + num_ref_idx_l1_default_active_minus1: pps.num_ref_idx_l1_default_active_minus1, + pps_beta_offset_div2: pps.beta_offset_div2, + pps_tc_offset_div2: pps.tc_offset_div2, + num_extra_slice_header_bits: pps.num_extra_slice_header_bits, + st_rps_bits: pic.short_term_ref_pic_set_size_bits, + va_reserved: [0; 8], + }; + + // The DXVA rung's rule, for the same reason: PPS lists win unless only the SPS + // carried them. Gated on scaling_list_enabled_flag so a stream that codes none + // gets no buffer at all rather than a table of parser defaults. + let iq_matrix = sps.scaling_list_enabled_flag.then(|| { + let sl = if sps.scaling_list_data_present_flag && !pps.scaling_list_data_present_flag { + &sps.scaling_list + } else { + &pps.scaling_list + }; + VaIqMatrixBufferHEVC { + scaling_list4x4: sl.scaling_list_4x4, + scaling_list8x8: sl.scaling_list_8x8, + scaling_list16x16: sl.scaling_list_16x16, + // Only matrixIds 0 and 3 exist at 32x32; the parser keeps six slots. + scaling_list32x32: [sl.scaling_list_32x32[0], sl.scaling_list_32x32[3]], + // libva takes the VALUE, the parser stores `minus8`. + scaling_list_dc16x16: std::array::from_fn(|i| { + (sl.scaling_list_dc_coef_minus8_16x16[i] + 8) as u8 + }), + scaling_list_dc32x32: [ + (sl.scaling_list_dc_coef_minus8_32x32[0] + 8) as u8, + (sl.scaling_list_dc_coef_minus8_32x32[3] + 8) as u8, + ], + va_reserved: [0; 4], + } + }); + + Ok(DecodePlanVaH265 { + pic_params, + iq_matrix, + slices, + slice_data, + setup_slot, + }) +} + +/// `ChromaOffsetLX` per equation 7-56. +/// +/// libva takes the derived value; the vendored parser stores the coded +/// `delta_chroma_offset_lX`, so the derivation happens here rather than being +/// mistaken for a straight copy — which would put a delta where a driver expects an +/// offset and tint every weighted-predicted block. +fn chroma_offsets( + delta_weight: &[[i8; 2]; 15], + delta_offset: &[[i16; 2]; 15], + chroma_log2_weight_denom: i32, + half_range: i32, +) -> [[i8; 2]; 15] { + std::array::from_fn(|i| { + std::array::from_fn(|j| { + let weight = (1i32 << chroma_log2_weight_denom) + i32::from(delta_weight[i][j]); + let offset = half_range + i32::from(delta_offset[i][j]) + - ((half_range * weight) >> chroma_log2_weight_denom); + offset.clamp(-half_range, half_range - 1).clamp(-128, 127) as i8 + }) + }) +} + +/// `column_width_minus1` is `u32` in the parser and `u16` in libva; a value past +/// 65535 columns is impossible for any real picture, so saturating is honest here +/// and a wrap would not be. +fn narrow_19(src: &[u32; 19]) -> [u16; 19] { + std::array::from_fn(|i| u16::try_from(src[i]).unwrap_or(u16::MAX)) +} + +fn narrow_21(src: &[u32; 21]) -> [u16; 21] { + std::array::from_fn(|i| u16::try_from(src[i]).unwrap_or(u16::MAX)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::va_h265::REF_PIC_LIST_UNUSED; + use crate::va_h265::VA_PICTURE_HEVC_INVALID; + + /// `SURFACE_BASE + access-unit index` — see the H.264 twin's constant. + const SURFACE_BASE: u32 = 0xa000; + + const TEST_25FPS_H265: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265" + ); + const TEST_MAIN10_H265: &[u8] = include_bytes!("../../pf-vkdecode/tests/data/test-main10.h265"); + + /// HEVC access-unit splitter — two-byte NAL header, so + /// `first_slice_segment_in_pic_flag` is the top bit at `+2` where H.264 reads + /// `+1`, and "is a slice" is the range `< 32` rather than an enum pair. + fn split_aus(stream: &[u8]) -> Vec<&[u8]> { + let mut aus = Vec::new(); + let (mut au_start, mut au_has_slice) = (0usize, false); + let mut i = 0usize; + while i + 3 <= stream.len() { + if stream[i..i + 3] != [0x00, 0x00, 0x01] { + i += 1; + continue; + } + let header = i + 3; + let mut start = i; + if start > 0 && stream[start - 1] == 0x00 { + start -= 1; + } + let is_slice = (stream[header] >> 1) & 0x3f < 32; + let first = is_slice && stream.get(header + 2).is_some_and(|b| b & 0x80 != 0); + if au_has_slice && (!is_slice || first) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + i += 3; + } + aus.push(&stream[au_start..]); + aus + } + + fn walk(stream: &[u8], expect_aus: usize, label: &str) { + use pf_bitstream::h265::H265Planner; + + let aus = split_aus(stream); + assert_eq!(aus.len(), expect_aus, "{label}: access-unit count"); + + let mut planner = H265Planner::new(); + // The caller's binding model — see the H.264 twin: one never-reused surface + // id per picture, bound to its slot after the conversion returns. + let mut surfaces: Vec = Vec::new(); + let mut slots: Option = None; + let mut saw_rps_flags = false; + let mut saw_list_entries = false; + + for (index, au) in aus.iter().enumerate() { + let plan = planner + .plan_au(au) + .unwrap_or_else(|e| panic!("{label} AU {index}: must plan, got {e:?}")); + let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames)); + surfaces.resize(map.capacity(), crate::va::VA_INVALID_SURFACE); + let setup_surface = SURFACE_BASE + index as u32; + let out = plan_to_va_h265(&plan, au, map, &surfaces, setup_surface) + .unwrap_or_else(|e| panic!("{label} AU {index}: conversion failed: {e}")); + surfaces[usize::from(out.setup_slot)] = setup_surface; + + assert_eq!(out.slices.len(), plan.slices.len()); + for (n, (rec, range)) in out.slices.iter().zip(&out.slice_data).enumerate() { + assert!(range.end <= au.len() && range.start < range.end); + assert_eq!(rec.slice_data_size as usize, range.end - range.start); + assert_ne!( + &au[range.start..range.start + 3.min(range.end - range.start)], + &[0x00, 0x00, 0x01][..], + "{label} AU {index} slice {n}: start code not trimmed" + ); + assert!(rec.slice_data_byte_offset > 0); + assert!((rec.slice_data_byte_offset as usize) < range.end - range.start); + // Every used list entry must index a VALID DPB array slot — HEVC's + // lists are indices, so a stale 0xff or an out-of-range index is a + // silently wrong reference rather than a refusal. + for list in &rec.ref_pic_list { + for &idx in list.iter().filter(|&&i| i != REF_PIC_LIST_UNUSED) { + saw_list_entries = true; + let e = out.pic_params.reference_frames[usize::from(idx)]; + assert_eq!( + e.flags & VA_PICTURE_HEVC_INVALID, + 0, + "{label} AU {index}: a list entry indexes an invalid DPB slot" + ); + } + } + } + + let valid = out + .pic_params + .reference_frames + .iter() + .filter(|e| e.flags & VA_PICTURE_HEVC_INVALID == 0) + .count(); + assert_eq!(valid, plan.dpb_refs.len(), "{label} AU {index}: DPB count"); + + let rps_marked = out + .pic_params + .reference_frames + .iter() + .filter(|e| { + e.flags + & (VA_PICTURE_HEVC_RPS_ST_CURR_BEFORE + | VA_PICTURE_HEVC_RPS_ST_CURR_AFTER + | VA_PICTURE_HEVC_RPS_LT_CURR) + != 0 + }) + .count(); + assert_eq!( + rps_marked, + plan.rps.st_curr_before.len() + + plan.rps.st_curr_after.len() + + plan.rps.lt_curr.len(), + "{label} AU {index}: exactly the current sets carry RPS flags" + ); + saw_rps_flags |= rps_marked > 0; + assert_eq!( + out.pic_params.curr_pic.pic_order_cnt, + plan.picture.pic_order_cnt + ); + } + assert!( + saw_rps_flags, + "{label}: no picture ever carried an RPS flag" + ); + assert!(saw_list_entries, "{label}: no slice ever named a reference"); + } + + #[test] + fn the_whole_vendored_vector_converts() { + walk(TEST_25FPS_H265, 250, "8-bit"); + } + + /// The Main 10 vector too — the pic-params carry depth, and a conversion that + /// only ever saw 8-bit would not notice a depth field wired to a constant. + #[test] + fn the_main10_vector_converts() { + walk(TEST_MAIN10_H265, 50, "Main 10"); + } +} diff --git a/crates/pf-vaadec/src/va.rs b/crates/pf-vaadec/src/va.rs new file mode 100644 index 00000000..6de946ca --- /dev/null +++ b/crates/pf-vaadec/src/va.rs @@ -0,0 +1,588 @@ +//! The libva decode buffer layouts for H.264, **hand-declared**. +//! +//! There is no libva binding in this workspace and this crate deliberately does not +//! introduce one: it must compile and be tested on macOS and in the Linux container, +//! where `libva` headers need not exist at all. So the structures VAAPI reads are +//! declared here as plain `#[repr(C)]` PODs, exactly as `pf-dxvadec`'s `dxva` module declares +//! DXVA's — same reasoning, same discipline. +//! +//! # These are not eyeballed +//! +//! Every size, every field offset and the bit-field allocation order below were +//! measured against the real headers (libva **2.23.0**, `x86_64-linux-gnu`) by +//! compiling a probe that printed `sizeof`, `_Alignof` and `offsetof` for each field +//! and set individual bit-fields to read the resulting word back. The numbers that +//! probe produced are pinned as `const` assertions at the bottom of this module, so +//! a mistake here is a compile error rather than a driver reading the wrong byte. +//! +//! The measured facts worth stating in prose, because they are the ones a reader +//! would otherwise assume wrongly: +//! +//! * `VAPictureH264` is **36** bytes — five 4-byte fields plus `VA_PADDING_LOW` +//! (4 × `uint32_t`) of reserved tail. It is embedded 1 + 16 times in the picture +//! parameter buffer and 64 times in the slice parameter buffer, so its size being +//! right is load-bearing for every offset after it. +//! * `VAPictureParameterBufferH264` is **672** bytes, `VAIQMatrixBufferH264` **240**, +//! and `VASliceParameterBufferH264` **3128** — the last one because it carries two +//! 32-entry reference lists *and* the full prediction weight tables inline. +//! * The three deprecated FMO fields (`num_slice_groups_minus1`, +//! `slice_group_map_type`, `slice_group_change_rate_minus1`) still occupy bytes +//! 624..628. Deprecated does not mean absent: dropping them would shift every +//! later field. They are declared, and always zero. +//! * C bit-fields on this ABI allocate from the **least significant bit**, proven +//! rather than assumed: setting `log2_max_frame_num_minus4` (the 4 bits declared +//! after eight single-bit flags and a 2-bit field) to `0xf` yields `0x0000_0f00`, +//! and `weighted_bipred_idc = 3` yields `0x0000_000c`. +//! +//! # Surface identity +//! +//! `VAPictureH264::picture_id` is a `VASurfaceID`, not a slot index — unlike DXVA, +//! where the surface index and the DPB slot are the same number by construction. +//! This crate never invents one: the conversion (`plan_to_va`) takes the caller's +//! slot → `VASurfaceID` table and indexes it, so the Linux layer owns surface +//! allocation and this half stays pure. + +/// `VA_INVALID_SURFACE` — what an unused `ReferenceFrames` / `RefPicList` entry +/// carries. Paired with [`VA_PICTURE_H264_INVALID`]; drivers key on the flag, but a +/// stale surface id in an "invalid" entry is the kind of thing that decodes fine on +/// one vendor and not another, so both are always written together. +pub const VA_INVALID_SURFACE: u32 = 0xffff_ffff; + +/// `VABufferType` for the four buffers a decode submits, measured off real headers +/// by `layout-probe.c` rather than counted off the enum in the header. +/// +/// ⚠ The last two are the trap: `VASliceParameterBufferType` is **4** and +/// `VASliceDataBufferType` is **5**, not the 3 and 4 that counting from the top +/// gives — `VABitPlaneBufferType` and `VASliceGroupMapBufferType` sit in between +/// for the codecs that need them. Getting these wrong hands the driver a slice as +/// if it were something else, which is not a decode error but a decode of garbage. +pub const VA_PICTURE_PARAMETER_BUFFER_TYPE: u32 = 0; +pub const VA_IQ_MATRIX_BUFFER_TYPE: u32 = 1; +pub const VA_SLICE_PARAMETER_BUFFER_TYPE: u32 = 4; +pub const VA_SLICE_DATA_BUFFER_TYPE: u32 = 5; + +/// Flags for [`VaPictureH264::flags`]. +pub const VA_PICTURE_H264_INVALID: u32 = 0x0000_0001; +pub const VA_PICTURE_H264_TOP_FIELD: u32 = 0x0000_0002; +pub const VA_PICTURE_H264_BOTTOM_FIELD: u32 = 0x0000_0004; +pub const VA_PICTURE_H264_SHORT_TERM_REFERENCE: u32 = 0x0000_0008; +pub const VA_PICTURE_H264_LONG_TERM_REFERENCE: u32 = 0x0000_0010; + +/// `VA_SLICE_DATA_FLAG_ALL` — this buffer holds the whole slice, which is the only +/// shape we submit (the wire delivers complete access units; nothing here streams a +/// slice in fragments). +pub const VA_SLICE_DATA_FLAG_ALL: u32 = 0x00; + +/// `VAPictureH264` — one DPB entry, or the current picture. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaPictureH264 { + /// `VASurfaceID` of the decode surface holding this picture. + pub picture_id: u32, + /// `frame_num` for a short-term reference, `LongTermFrameIdx` for a long-term + /// one — the same pair DXVA and Vulkan key references by, which is why + /// [`pf_bitstream::h264::RefPic`] already carries exactly this. + pub frame_idx: u32, + pub flags: u32, + pub top_field_order_cnt: i32, + pub bottom_field_order_cnt: i32, + /// `va_reserved[VA_PADDING_LOW]` — "must be zero". + pub va_reserved: [u32; 4], +} + +impl VaPictureH264 { + /// The entry an unused reference slot carries: invalid flag AND invalid surface. + pub const fn invalid() -> Self { + VaPictureH264 { + picture_id: VA_INVALID_SURFACE, + frame_idx: 0, + flags: VA_PICTURE_H264_INVALID, + top_field_order_cnt: 0, + bottom_field_order_cnt: 0, + va_reserved: [0; 4], + } + } +} + +/// `VAPictureParameterBufferH264::seq_fields`, unpacked. +/// +/// Declared as its own type rather than as a bare `u32` so the bit layout lives +/// beside the structure it belongs to and can be unit-tested on its own; [`Self::pack`] +/// is the only place the shifts appear. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct SeqFieldsH264 { + pub chroma_format_idc: u8, + /// `residual_colour_transform_flag` in the header's older spelling; the standard + /// renamed it `separate_colour_plane_flag`. + pub separate_colour_plane_flag: bool, + pub gaps_in_frame_num_value_allowed_flag: bool, + pub frame_mbs_only_flag: bool, + pub mb_adaptive_frame_field_flag: bool, + pub direct_8x8_inference_flag: bool, + /// A.3.3.2 — level-derived, not an SPS syntax element. + pub min_luma_bi_pred_size8x8: bool, + pub log2_max_frame_num_minus4: u8, + pub pic_order_cnt_type: u8, + pub log2_max_pic_order_cnt_lsb_minus4: u8, + pub delta_pic_order_always_zero_flag: bool, +} + +impl SeqFieldsH264 { + /// Pack to the `uint32_t` the union aliases. Bit positions are the measured + /// allocation order (module docs), LSB first, in declaration order. + pub const fn pack(self) -> u32 { + (self.chroma_format_idc as u32 & 0x3) + | ((self.separate_colour_plane_flag as u32) << 2) + | ((self.gaps_in_frame_num_value_allowed_flag as u32) << 3) + | ((self.frame_mbs_only_flag as u32) << 4) + | ((self.mb_adaptive_frame_field_flag as u32) << 5) + | ((self.direct_8x8_inference_flag as u32) << 6) + | ((self.min_luma_bi_pred_size8x8 as u32) << 7) + | ((self.log2_max_frame_num_minus4 as u32 & 0xf) << 8) + | ((self.pic_order_cnt_type as u32 & 0x3) << 12) + | ((self.log2_max_pic_order_cnt_lsb_minus4 as u32 & 0xf) << 14) + | ((self.delta_pic_order_always_zero_flag as u32) << 18) + } +} + +/// `VAPictureParameterBufferH264::pic_fields`, unpacked. See [`SeqFieldsH264`]. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct PicFieldsH264 { + pub entropy_coding_mode_flag: bool, + pub weighted_pred_flag: bool, + pub weighted_bipred_idc: u8, + pub transform_8x8_mode_flag: bool, + pub field_pic_flag: bool, + pub constrained_intra_pred_flag: bool, + /// `bottom_field_pic_order_in_frame_present_flag` in current spec spelling. + pub pic_order_present_flag: bool, + pub deblocking_filter_control_present_flag: bool, + pub redundant_pic_cnt_present_flag: bool, + /// `nal_ref_idc != 0` — a statement about THIS picture, not the PPS. + pub reference_pic_flag: bool, +} + +impl PicFieldsH264 { + pub const fn pack(self) -> u32 { + (self.entropy_coding_mode_flag as u32) + | ((self.weighted_pred_flag as u32) << 1) + | ((self.weighted_bipred_idc as u32 & 0x3) << 2) + | ((self.transform_8x8_mode_flag as u32) << 4) + | ((self.field_pic_flag as u32) << 5) + | ((self.constrained_intra_pred_flag as u32) << 6) + | ((self.pic_order_present_flag as u32) << 7) + | ((self.deblocking_filter_control_present_flag as u32) << 8) + | ((self.redundant_pic_cnt_present_flag as u32) << 9) + | ((self.reference_pic_flag as u32) << 10) + } +} + +/// `VAPictureParameterBufferH264` — one per picture, before any slice data. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaPictureParameterBufferH264 { + pub curr_pic: VaPictureH264, + /// The DPB, not this AU's reference lists. VAAPI documents this as "in DPB", + /// which is the same statement DXVA's `RefFrameList` makes and the opposite of + /// Vulkan's `pReferenceSlots` — so it is filled from pf-bitstream's per-AU + /// `dpb_refs` snapshot, the accessor M5 added for exactly this distinction. + pub reference_frames: [VaPictureH264; 16], + pub picture_width_in_mbs_minus1: u16, + pub picture_height_in_mbs_minus1: u16, + pub bit_depth_luma_minus8: u8, + pub bit_depth_chroma_minus8: u8, + pub num_ref_frames: u8, + /// Packed [`SeqFieldsH264`]. (One byte of padding precedes it — `num_ref_frames` + /// ends at 619 and the union is 4-aligned at 620.) + pub seq_fields: u32, + /// Deprecated FMO fields. Still occupy bytes 624..628; always zero here, and + /// the conversion (`plan_to_va`) refuses a stream that uses slice groups rather + /// than silently ignoring them. + pub num_slice_groups_minus1: u8, + pub slice_group_map_type: u8, + pub slice_group_change_rate_minus1: u16, + pub pic_init_qp_minus26: i8, + pub pic_init_qs_minus26: i8, + pub chroma_qp_index_offset: i8, + pub second_chroma_qp_index_offset: i8, + /// Packed [`PicFieldsH264`]. + pub pic_fields: u32, + pub frame_num: u16, + /// `va_reserved[VA_PADDING_MEDIUM]`. Two bytes of padding precede it (`frame_num` + /// ends at 638, the array is 4-aligned at 640). + pub va_reserved: [u32; 8], +} + +/// `VAIQMatrixBufferH264` — both scaling list sets, raster scan order. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaIqMatrixBufferH264 { + pub scaling_list4x4: [[u8; 16]; 6], + pub scaling_list8x8: [[u8; 64]; 2], + pub va_reserved: [u32; 4], +} + +/// `VASliceParameterBufferH264` — one per slice NALU. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaSliceParameterBufferH264 { + pub slice_data_size: u32, + pub slice_data_offset: u32, + pub slice_data_flag: u32, + /// Bit offset from the start of the NAL unit byte to the first bit of + /// `slice_data()`, counted **after emulation-prevention bytes are removed** even + /// though the buffer handed to the driver still contains them. + /// + /// Nothing else in this program needs this number: DXVA takes a byte offset to + /// the slice and Vulkan takes none at all. The vendored parser records it as + /// `SliceHeader::header_bit_size` because its own production backend is VAAPI, + /// so it costs no new parsing — see the crate docs. + pub slice_data_bit_offset: u16, + pub first_mb_in_slice: u16, + pub slice_type: u8, + pub direct_spatial_mv_pred_flag: u8, + pub num_ref_idx_l0_active_minus1: u8, + pub num_ref_idx_l1_active_minus1: u8, + pub cabac_init_idc: u8, + pub slice_qp_delta: i8, + pub disable_deblocking_filter_idc: u8, + pub slice_alpha_c0_offset_div2: i8, + pub slice_beta_offset_div2: i8, + /// 8.2.4.2 reference lists — the AU's own, unlike + /// [`VaPictureParameterBufferH264::reference_frames`]. + pub ref_pic_list0: [VaPictureH264; 32], + pub ref_pic_list1: [VaPictureH264; 32], + pub luma_log2_weight_denom: u8, + pub chroma_log2_weight_denom: u8, + pub luma_weight_l0_flag: u8, + pub luma_weight_l0: [i16; 32], + pub luma_offset_l0: [i16; 32], + pub chroma_weight_l0_flag: u8, + pub chroma_weight_l0: [[i16; 2]; 32], + pub chroma_offset_l0: [[i16; 2]; 32], + pub luma_weight_l1_flag: u8, + pub luma_weight_l1: [i16; 32], + pub luma_offset_l1: [i16; 32], + pub chroma_weight_l1_flag: u8, + pub chroma_weight_l1: [[i16; 2]; 32], + pub chroma_offset_l1: [[i16; 2]; 32], + pub va_reserved: [u32; 4], +} + +impl VaSliceParameterBufferH264 { + /// An all-zero record with the reference lists invalidated — the base every + /// slice is built from, so an unwritten entry is never a stale surface id. + pub const fn zeroed() -> Self { + VaSliceParameterBufferH264 { + slice_data_size: 0, + slice_data_offset: 0, + slice_data_flag: VA_SLICE_DATA_FLAG_ALL, + slice_data_bit_offset: 0, + first_mb_in_slice: 0, + slice_type: 0, + direct_spatial_mv_pred_flag: 0, + num_ref_idx_l0_active_minus1: 0, + num_ref_idx_l1_active_minus1: 0, + cabac_init_idc: 0, + slice_qp_delta: 0, + disable_deblocking_filter_idc: 0, + slice_alpha_c0_offset_div2: 0, + slice_beta_offset_div2: 0, + ref_pic_list0: [VaPictureH264::invalid(); 32], + ref_pic_list1: [VaPictureH264::invalid(); 32], + luma_log2_weight_denom: 0, + chroma_log2_weight_denom: 0, + luma_weight_l0_flag: 0, + luma_weight_l0: [0; 32], + luma_offset_l0: [0; 32], + chroma_weight_l0_flag: 0, + chroma_weight_l0: [[0; 2]; 32], + chroma_offset_l0: [[0; 2]; 32], + luma_weight_l1_flag: 0, + luma_weight_l1: [0; 32], + luma_offset_l1: [0; 32], + chroma_weight_l1_flag: 0, + chroma_weight_l1: [[0; 2]; 32], + chroma_offset_l1: [[0; 2]; 32], + va_reserved: [0; 4], + } + } +} + +// --------------------------------------------------------------------------- +// Layout proofs — the probe's output, pinned. +// +// libva 2.23.0, x86_64-linux-gnu. A `#[repr(C)]` Rust struct and a C struct agree +// by definition of repr(C), so these assertions are not testing the compiler: they +// are testing that the FIELDS AND THEIR ORDER above match the header, which is the +// part a human transcribed and can get wrong. +// --------------------------------------------------------------------------- + +const _: () = { + use std::mem::offset_of; + use std::mem::size_of; + + assert!(size_of::() == 36); + assert!(offset_of!(VaPictureH264, picture_id) == 0); + assert!(offset_of!(VaPictureH264, frame_idx) == 4); + assert!(offset_of!(VaPictureH264, flags) == 8); + assert!(offset_of!(VaPictureH264, top_field_order_cnt) == 12); + assert!(offset_of!(VaPictureH264, bottom_field_order_cnt) == 16); + assert!(offset_of!(VaPictureH264, va_reserved) == 20); + + assert!(size_of::() == 672); + assert!(offset_of!(VaPictureParameterBufferH264, curr_pic) == 0); + assert!(offset_of!(VaPictureParameterBufferH264, reference_frames) == 36); + assert!(offset_of!(VaPictureParameterBufferH264, picture_width_in_mbs_minus1) == 612); + assert!(offset_of!(VaPictureParameterBufferH264, picture_height_in_mbs_minus1) == 614); + assert!(offset_of!(VaPictureParameterBufferH264, bit_depth_luma_minus8) == 616); + assert!(offset_of!(VaPictureParameterBufferH264, bit_depth_chroma_minus8) == 617); + assert!(offset_of!(VaPictureParameterBufferH264, num_ref_frames) == 618); + assert!(offset_of!(VaPictureParameterBufferH264, seq_fields) == 620); + assert!(offset_of!(VaPictureParameterBufferH264, num_slice_groups_minus1) == 624); + assert!(offset_of!(VaPictureParameterBufferH264, slice_group_map_type) == 625); + assert!(offset_of!(VaPictureParameterBufferH264, slice_group_change_rate_minus1) == 626); + assert!(offset_of!(VaPictureParameterBufferH264, pic_init_qp_minus26) == 628); + assert!(offset_of!(VaPictureParameterBufferH264, pic_init_qs_minus26) == 629); + assert!(offset_of!(VaPictureParameterBufferH264, chroma_qp_index_offset) == 630); + assert!(offset_of!(VaPictureParameterBufferH264, second_chroma_qp_index_offset) == 631); + assert!(offset_of!(VaPictureParameterBufferH264, pic_fields) == 632); + assert!(offset_of!(VaPictureParameterBufferH264, frame_num) == 636); + assert!(offset_of!(VaPictureParameterBufferH264, va_reserved) == 640); + + assert!(size_of::() == 240); + assert!(offset_of!(VaIqMatrixBufferH264, scaling_list4x4) == 0); + assert!(offset_of!(VaIqMatrixBufferH264, scaling_list8x8) == 96); + assert!(offset_of!(VaIqMatrixBufferH264, va_reserved) == 224); + + assert!(size_of::() == 3128); + assert!(offset_of!(VaSliceParameterBufferH264, slice_data_size) == 0); + assert!(offset_of!(VaSliceParameterBufferH264, slice_data_offset) == 4); + assert!(offset_of!(VaSliceParameterBufferH264, slice_data_flag) == 8); + assert!(offset_of!(VaSliceParameterBufferH264, slice_data_bit_offset) == 12); + assert!(offset_of!(VaSliceParameterBufferH264, first_mb_in_slice) == 14); + assert!(offset_of!(VaSliceParameterBufferH264, slice_type) == 16); + assert!(offset_of!(VaSliceParameterBufferH264, direct_spatial_mv_pred_flag) == 17); + assert!(offset_of!(VaSliceParameterBufferH264, num_ref_idx_l0_active_minus1) == 18); + assert!(offset_of!(VaSliceParameterBufferH264, num_ref_idx_l1_active_minus1) == 19); + assert!(offset_of!(VaSliceParameterBufferH264, cabac_init_idc) == 20); + assert!(offset_of!(VaSliceParameterBufferH264, slice_qp_delta) == 21); + assert!(offset_of!(VaSliceParameterBufferH264, disable_deblocking_filter_idc) == 22); + assert!(offset_of!(VaSliceParameterBufferH264, slice_alpha_c0_offset_div2) == 23); + assert!(offset_of!(VaSliceParameterBufferH264, slice_beta_offset_div2) == 24); + assert!(offset_of!(VaSliceParameterBufferH264, ref_pic_list0) == 28); + assert!(offset_of!(VaSliceParameterBufferH264, ref_pic_list1) == 1180); + assert!(offset_of!(VaSliceParameterBufferH264, luma_log2_weight_denom) == 2332); + assert!(offset_of!(VaSliceParameterBufferH264, chroma_log2_weight_denom) == 2333); + assert!(offset_of!(VaSliceParameterBufferH264, luma_weight_l0_flag) == 2334); + assert!(offset_of!(VaSliceParameterBufferH264, luma_weight_l0) == 2336); + assert!(offset_of!(VaSliceParameterBufferH264, luma_offset_l0) == 2400); + assert!(offset_of!(VaSliceParameterBufferH264, chroma_weight_l0_flag) == 2464); + assert!(offset_of!(VaSliceParameterBufferH264, chroma_weight_l0) == 2466); + assert!(offset_of!(VaSliceParameterBufferH264, chroma_offset_l0) == 2594); + assert!(offset_of!(VaSliceParameterBufferH264, luma_weight_l1_flag) == 2722); + assert!(offset_of!(VaSliceParameterBufferH264, luma_weight_l1) == 2724); + assert!(offset_of!(VaSliceParameterBufferH264, luma_offset_l1) == 2788); + assert!(offset_of!(VaSliceParameterBufferH264, chroma_weight_l1_flag) == 2852); + assert!(offset_of!(VaSliceParameterBufferH264, chroma_weight_l1) == 2854); + assert!(offset_of!(VaSliceParameterBufferH264, chroma_offset_l1) == 2982); + assert!(offset_of!(VaSliceParameterBufferH264, va_reserved) == 3112); +}; + +#[cfg(test)] +mod tests { + use super::*; + + // The three bit patterns the probe read back off real headers. If the shifts + // above are ever "tidied", these fail with the measured value in hand. + #[test] + fn seq_fields_pack_where_the_probe_measured() { + assert_eq!( + SeqFieldsH264 { + chroma_format_idc: 3, + ..Default::default() + } + .pack(), + 0x0000_0003 + ); + assert_eq!( + SeqFieldsH264 { + log2_max_frame_num_minus4: 0xf, + ..Default::default() + } + .pack(), + 0x0000_0f00 + ); + } + + #[test] + fn pic_fields_pack_where_the_probe_measured() { + assert_eq!( + PicFieldsH264 { + reference_pic_flag: true, + ..Default::default() + } + .pack(), + 0x0000_0400 + ); + assert_eq!( + PicFieldsH264 { + weighted_bipred_idc: 3, + ..Default::default() + } + .pack(), + 0x0000_000c + ); + } + + #[test] + fn every_seq_field_owns_a_distinct_bit_range() { + // Each field set alone must light only its own bits, and the OR of all of + // them must equal the packing of all-at-once: a shift typo that overlapped + // two fields would still pass the two probe vectors above. + let each = [ + SeqFieldsH264 { + chroma_format_idc: 3, + ..Default::default() + }, + SeqFieldsH264 { + separate_colour_plane_flag: true, + ..Default::default() + }, + SeqFieldsH264 { + gaps_in_frame_num_value_allowed_flag: true, + ..Default::default() + }, + SeqFieldsH264 { + frame_mbs_only_flag: true, + ..Default::default() + }, + SeqFieldsH264 { + mb_adaptive_frame_field_flag: true, + ..Default::default() + }, + SeqFieldsH264 { + direct_8x8_inference_flag: true, + ..Default::default() + }, + SeqFieldsH264 { + min_luma_bi_pred_size8x8: true, + ..Default::default() + }, + SeqFieldsH264 { + log2_max_frame_num_minus4: 0xf, + ..Default::default() + }, + SeqFieldsH264 { + pic_order_cnt_type: 3, + ..Default::default() + }, + SeqFieldsH264 { + log2_max_pic_order_cnt_lsb_minus4: 0xf, + ..Default::default() + }, + SeqFieldsH264 { + delta_pic_order_always_zero_flag: true, + ..Default::default() + }, + ]; + let mut seen = 0u32; + for f in each { + let bits = f.pack(); + assert_ne!(bits, 0, "a field packed to nothing"); + assert_eq!(seen & bits, 0, "two fields share a bit: {bits:#010x}"); + seen |= bits; + } + let all = SeqFieldsH264 { + chroma_format_idc: 3, + separate_colour_plane_flag: true, + gaps_in_frame_num_value_allowed_flag: true, + frame_mbs_only_flag: true, + mb_adaptive_frame_field_flag: true, + direct_8x8_inference_flag: true, + min_luma_bi_pred_size8x8: true, + log2_max_frame_num_minus4: 0xf, + pic_order_cnt_type: 3, + log2_max_pic_order_cnt_lsb_minus4: 0xf, + delta_pic_order_always_zero_flag: true, + }; + assert_eq!(all.pack(), seen); + // Nothing may reach past bit 18 — the last declared bit. + assert_eq!(seen & !0x0007_ffff, 0); + } + + #[test] + fn every_pic_field_owns_a_distinct_bit_range() { + let each = [ + PicFieldsH264 { + entropy_coding_mode_flag: true, + ..Default::default() + }, + PicFieldsH264 { + weighted_pred_flag: true, + ..Default::default() + }, + PicFieldsH264 { + weighted_bipred_idc: 3, + ..Default::default() + }, + PicFieldsH264 { + transform_8x8_mode_flag: true, + ..Default::default() + }, + PicFieldsH264 { + field_pic_flag: true, + ..Default::default() + }, + PicFieldsH264 { + constrained_intra_pred_flag: true, + ..Default::default() + }, + PicFieldsH264 { + pic_order_present_flag: true, + ..Default::default() + }, + PicFieldsH264 { + deblocking_filter_control_present_flag: true, + ..Default::default() + }, + PicFieldsH264 { + redundant_pic_cnt_present_flag: true, + ..Default::default() + }, + PicFieldsH264 { + reference_pic_flag: true, + ..Default::default() + }, + ]; + let mut seen = 0u32; + for f in each { + let bits = f.pack(); + assert_ne!(bits, 0); + assert_eq!(seen & bits, 0, "two fields share a bit: {bits:#010x}"); + seen |= bits; + } + assert_eq!(seen & !0x0000_07ff, 0); + } + + #[test] + fn an_unused_reference_entry_is_invalid_in_both_ways() { + let e = VaPictureH264::invalid(); + assert_eq!(e.flags, VA_PICTURE_H264_INVALID); + assert_eq!(e.picture_id, VA_INVALID_SURFACE); + } + + #[test] + fn a_zeroed_slice_record_starts_with_invalidated_lists() { + let s = VaSliceParameterBufferH264::zeroed(); + assert!(s + .ref_pic_list0 + .iter() + .all(|e| e.flags == VA_PICTURE_H264_INVALID)); + assert!(s + .ref_pic_list1 + .iter() + .all(|e| e.picture_id == VA_INVALID_SURFACE)); + assert_eq!(s.slice_data_flag, VA_SLICE_DATA_FLAG_ALL); + } +} diff --git a/crates/pf-vaadec/src/va_av1.rs b/crates/pf-vaadec/src/va_av1.rs new file mode 100644 index 00000000..72e076ea --- /dev/null +++ b/crates/pf-vaadec/src/va_av1.rs @@ -0,0 +1,1305 @@ +//! The libva decode buffer layouts for AV1, hand-declared — the third sibling of +//! [`crate::va`] and [`crate::va_h265`], measured the same way and pinned the same +//! way. +//! +//! Sizes and offsets come from the committed `layout-probe.c` run against libva +//! **2.23.0** headers (`va_dec_av1.h`, x86_64-linux-gnu): +//! `VASegmentationStructAV1` **156**, `VAFilmGrainStructAV1` **176**, +//! `VAWarpedMotionParamsAV1` **56**, `VADecPictureParameterBufferAV1` **1160** +//! (align **8**), `VASliceParameterBufferAV1` **40**. Every bit position below was +//! read back off a real header one field at a time, not counted by eye — which +//! matters more here than for the other two codecs, because **three of AV1's six +//! bit-field unions are NARROWER than a word**: `loop_filter_info_fields` is a +//! `uint8_t`, `qmatrix_fields` and `loop_restoration_fields` are `uint16_t`. A +//! `u32` packer over any of them would write straight through the neighbouring +//! field, and on the two `uint16_t` ones that neighbour is padding on one side and +//! `mode_control_fields` / `wm[0]` on the other. +//! +//! # AV1's reference plumbing is a FIFTH convention +//! +//! This program has now written down five spellings of "which pictures does this +//! frame use", and they are not interchangeable: +//! +//! * **Vulkan H.265** — DPB *slot* indices in `RefPicSetStCurr*`; +//! * **DXVA H.265** — positions into `RefPicList[]` in identically named arrays; +//! * **VAAPI H.265** — membership *flags* ORed onto each DPB entry, with per-slice +//! lists indexing `ReferenceFrames`; +//! * **DXVA AV1** — `frame_refs[7]` by reference NAME, each entry carrying a +//! reference SLOT that indexes `RefFrameMapTextureIndex[8]`, plus that +//! reference's own size and own global motion; +//! * **VAAPI AV1** — [`VaDecPictureParameterBufferAV1::ref_frame_map`] is indexed +//! by AV1 reference **SLOT** (0..8) and holds a **`VASurfaceID`** — an actual +//! surface handle, not an index into anything — while +//! [`VaDecPictureParameterBufferAV1::ref_frame_idx`] is indexed by reference +//! **NAME** and holds *"a list of indices into `ref_frame_map[8]`"*, i.e. the +//! slot. Global motion is a **picture-level** array +//! ([`VaDecPictureParameterBufferAV1::wm`], seven entries, `wm[0]` = `LAST_FRAME`) +//! and NOT part of a reference entry, and **there is no per-reference size +//! anywhere in this structure at all**. +//! +//! Both halves of that last sentence are measured rather than assumed: +//! `grep -c ref_frame_width /usr/include/va/va_dec_av1.h` is **0** on libva 2.23.0, +//! so a VAAPI driver takes each reference's dimensions from the SURFACE it was +//! decoded into. (`pf_bitstream::av1::RefState::upscaled_width`'s doc comment says +//! VA-API has `ref_frame_width`/`ref_frame_height`; it does not, and the field is +//! still load-bearing for DXVA, which does.) The two statements that DO reach a +//! VAAPI driver — the slot table and the name table — come from `va_dec_av1.h`'s +//! own comments and from libavcodec's `vaapi_av1.c`: +//! +//! ```text +//! pic_param.ref_frame_map[i] = for i in 0..8 +//! pic_param.ref_frame_idx[i] = frame_header->ref_frame_idx[i] for i in 0..7 +//! pic_param.wm[i - 1] = +//! for i in LAST_FRAME..=ALTREF_FRAME +//! ``` +//! +//! # Where libva's AV1 buffers differ from every other codec here +//! +//! * **The "slice" parameter buffer is a TILE parameter buffer.** The header says so +//! in as many words: *"It uses the name VASliceParameterBufferAV1 to be consistent +//! with other codec, but actually means VATileParameterBufferAV1."* One record per +//! TILE, not per tile group. +//! * **Several records share one data buffer.** libavcodec's `vaapi_av1.c` calls +//! `ff_vaapi_decode_make_slice_buffer` once per tile-group OBU with `nb_params = +//! tg_end - tg_start + 1`, so one `VASliceParameterBufferType` buffer carries +//! `nb_params` ELEMENTS beside one `VASliceDataBufferType` buffer holding the whole +//! group's `tile_data` region, and each record's `slice_data_offset` is relative to +//! THAT buffer. H.264 and H.265 send one record per buffer, so this is the only +//! place `vaCreateBuffer`'s `num_elements` is not 1. +//! * **There is no IQ matrix buffer.** AV1's quantiser matrices are SELECTED by +//! index out of tables the decoder already holds +//! ([`QmatrixFieldsAV1`]), so a submission is picture parameters plus tile +//! pairs and nothing else. + +/// `VASliceParameterBufferAV1::anchor_frame_idx` on an ordinary frame. +/// +/// `anchor_frame_idx` selects a reference for LARGE-SCALE TILE decoding, which no +/// punktfunk stream and no conformance vector here uses; libavcodec leaves the whole +/// record zero-initialised and never writes the field. +pub const ANCHOR_FRAME_UNUSED: u8 = 0; + +/// `PRIMARY_REF_NONE` (AV1 spec 6.8.2): `primary_ref_frame` meaning "this frame +/// loads no propagated state". +pub const PRIMARY_REF_NONE: u8 = 7; + +/// `SUPERRES_NUM` (AV1 spec): the `superres_scale_denominator` that means "no +/// upscaling". libva documents the field as 8 when `use_superres` is 0 and 9..=16 +/// when it is 1 — so a frame without superres does NOT send 0 here. +pub const SUPERRES_NUM: u8 = 8; + +/// `VAAV1TransformationType` (measured: 0, 1, 2, 3). The same numbering AV1 5.9.24 +/// gives `GmType`, and the same the vendored parser's `WarpModelType` uses — so the +/// conversion casts rather than remaps, and this table is here to make that +/// checkable. +pub const VA_AV1_TRANSFORMATION_IDENTITY: u32 = 0; +pub const VA_AV1_TRANSFORMATION_TRANSLATION: u32 = 1; +pub const VA_AV1_TRANSFORMATION_ROTZOOM: u32 = 2; +pub const VA_AV1_TRANSFORMATION_AFFINE: u32 = 3; + +/// `ref_frame_map[8]` — AV1's `NUM_REF_FRAMES`. +pub const REF_FRAME_MAP_LEN: usize = 8; + +/// `ref_frame_idx[7]` / `wm[7]` — AV1's `REFS_PER_FRAME`. +pub const REFS_PER_FRAME: usize = 7; + +/// `ref_deltas[8]` — AV1's `TOTAL_REFS_PER_FRAME`. +pub const TOTAL_REFS_PER_FRAME: usize = 8; + +/// `cdef_y_strengths[8]` / `cdef_uv_strengths[8]` — as many as `cdef_bits` can +/// select (`1 << 3`). +pub const CDEF_MAX: usize = 8; + +/// `width_in_sbs_minus_1[63]` / `height_in_sbs_minus_1[63]`. +/// +/// ⚠ **63, not 64**, and the header explains why: *"Though the maximum number of +/// tiles is 64, since ones of the last tile are computed from ones of the other +/// tiles and frame_width/height, they are not necessarily specified."* libavcodec's +/// `vaapi_av1.c` nonetheless loops `for (i = 0; i < frame_header->tile_cols; i++)`, +/// which writes index 63 — one past the end — on a 64-column frame. The conversion +/// clamps instead; see [`crate::pic_av1`]. +pub const TILE_SBS_LEN: usize = 63; + +/// `wm[7]`'s index for a reference NAME. +/// +/// libavcodec writes `pic_param.wm[i - 1]` for `i = LAST_FRAME..=ALTREF_FRAME`, so +/// `wm[0]` is `LAST_FRAME` and `wm[6]` is `ALTREF_FRAME` — the same indexing +/// `pf_bitstream::av1::AuPlan::refs` uses, and one step off the parser's own +/// `gm_params[]`, which is indexed by the spec's reference index (`INTRA_FRAME` = 0). +pub const LAST_FRAME: usize = 1; + +// --------------------------------------------------------------------------- +// The bit-field unions, unpacked +// --------------------------------------------------------------------------- + +/// `VADecPictureParameterBufferAV1::seq_info_fields` (32 bits). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct SeqInfoFieldsAV1 { + pub still_picture: bool, + pub use_128x128_superblock: bool, + pub enable_filter_intra: bool, + pub enable_intra_edge_filter: bool, + pub enable_interintra_compound: bool, + pub enable_masked_compound: bool, + pub enable_dual_filter: bool, + pub enable_order_hint: bool, + pub enable_jnt_comp: bool, + pub enable_cdef: bool, + pub mono_chrome: bool, + pub color_range: bool, + pub subsampling_x: bool, + pub subsampling_y: bool, + /// `va_deprecated` in the header, and still part of the layout — a field that is + /// deprecated is not a field that moved. + /// + /// ⚠ **ONE bit**, where AV1's `chroma_sample_position` is a two-bit enumerator + /// (UNKNOWN 0, VERTICAL 1, COLOCATED 2). [`Self::pack`] masks, which is exactly + /// what libavcodec's assignment into the C bit-field does — so COLOCATED reaches a + /// driver as UNKNOWN through both paths. Not a defect this rung can fix: there is + /// no second bit to put it in, and the field is deprecated precisely because + /// drivers do not read it. + pub chroma_sample_position: u8, + pub film_grain_params_present: bool, +} + +impl SeqInfoFieldsAV1 { + pub const fn pack(self) -> u32 { + (self.still_picture as u32) + | ((self.use_128x128_superblock as u32) << 1) + | ((self.enable_filter_intra as u32) << 2) + | ((self.enable_intra_edge_filter as u32) << 3) + | ((self.enable_interintra_compound as u32) << 4) + | ((self.enable_masked_compound as u32) << 5) + | ((self.enable_dual_filter as u32) << 6) + | ((self.enable_order_hint as u32) << 7) + | ((self.enable_jnt_comp as u32) << 8) + | ((self.enable_cdef as u32) << 9) + | ((self.mono_chrome as u32) << 10) + | ((self.color_range as u32) << 11) + | ((self.subsampling_x as u32) << 12) + | ((self.subsampling_y as u32) << 13) + | ((self.chroma_sample_position as u32 & 0x1) << 14) + | ((self.film_grain_params_present as u32) << 15) + } +} + +/// `VADecPictureParameterBufferAV1::pic_info_fields` (32 bits). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct PicInfoFieldsAV1 { + /// 0 KEY, 1 INTER, 2 INTRA_ONLY, 3 SWITCH — the AV1 spec's own numbering, which + /// is the vendored parser's `FrameType` discriminant too. + pub frame_type: u8, + pub show_frame: bool, + pub showable_frame: bool, + pub error_resilient_mode: bool, + pub disable_cdf_update: bool, + pub allow_screen_content_tools: bool, + pub force_integer_mv: bool, + pub allow_intrabc: bool, + pub use_superres: bool, + pub allow_high_precision_mv: bool, + pub is_motion_mode_switchable: bool, + pub use_ref_frame_mvs: bool, + pub disable_frame_end_update_cdf: bool, + pub uniform_tile_spacing_flag: bool, + pub allow_warped_motion: bool, + /// Large-scale tile decoding — outside this rung's envelope, always false. + pub large_scale_tile: bool, +} + +impl PicInfoFieldsAV1 { + pub const fn pack(self) -> u32 { + (self.frame_type as u32 & 0x3) + | ((self.show_frame as u32) << 2) + | ((self.showable_frame as u32) << 3) + | ((self.error_resilient_mode as u32) << 4) + | ((self.disable_cdf_update as u32) << 5) + | ((self.allow_screen_content_tools as u32) << 6) + | ((self.force_integer_mv as u32) << 7) + | ((self.allow_intrabc as u32) << 8) + | ((self.use_superres as u32) << 9) + | ((self.allow_high_precision_mv as u32) << 10) + | ((self.is_motion_mode_switchable as u32) << 11) + | ((self.use_ref_frame_mvs as u32) << 12) + | ((self.disable_frame_end_update_cdf as u32) << 13) + | ((self.uniform_tile_spacing_flag as u32) << 14) + | ((self.allow_warped_motion as u32) << 15) + | ((self.large_scale_tile as u32) << 16) + } +} + +/// `VADecPictureParameterBufferAV1::loop_filter_info_fields` — **8 bits**, not 32. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct LoopFilterInfoFieldsAV1 { + pub sharpness_level: u8, + pub mode_ref_delta_enabled: bool, + pub mode_ref_delta_update: bool, +} + +impl LoopFilterInfoFieldsAV1 { + pub const fn pack(self) -> u8 { + (self.sharpness_level & 0x7) + | ((self.mode_ref_delta_enabled as u8) << 3) + | ((self.mode_ref_delta_update as u8) << 4) + } +} + +/// `VADecPictureParameterBufferAV1::qmatrix_fields` — **16 bits**, not 32. +/// +/// Unlike DXVA, libva carries `using_qmatrix` itself, so the three indices need no +/// `0xFF` sentinel: they are simply ignored when the flag is clear. (`DXVA_PicParams_AV1` +/// has no such flag, which is why `pf_dxvadec::pic_av1` has to send 0xFF and why +/// leaving the parser's 0 there dequantised against matrix 0 on every frame.) +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct QmatrixFieldsAV1 { + pub using_qmatrix: bool, + pub qm_y: u8, + pub qm_u: u8, + pub qm_v: u8, +} + +impl QmatrixFieldsAV1 { + pub const fn pack(self) -> u16 { + (self.using_qmatrix as u16) + | ((self.qm_y as u16 & 0xf) << 1) + | ((self.qm_u as u16 & 0xf) << 5) + | ((self.qm_v as u16 & 0xf) << 9) + } +} + +/// `VADecPictureParameterBufferAV1::mode_control_fields` (32 bits). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct ModeControlFieldsAV1 { + pub delta_q_present_flag: bool, + pub log2_delta_q_res: u8, + pub delta_lf_present_flag: bool, + pub log2_delta_lf_res: u8, + pub delta_lf_multi: bool, + /// 0 ONLY_4X4, 1 LARGEST, 2 SELECT. + pub tx_mode: u8, + pub reference_select: bool, + pub reduced_tx_set_used: bool, + pub skip_mode_present: bool, +} + +impl ModeControlFieldsAV1 { + pub const fn pack(self) -> u32 { + (self.delta_q_present_flag as u32) + | ((self.log2_delta_q_res as u32 & 0x3) << 1) + | ((self.delta_lf_present_flag as u32) << 3) + | ((self.log2_delta_lf_res as u32 & 0x3) << 4) + | ((self.delta_lf_multi as u32) << 6) + | ((self.tx_mode as u32 & 0x3) << 7) + | ((self.reference_select as u32) << 9) + | ((self.reduced_tx_set_used as u32) << 10) + | ((self.skip_mode_present as u32) << 11) + } +} + +/// `VADecPictureParameterBufferAV1::loop_restoration_fields` — **16 bits**, not 32. +/// +/// The three `*frame_restoration_type` fields take the SPEC's `FrameRestorationType` +/// (`RESTORE_NONE` 0, `RESTORE_WIENER` 1, `RESTORE_SGRPROJ` 2, `RESTORE_SWITCHABLE` +/// 3), not the coded two-bit `lr_type`. libavcodec sends +/// `remap_lr_type[frame_header->lr_type[i]]` with +/// `remap_lr_type = {NONE, SWITCHABLE, WIENER, SGRPROJ}` — i.e. it applies AV1 +/// 5.9.20's `Remap_Lr_Type` mapping — and the vendored parser has already applied it +/// (`LoopRestorationParams::frame_restoration_type` is documented "Same as +/// FrameRestorationType in the specification"), so the conversion casts the parser's +/// enum and remaps nothing. Sending the coded value instead swaps WIENER and +/// SWITCHABLE on every frame that restores. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct LoopRestorationFieldsAV1 { + pub yframe_restoration_type: u8, + pub cbframe_restoration_type: u8, + pub crframe_restoration_type: u8, + pub lr_unit_shift: u8, + pub lr_uv_shift: u8, +} + +impl LoopRestorationFieldsAV1 { + pub const fn pack(self) -> u16 { + (self.yframe_restoration_type as u16 & 0x3) + | ((self.cbframe_restoration_type as u16 & 0x3) << 2) + | ((self.crframe_restoration_type as u16 & 0x3) << 4) + | ((self.lr_unit_shift as u16 & 0x3) << 6) + | ((self.lr_uv_shift as u16 & 0x1) << 8) + } +} + +/// `VASegmentationStructAV1::segment_info_fields` (32 bits). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct SegmentInfoFieldsAV1 { + pub enabled: bool, + pub update_map: bool, + pub temporal_update: bool, + pub update_data: bool, +} + +impl SegmentInfoFieldsAV1 { + pub const fn pack(self) -> u32 { + (self.enabled as u32) + | ((self.update_map as u32) << 1) + | ((self.temporal_update as u32) << 2) + | ((self.update_data as u32) << 3) + } +} + +/// `VAFilmGrainStructAV1::film_grain_info_fields` (32 bits). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct FilmGrainInfoFieldsAV1 { + pub apply_grain: bool, + pub chroma_scaling_from_luma: bool, + pub grain_scaling_minus_8: u8, + pub ar_coeff_lag: u8, + pub ar_coeff_shift_minus_6: u8, + pub grain_scale_shift: u8, + pub overlap_flag: bool, + pub clip_to_restricted_range: bool, +} + +impl FilmGrainInfoFieldsAV1 { + pub const fn pack(self) -> u32 { + (self.apply_grain as u32) + | ((self.chroma_scaling_from_luma as u32) << 1) + | ((self.grain_scaling_minus_8 as u32 & 0x3) << 2) + | ((self.ar_coeff_lag as u32 & 0x3) << 4) + | ((self.ar_coeff_shift_minus_6 as u32 & 0x3) << 6) + | ((self.grain_scale_shift as u32 & 0x3) << 8) + | ((self.overlap_flag as u32) << 10) + | ((self.clip_to_restricted_range as u32) << 11) + } +} + +// --------------------------------------------------------------------------- +// The structures +// --------------------------------------------------------------------------- + +/// `VASegmentationStructAV1`. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaSegmentationStructAV1 { + pub segment_info_fields: u32, + /// `FeatureData[segment][feature]` **after** AV1 5.9.14's `Clip3` — libva says + /// so ("equivalent to variable FeatureData\[\]\[\] in spec, which is after + /// clip3() operation"), and the vendored parser clips as it reads + /// (`parse_segmentation_params` calls `helpers::clip3` with the spec's + /// `FEATURE_MAX`), so no clipping happens in the conversion. + pub feature_data: [[i16; 8]; 8], + /// Bit `feature` set where `feature_enabled[segment][feature]` is. Indexed by + /// SEGMENT; the bit position is the feature id. + pub feature_mask: [u8; 8], + pub va_reserved: [u32; 4], +} + +impl VaSegmentationStructAV1 { + pub const fn zeroed() -> Self { + VaSegmentationStructAV1 { + segment_info_fields: 0, + feature_data: [[0; 8]; 8], + feature_mask: [0; 8], + va_reserved: [0; 4], + } + } +} + +/// `VAFilmGrainStructAV1`. +/// +/// ⚠ The `ar_coeffs_*` are **signed** here (`int8_t`), where the bitstream — and the +/// vendored parser, and `DXVA_FilmGrain_AV1` — carry the `+128` biased form. +/// libavcodec writes `film_grain->ar_coeffs_y_plus_128[i] - 128`. Copying the biased +/// bytes across unchanged is a silent 128-offset on every autoregressive coefficient. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaFilmGrainStructAV1 { + pub film_grain_info_fields: u32, + pub grain_seed: u16, + pub num_y_points: u8, + pub point_y_value: [u8; 14], + pub point_y_scaling: [u8; 14], + pub num_cb_points: u8, + pub point_cb_value: [u8; 10], + pub point_cb_scaling: [u8; 10], + pub num_cr_points: u8, + pub point_cr_value: [u8; 10], + pub point_cr_scaling: [u8; 10], + pub ar_coeffs_y: [i8; 24], + pub ar_coeffs_cb: [i8; 25], + pub ar_coeffs_cr: [i8; 25], + pub cb_mult: u8, + pub cb_luma_mult: u8, + pub cb_offset: u16, + pub cr_mult: u8, + pub cr_luma_mult: u8, + pub cr_offset: u16, + pub va_reserved: [u32; 4], +} + +impl VaFilmGrainStructAV1 { + pub const fn zeroed() -> Self { + VaFilmGrainStructAV1 { + film_grain_info_fields: 0, + grain_seed: 0, + num_y_points: 0, + point_y_value: [0; 14], + point_y_scaling: [0; 14], + num_cb_points: 0, + point_cb_value: [0; 10], + point_cb_scaling: [0; 10], + num_cr_points: 0, + point_cr_value: [0; 10], + point_cr_scaling: [0; 10], + ar_coeffs_y: [0; 24], + ar_coeffs_cb: [0; 25], + ar_coeffs_cr: [0; 25], + cb_mult: 0, + cb_luma_mult: 0, + cb_offset: 0, + cr_mult: 0, + cr_luma_mult: 0, + cr_offset: 0, + va_reserved: [0; 4], + } + } +} + +/// `VAWarpedMotionParamsAV1` — one reference NAME's global motion. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaWarpedMotionParamsAV1 { + /// `VAAV1TransformationType`. Declared `u32` because a C enum whose enumerators + /// are all non-negative is `unsigned int` on this ABI; either signedness is four + /// bytes and every value written here is 0..=3, so the choice is a naming one. + pub wmtype: u32, + /// `gm_params[ref][0..6]`. ⚠ Only the first SIX are meaningful: AV1 5.9.24 codes + /// six warp parameters and libavcodec copies `for (j = 0; j < 6; j++)`, leaving + /// `wmmat[6]`/`wmmat[7]` zero. + pub wmmat: [i32; 8], + /// The INVERSE of the parser's `warp_valid` (`setup_shear`'s verdict): libva's + /// field says the affine set is unusable. + pub invalid: u8, + pub va_reserved: [u32; 4], +} + +impl VaWarpedMotionParamsAV1 { + pub const fn zeroed() -> Self { + VaWarpedMotionParamsAV1 { + wmtype: VA_AV1_TRANSFORMATION_IDENTITY, + wmmat: [0; 8], + invalid: 0, + va_reserved: [0; 4], + } + } +} + +/// `VADecPictureParameterBufferAV1`. +/// +/// ⚠ **Eight-byte aligned, 1160 bytes**, and the reason is +/// [`Self::anchor_frames_list`]: a pointer member drags the whole structure's +/// alignment up and inserts seven bytes of padding after `anchor_frames_num` that +/// nothing in the field list suggests. Measured, not counted. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaDecPictureParameterBufferAV1 { + /// `seq_profile`: 0, 1 or 2. + pub profile: u8, + /// ⚠ The parser types this `i32` and leaves it **-1** when `enable_order_hint` + /// is 0. Narrowed to a `u8` that would be 255 — a 256-bit order hint — so the + /// conversion sends 0 there instead. + pub order_hint_bits_minus_1: u8, + /// 0 = 8-bit, 1 = 10-bit, 2 = 12-bit. An INDEX, not a depth. + pub bit_depth_idx: u8, + pub matrix_coefficients: u8, + pub seq_info_fields: u32, + /// The decode target's `VASurfaceID`. + pub current_frame: u32, + /// The surface the film-grained picture is written to. libva: *"Valid only when + /// apply_grain equals 1."* This rung refuses `apply_grain` (see + /// [`crate::pic_av1`]), so it always equals [`Self::current_frame`]. + pub current_display_picture: u32, + /// Large-scale tile only; always 0 here. + pub anchor_frames_num: u8, + /// Large-scale tile only; always null here. Declared as a real pointer so the + /// layout follows the target ABI rather than a hard-coded width — which also + /// means the offsets pinned below are the LP64 ones, as everywhere else in this + /// crate. + pub anchor_frames_list: *mut u32, + /// ⚠ The **upscaled** (post-superres) width minus one — libva: *"Picture + /// original resolution. If SuperRes is enabled, this is the upscaled + /// resolution."* libavcodec sends the coded `frame_width_minus_1` syntax + /// element, which is that same quantity: AV1 5.9.8 reads it into + /// `UpscaledWidth` and only then divides down into `FrameWidth`. + pub frame_width_minus1: u16, + pub frame_height_minus1: u16, + /// Large-scale tile only. + pub output_frame_width_in_tiles_minus_1: u16, + pub output_frame_height_in_tiles_minus_1: u16, + /// Indexed by AV1 reference **SLOT**, holding a **`VASurfaceID`** (module docs). + /// `VA_INVALID_ID` for a slot holding nothing — which is the DEFAULT this + /// structure zeroes to, not what a submission carries: `pic_av1` substitutes a live + /// surface for every empty entry before the buffer reaches a driver, because + /// `va_dec_av1.h:352` says the driver will not check the ids and prescribes exactly + /// that recovery. + pub ref_frame_map: [u32; REF_FRAME_MAP_LEN], + /// Indexed by reference **NAME**, holding an index into [`Self::ref_frame_map`] + /// — i.e. an AV1 slot (module docs). + pub ref_frame_idx: [u8; REFS_PER_FRAME], + /// Index into [`Self::ref_frame_idx`], or [`PRIMARY_REF_NONE`]. + pub primary_ref_frame: u8, + /// ⚠ A `u8`, where AV1 allows up to 8 order-hint bits — so the full range fits, + /// but only just. + pub order_hint: u8, + pub seg_info: VaSegmentationStructAV1, + pub film_grain_info: VaFilmGrainStructAV1, + pub tile_cols: u8, + pub tile_rows: u8, + /// Each tile's width in superblocks MINUS ONE — the coded syntax element, not a + /// count. (DXVA's `tiles.widths[]` is the count, `+1`; the two APIs disagree and + /// both are documented.) + pub width_in_sbs_minus_1: [u16; TILE_SBS_LEN], + pub height_in_sbs_minus_1: [u16; TILE_SBS_LEN], + /// Large-scale tile only. + pub tile_count_minus_1: u16, + pub context_update_tile_id: u16, + pub pic_info_fields: u32, + pub superres_scale_denominator: u8, + pub interp_filter: u8, + /// `loop_filter_level[0..2]` — the two LUMA levels. + pub filter_level: [u8; 2], + /// `loop_filter_level[2]`. + pub filter_level_u: u8, + /// `loop_filter_level[3]`. + pub filter_level_v: u8, + /// An **8-bit** union ([`LoopFilterInfoFieldsAV1`]). + pub loop_filter_info_fields: u8, + pub ref_deltas: [i8; TOTAL_REFS_PER_FRAME], + pub mode_deltas: [i8; 2], + pub base_qindex: u8, + pub y_dc_delta_q: i8, + pub u_dc_delta_q: i8, + pub u_ac_delta_q: i8, + pub v_dc_delta_q: i8, + pub v_ac_delta_q: i8, + /// A **16-bit** union ([`QmatrixFieldsAV1`]). + pub qmatrix_fields: u16, + pub mode_control_fields: u32, + pub cdef_damping_minus_3: u8, + pub cdef_bits: u8, + /// `(primary << 2) | (secondary & 3)`, per the header's own formula. The + /// secondary strength must be the CODED two-bit value — see + /// [`pf_bitstream::av1::coded_cdef_sec_strength`]. + pub cdef_y_strengths: [u8; CDEF_MAX], + pub cdef_uv_strengths: [u8; CDEF_MAX], + /// A **16-bit** union ([`LoopRestorationFieldsAV1`]). + pub loop_restoration_fields: u16, + /// Global motion by reference NAME: `wm[0]` is `LAST_FRAME` (module docs). + pub wm: [VaWarpedMotionParamsAV1; REFS_PER_FRAME], + /// `va_reserved[VA_PADDING_MEDIUM]` — eight, where the other two codecs' picture + /// buffers use `VA_PADDING_MEDIUM` and `VA_PADDING_LOW` respectively. + pub va_reserved: [u32; 8], +} + +impl VaDecPictureParameterBufferAV1 { + /// An all-zero buffer with the sentinels a driver must not read as real values: + /// every reference slot empty, and no anchor-frame list. + pub const fn zeroed() -> Self { + VaDecPictureParameterBufferAV1 { + profile: 0, + order_hint_bits_minus_1: 0, + bit_depth_idx: 0, + matrix_coefficients: 0, + seq_info_fields: 0, + current_frame: crate::va::VA_INVALID_SURFACE, + current_display_picture: crate::va::VA_INVALID_SURFACE, + anchor_frames_num: 0, + anchor_frames_list: std::ptr::null_mut(), + frame_width_minus1: 0, + frame_height_minus1: 0, + output_frame_width_in_tiles_minus_1: 0, + output_frame_height_in_tiles_minus_1: 0, + ref_frame_map: [crate::va::VA_INVALID_SURFACE; REF_FRAME_MAP_LEN], + ref_frame_idx: [0; REFS_PER_FRAME], + primary_ref_frame: PRIMARY_REF_NONE, + order_hint: 0, + seg_info: VaSegmentationStructAV1::zeroed(), + film_grain_info: VaFilmGrainStructAV1::zeroed(), + tile_cols: 0, + tile_rows: 0, + width_in_sbs_minus_1: [0; TILE_SBS_LEN], + height_in_sbs_minus_1: [0; TILE_SBS_LEN], + tile_count_minus_1: 0, + context_update_tile_id: 0, + pic_info_fields: 0, + superres_scale_denominator: SUPERRES_NUM, + interp_filter: 0, + filter_level: [0; 2], + filter_level_u: 0, + filter_level_v: 0, + loop_filter_info_fields: 0, + ref_deltas: [0; TOTAL_REFS_PER_FRAME], + mode_deltas: [0; 2], + base_qindex: 0, + y_dc_delta_q: 0, + u_dc_delta_q: 0, + u_ac_delta_q: 0, + v_dc_delta_q: 0, + v_ac_delta_q: 0, + qmatrix_fields: 0, + mode_control_fields: 0, + cdef_damping_minus_3: 0, + cdef_bits: 0, + cdef_y_strengths: [0; CDEF_MAX], + cdef_uv_strengths: [0; CDEF_MAX], + loop_restoration_fields: 0, + wm: [VaWarpedMotionParamsAV1::zeroed(); REFS_PER_FRAME], + va_reserved: [0; 8], + } + } +} + +/// `VASliceParameterBufferAV1` — **one per TILE**, not per tile group (module docs). +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaSliceParameterBufferAV1 { + /// This tile's byte count. + pub slice_data_size: u32, + /// This tile's offset **inside the accompanying `VASliceDataBufferType` buffer** + /// — which holds the whole tile group's `tile_data` region, not just this tile. + pub slice_data_offset: u32, + pub slice_data_flag: u32, + pub tile_row: u16, + pub tile_column: u16, + /// `va_deprecated` in the header — and libavcodec fills both anyway, so this + /// rung does too. A deprecated field a driver may still read is not a field to + /// leave at whatever `zeroed()` chose. + pub tg_start: u16, + pub tg_end: u16, + pub anchor_frame_idx: u8, + pub tile_idx_in_tile_list: u16, + pub va_reserved: [u32; 4], +} + +impl VaSliceParameterBufferAV1 { + pub const fn zeroed() -> Self { + VaSliceParameterBufferAV1 { + slice_data_size: 0, + slice_data_offset: 0, + slice_data_flag: crate::va::VA_SLICE_DATA_FLAG_ALL, + tile_row: 0, + tile_column: 0, + tg_start: 0, + tg_end: 0, + anchor_frame_idx: ANCHOR_FRAME_UNUSED, + tile_idx_in_tile_list: 0, + va_reserved: [0; 4], + } + } +} + +// --------------------------------------------------------------------------- +// Layout proofs — the probe's output, pinned (libva 2.23.0, x86_64-linux-gnu). +// --------------------------------------------------------------------------- + +const _: () = { + use std::mem::align_of; + use std::mem::offset_of; + use std::mem::size_of; + + assert!(size_of::() == 156); + assert!(offset_of!(VaSegmentationStructAV1, segment_info_fields) == 0); + assert!(offset_of!(VaSegmentationStructAV1, feature_data) == 4); + assert!(offset_of!(VaSegmentationStructAV1, feature_mask) == 132); + assert!(offset_of!(VaSegmentationStructAV1, va_reserved) == 140); + + assert!(size_of::() == 176); + assert!(offset_of!(VaFilmGrainStructAV1, film_grain_info_fields) == 0); + assert!(offset_of!(VaFilmGrainStructAV1, grain_seed) == 4); + assert!(offset_of!(VaFilmGrainStructAV1, num_y_points) == 6); + assert!(offset_of!(VaFilmGrainStructAV1, point_y_value) == 7); + assert!(offset_of!(VaFilmGrainStructAV1, point_y_scaling) == 21); + assert!(offset_of!(VaFilmGrainStructAV1, num_cb_points) == 35); + assert!(offset_of!(VaFilmGrainStructAV1, point_cb_value) == 36); + assert!(offset_of!(VaFilmGrainStructAV1, point_cb_scaling) == 46); + assert!(offset_of!(VaFilmGrainStructAV1, num_cr_points) == 56); + assert!(offset_of!(VaFilmGrainStructAV1, point_cr_value) == 57); + assert!(offset_of!(VaFilmGrainStructAV1, point_cr_scaling) == 67); + assert!(offset_of!(VaFilmGrainStructAV1, ar_coeffs_y) == 77); + assert!(offset_of!(VaFilmGrainStructAV1, ar_coeffs_cb) == 101); + assert!(offset_of!(VaFilmGrainStructAV1, ar_coeffs_cr) == 126); + assert!(offset_of!(VaFilmGrainStructAV1, cb_mult) == 151); + assert!(offset_of!(VaFilmGrainStructAV1, cb_luma_mult) == 152); + assert!(offset_of!(VaFilmGrainStructAV1, cb_offset) == 154); + assert!(offset_of!(VaFilmGrainStructAV1, cr_mult) == 156); + assert!(offset_of!(VaFilmGrainStructAV1, cr_luma_mult) == 157); + assert!(offset_of!(VaFilmGrainStructAV1, cr_offset) == 158); + assert!(offset_of!(VaFilmGrainStructAV1, va_reserved) == 160); + + assert!(size_of::() == 56); + assert!(offset_of!(VaWarpedMotionParamsAV1, wmtype) == 0); + assert!(offset_of!(VaWarpedMotionParamsAV1, wmmat) == 4); + assert!(offset_of!(VaWarpedMotionParamsAV1, invalid) == 36); + assert!(offset_of!(VaWarpedMotionParamsAV1, va_reserved) == 40); + + // The pointer member is what makes this one align 8 rather than 4, and it is + // asserted for its own sake: the padding it creates at offsets 17..24 is the + // kind a hand-written declaration silently omits. + assert!(size_of::() == 1160); + assert!(align_of::() == 8); + assert!(offset_of!(VaDecPictureParameterBufferAV1, profile) == 0); + assert!(offset_of!(VaDecPictureParameterBufferAV1, order_hint_bits_minus_1) == 1); + assert!(offset_of!(VaDecPictureParameterBufferAV1, bit_depth_idx) == 2); + assert!(offset_of!(VaDecPictureParameterBufferAV1, matrix_coefficients) == 3); + assert!(offset_of!(VaDecPictureParameterBufferAV1, seq_info_fields) == 4); + assert!(offset_of!(VaDecPictureParameterBufferAV1, current_frame) == 8); + assert!(offset_of!(VaDecPictureParameterBufferAV1, current_display_picture) == 12); + assert!(offset_of!(VaDecPictureParameterBufferAV1, anchor_frames_num) == 16); + assert!(offset_of!(VaDecPictureParameterBufferAV1, anchor_frames_list) == 24); + assert!(offset_of!(VaDecPictureParameterBufferAV1, frame_width_minus1) == 32); + assert!(offset_of!(VaDecPictureParameterBufferAV1, frame_height_minus1) == 34); + assert!( + offset_of!( + VaDecPictureParameterBufferAV1, + output_frame_width_in_tiles_minus_1 + ) == 36 + ); + assert!( + offset_of!( + VaDecPictureParameterBufferAV1, + output_frame_height_in_tiles_minus_1 + ) == 38 + ); + assert!(offset_of!(VaDecPictureParameterBufferAV1, ref_frame_map) == 40); + assert!(offset_of!(VaDecPictureParameterBufferAV1, ref_frame_idx) == 72); + assert!(offset_of!(VaDecPictureParameterBufferAV1, primary_ref_frame) == 79); + assert!(offset_of!(VaDecPictureParameterBufferAV1, order_hint) == 80); + assert!(offset_of!(VaDecPictureParameterBufferAV1, seg_info) == 84); + assert!(offset_of!(VaDecPictureParameterBufferAV1, film_grain_info) == 240); + assert!(offset_of!(VaDecPictureParameterBufferAV1, tile_cols) == 416); + assert!(offset_of!(VaDecPictureParameterBufferAV1, tile_rows) == 417); + assert!(offset_of!(VaDecPictureParameterBufferAV1, width_in_sbs_minus_1) == 418); + assert!(offset_of!(VaDecPictureParameterBufferAV1, height_in_sbs_minus_1) == 544); + assert!(offset_of!(VaDecPictureParameterBufferAV1, tile_count_minus_1) == 670); + assert!(offset_of!(VaDecPictureParameterBufferAV1, context_update_tile_id) == 672); + assert!(offset_of!(VaDecPictureParameterBufferAV1, pic_info_fields) == 676); + assert!(offset_of!(VaDecPictureParameterBufferAV1, superres_scale_denominator) == 680); + assert!(offset_of!(VaDecPictureParameterBufferAV1, interp_filter) == 681); + assert!(offset_of!(VaDecPictureParameterBufferAV1, filter_level) == 682); + assert!(offset_of!(VaDecPictureParameterBufferAV1, filter_level_u) == 684); + assert!(offset_of!(VaDecPictureParameterBufferAV1, filter_level_v) == 685); + assert!(offset_of!(VaDecPictureParameterBufferAV1, loop_filter_info_fields) == 686); + assert!(offset_of!(VaDecPictureParameterBufferAV1, ref_deltas) == 687); + assert!(offset_of!(VaDecPictureParameterBufferAV1, mode_deltas) == 695); + assert!(offset_of!(VaDecPictureParameterBufferAV1, base_qindex) == 697); + assert!(offset_of!(VaDecPictureParameterBufferAV1, y_dc_delta_q) == 698); + assert!(offset_of!(VaDecPictureParameterBufferAV1, u_dc_delta_q) == 699); + assert!(offset_of!(VaDecPictureParameterBufferAV1, u_ac_delta_q) == 700); + assert!(offset_of!(VaDecPictureParameterBufferAV1, v_dc_delta_q) == 701); + assert!(offset_of!(VaDecPictureParameterBufferAV1, v_ac_delta_q) == 702); + assert!(offset_of!(VaDecPictureParameterBufferAV1, qmatrix_fields) == 704); + assert!(offset_of!(VaDecPictureParameterBufferAV1, mode_control_fields) == 708); + assert!(offset_of!(VaDecPictureParameterBufferAV1, cdef_damping_minus_3) == 712); + assert!(offset_of!(VaDecPictureParameterBufferAV1, cdef_bits) == 713); + assert!(offset_of!(VaDecPictureParameterBufferAV1, cdef_y_strengths) == 714); + assert!(offset_of!(VaDecPictureParameterBufferAV1, cdef_uv_strengths) == 722); + assert!(offset_of!(VaDecPictureParameterBufferAV1, loop_restoration_fields) == 730); + assert!(offset_of!(VaDecPictureParameterBufferAV1, wm) == 732); + assert!(offset_of!(VaDecPictureParameterBufferAV1, va_reserved) == 1124); + + assert!(size_of::() == 40); + assert!(offset_of!(VaSliceParameterBufferAV1, slice_data_size) == 0); + assert!(offset_of!(VaSliceParameterBufferAV1, slice_data_offset) == 4); + assert!(offset_of!(VaSliceParameterBufferAV1, slice_data_flag) == 8); + assert!(offset_of!(VaSliceParameterBufferAV1, tile_row) == 12); + assert!(offset_of!(VaSliceParameterBufferAV1, tile_column) == 14); + assert!(offset_of!(VaSliceParameterBufferAV1, tg_start) == 16); + assert!(offset_of!(VaSliceParameterBufferAV1, tg_end) == 18); + assert!(offset_of!(VaSliceParameterBufferAV1, anchor_frame_idx) == 20); + assert!(offset_of!(VaSliceParameterBufferAV1, tile_idx_in_tile_list) == 22); + assert!(offset_of!(VaSliceParameterBufferAV1, va_reserved) == 24); +}; + +#[cfg(test)] +mod tests { + use super::*; + + /// The probe's own single-field vectors, restated. Each of these is a number a + /// real `gcc` printed after setting exactly one bit-field. + #[test] + fn av1_bit_fields_pack_where_the_probe_measured() { + assert_eq!( + SeqInfoFieldsAV1 { + still_picture: true, + ..Default::default() + } + .pack(), + 0x0000_0001 + ); + assert_eq!( + SeqInfoFieldsAV1 { + mono_chrome: true, + ..Default::default() + } + .pack(), + 0x0000_0400 + ); + assert_eq!( + SeqInfoFieldsAV1 { + film_grain_params_present: true, + ..Default::default() + } + .pack(), + 0x0000_8000 + ); + assert_eq!( + PicInfoFieldsAV1 { + frame_type: 3, + ..Default::default() + } + .pack(), + 0x0000_0003 + ); + assert_eq!( + PicInfoFieldsAV1 { + use_ref_frame_mvs: true, + ..Default::default() + } + .pack(), + 0x0000_1000 + ); + assert_eq!( + PicInfoFieldsAV1 { + large_scale_tile: true, + ..Default::default() + } + .pack(), + 0x0001_0000 + ); + assert_eq!( + LoopFilterInfoFieldsAV1 { + sharpness_level: 7, + ..Default::default() + } + .pack(), + 0x07 + ); + assert_eq!( + LoopFilterInfoFieldsAV1 { + mode_ref_delta_update: true, + ..Default::default() + } + .pack(), + 0x10 + ); + assert_eq!( + QmatrixFieldsAV1 { + using_qmatrix: true, + ..Default::default() + } + .pack(), + 0x0001 + ); + assert_eq!( + QmatrixFieldsAV1 { + qm_v: 0xf, + ..Default::default() + } + .pack(), + 0x1e00 + ); + assert_eq!( + ModeControlFieldsAV1 { + delta_q_present_flag: true, + ..Default::default() + } + .pack(), + 0x0000_0001 + ); + assert_eq!( + ModeControlFieldsAV1 { + tx_mode: 3, + ..Default::default() + } + .pack(), + 0x0000_0180 + ); + assert_eq!( + ModeControlFieldsAV1 { + skip_mode_present: true, + ..Default::default() + } + .pack(), + 0x0000_0800 + ); + assert_eq!( + LoopRestorationFieldsAV1 { + yframe_restoration_type: 3, + ..Default::default() + } + .pack(), + 0x0003 + ); + assert_eq!( + LoopRestorationFieldsAV1 { + lr_uv_shift: 1, + ..Default::default() + } + .pack(), + 0x0100 + ); + assert_eq!( + SegmentInfoFieldsAV1 { + enabled: true, + ..Default::default() + } + .pack(), + 0x0000_0001 + ); + assert_eq!( + SegmentInfoFieldsAV1 { + update_data: true, + ..Default::default() + } + .pack(), + 0x0000_0008 + ); + assert_eq!( + FilmGrainInfoFieldsAV1 { + apply_grain: true, + ..Default::default() + } + .pack(), + 0x0000_0001 + ); + assert_eq!( + FilmGrainInfoFieldsAV1 { + grain_scale_shift: 3, + ..Default::default() + } + .pack(), + 0x0000_0300 + ); + assert_eq!( + FilmGrainInfoFieldsAV1 { + clip_to_restricted_range: true, + ..Default::default() + } + .pack(), + 0x0000_0800 + ); + } + + /// Every field alone must light only its own bits, and nothing may reach the + /// reserved tail. Two probe vectors per word would not catch a shift typo that + /// overlapped two neighbours — and on the three NARROW unions, a field that + /// overflowed its declared width would be invisible in a `u32` comparison, which + /// is why each of those is checked against its own type's mask. + #[test] + fn every_av1_field_owns_a_distinct_bit_range() { + // A free function rather than a closure so `seen` can be reset between the + // unions without the closure's borrow outliving it. + fn check(seen: &mut u32, bits: u32, mask: u32) { + assert_ne!(bits, 0, "a field packed to nothing"); + assert_eq!(*seen & bits, 0, "two fields share a bit: {bits:#010x}"); + assert_eq!(bits & !mask, 0, "a field reached the reserved tail"); + *seen |= bits; + } + + let mut seen = 0u32; + const SEQ_MASK: u32 = 0x0000_ffff; + check( + &mut seen, + SeqInfoFieldsAV1 { + chroma_sample_position: 1, + ..Default::default() + } + .pack(), + SEQ_MASK, + ); + for set in [ + |f: &mut SeqInfoFieldsAV1| f.still_picture = true, + |f: &mut SeqInfoFieldsAV1| f.use_128x128_superblock = true, + |f: &mut SeqInfoFieldsAV1| f.enable_filter_intra = true, + |f: &mut SeqInfoFieldsAV1| f.enable_intra_edge_filter = true, + |f: &mut SeqInfoFieldsAV1| f.enable_interintra_compound = true, + |f: &mut SeqInfoFieldsAV1| f.enable_masked_compound = true, + |f: &mut SeqInfoFieldsAV1| f.enable_dual_filter = true, + |f: &mut SeqInfoFieldsAV1| f.enable_order_hint = true, + |f: &mut SeqInfoFieldsAV1| f.enable_jnt_comp = true, + |f: &mut SeqInfoFieldsAV1| f.enable_cdef = true, + |f: &mut SeqInfoFieldsAV1| f.mono_chrome = true, + |f: &mut SeqInfoFieldsAV1| f.color_range = true, + |f: &mut SeqInfoFieldsAV1| f.subsampling_x = true, + |f: &mut SeqInfoFieldsAV1| f.subsampling_y = true, + |f: &mut SeqInfoFieldsAV1| f.film_grain_params_present = true, + ] { + let mut f = SeqInfoFieldsAV1::default(); + set(&mut f); + check(&mut seen, f.pack(), SEQ_MASK); + } + assert_eq!(seen, SEQ_MASK, "all 16 seq_info bits accounted for"); + + seen = 0; + const PIC_MASK: u32 = 0x0001_ffff; + check( + &mut seen, + PicInfoFieldsAV1 { + frame_type: 3, + ..Default::default() + } + .pack(), + PIC_MASK, + ); + for set in [ + |f: &mut PicInfoFieldsAV1| f.show_frame = true, + |f: &mut PicInfoFieldsAV1| f.showable_frame = true, + |f: &mut PicInfoFieldsAV1| f.error_resilient_mode = true, + |f: &mut PicInfoFieldsAV1| f.disable_cdf_update = true, + |f: &mut PicInfoFieldsAV1| f.allow_screen_content_tools = true, + |f: &mut PicInfoFieldsAV1| f.force_integer_mv = true, + |f: &mut PicInfoFieldsAV1| f.allow_intrabc = true, + |f: &mut PicInfoFieldsAV1| f.use_superres = true, + |f: &mut PicInfoFieldsAV1| f.allow_high_precision_mv = true, + |f: &mut PicInfoFieldsAV1| f.is_motion_mode_switchable = true, + |f: &mut PicInfoFieldsAV1| f.use_ref_frame_mvs = true, + |f: &mut PicInfoFieldsAV1| f.disable_frame_end_update_cdf = true, + |f: &mut PicInfoFieldsAV1| f.uniform_tile_spacing_flag = true, + |f: &mut PicInfoFieldsAV1| f.allow_warped_motion = true, + |f: &mut PicInfoFieldsAV1| f.large_scale_tile = true, + ] { + let mut f = PicInfoFieldsAV1::default(); + set(&mut f); + check(&mut seen, f.pack(), PIC_MASK); + } + assert_eq!(seen, PIC_MASK, "all 17 pic_info bits accounted for"); + + seen = 0; + const MODE_MASK: u32 = 0x0000_0fff; + for set in [ + |f: &mut ModeControlFieldsAV1| f.delta_q_present_flag = true, + |f: &mut ModeControlFieldsAV1| f.log2_delta_q_res = 3, + |f: &mut ModeControlFieldsAV1| f.delta_lf_present_flag = true, + |f: &mut ModeControlFieldsAV1| f.log2_delta_lf_res = 3, + |f: &mut ModeControlFieldsAV1| f.delta_lf_multi = true, + |f: &mut ModeControlFieldsAV1| f.tx_mode = 3, + |f: &mut ModeControlFieldsAV1| f.reference_select = true, + |f: &mut ModeControlFieldsAV1| f.reduced_tx_set_used = true, + |f: &mut ModeControlFieldsAV1| f.skip_mode_present = true, + ] { + let mut f = ModeControlFieldsAV1::default(); + set(&mut f); + check(&mut seen, f.pack(), MODE_MASK); + } + assert_eq!(seen, MODE_MASK); + + seen = 0; + const FG_MASK: u32 = 0x0000_0fff; + for set in [ + |f: &mut FilmGrainInfoFieldsAV1| f.apply_grain = true, + |f: &mut FilmGrainInfoFieldsAV1| f.chroma_scaling_from_luma = true, + |f: &mut FilmGrainInfoFieldsAV1| f.grain_scaling_minus_8 = 3, + |f: &mut FilmGrainInfoFieldsAV1| f.ar_coeff_lag = 3, + |f: &mut FilmGrainInfoFieldsAV1| f.ar_coeff_shift_minus_6 = 3, + |f: &mut FilmGrainInfoFieldsAV1| f.grain_scale_shift = 3, + |f: &mut FilmGrainInfoFieldsAV1| f.overlap_flag = true, + |f: &mut FilmGrainInfoFieldsAV1| f.clip_to_restricted_range = true, + ] { + let mut f = FilmGrainInfoFieldsAV1::default(); + set(&mut f); + check(&mut seen, f.pack(), FG_MASK); + } + assert_eq!(seen, FG_MASK); + + seen = 0; + for set in [ + |f: &mut SegmentInfoFieldsAV1| f.enabled = true, + |f: &mut SegmentInfoFieldsAV1| f.update_map = true, + |f: &mut SegmentInfoFieldsAV1| f.temporal_update = true, + |f: &mut SegmentInfoFieldsAV1| f.update_data = true, + ] { + let mut f = SegmentInfoFieldsAV1::default(); + set(&mut f); + check(&mut seen, f.pack(), 0x0000_000f); + } + assert_eq!(seen, 0x0000_000f); + + // The three NARROW unions, checked in their own widths. + let mut seen8 = 0u8; + for bits in [ + LoopFilterInfoFieldsAV1 { + sharpness_level: 7, + ..Default::default() + } + .pack(), + LoopFilterInfoFieldsAV1 { + mode_ref_delta_enabled: true, + ..Default::default() + } + .pack(), + LoopFilterInfoFieldsAV1 { + mode_ref_delta_update: true, + ..Default::default() + } + .pack(), + ] { + assert_ne!(bits, 0); + assert_eq!(seen8 & bits, 0); + seen8 |= bits; + } + assert_eq!(seen8, 0x1f, "five bits, and nothing in the reserved three"); + + let mut seen16 = 0u16; + for bits in [ + QmatrixFieldsAV1 { + using_qmatrix: true, + ..Default::default() + } + .pack(), + QmatrixFieldsAV1 { + qm_y: 0xf, + ..Default::default() + } + .pack(), + QmatrixFieldsAV1 { + qm_u: 0xf, + ..Default::default() + } + .pack(), + QmatrixFieldsAV1 { + qm_v: 0xf, + ..Default::default() + } + .pack(), + ] { + assert_ne!(bits, 0); + assert_eq!(seen16 & bits, 0); + seen16 |= bits; + } + assert_eq!(seen16, 0x1fff, "13 bits, and nothing in the reserved three"); + + seen16 = 0; + for bits in [ + LoopRestorationFieldsAV1 { + yframe_restoration_type: 3, + ..Default::default() + } + .pack(), + LoopRestorationFieldsAV1 { + cbframe_restoration_type: 3, + ..Default::default() + } + .pack(), + LoopRestorationFieldsAV1 { + crframe_restoration_type: 3, + ..Default::default() + } + .pack(), + LoopRestorationFieldsAV1 { + lr_unit_shift: 3, + ..Default::default() + } + .pack(), + LoopRestorationFieldsAV1 { + lr_uv_shift: 1, + ..Default::default() + } + .pack(), + ] { + assert_ne!(bits, 0); + assert_eq!(seen16 & bits, 0); + seen16 |= bits; + } + assert_eq!( + seen16, 0x01ff, + "nine bits, and nothing in the reserved seven" + ); + } + + /// The sentinels a zeroed picture buffer must NOT leave as plausible values. + /// + /// `ref_frame_map` is the one that matters: a zero there is a perfectly valid + /// `VASurfaceID`, and `va_dec_av1.h` says outright that the *"Driver is not + /// responsible to validate reference frames' id"* — so an unfilled slot has to + /// carry `VA_INVALID_ID` or the driver predicts from surface 0. + #[test] + fn a_zeroed_picture_buffer_carries_the_sentinels_not_zeros() { + let p = VaDecPictureParameterBufferAV1::zeroed(); + assert!(p + .ref_frame_map + .iter() + .all(|&s| s == crate::va::VA_INVALID_SURFACE)); + assert_eq!(p.current_frame, crate::va::VA_INVALID_SURFACE); + assert_eq!(p.current_display_picture, crate::va::VA_INVALID_SURFACE); + assert_eq!(p.primary_ref_frame, PRIMARY_REF_NONE); + assert_eq!( + p.superres_scale_denominator, SUPERRES_NUM, + "a frame without superres sends 8, never 0 — libva documents 8 or 9..=16" + ); + assert!(p.anchor_frames_list.is_null()); + let t = VaSliceParameterBufferAV1::zeroed(); + assert_eq!(t.slice_data_flag, crate::va::VA_SLICE_DATA_FLAG_ALL); + } + + /// Every enumerator [`crate::pic_av1`] CASTS rather than remaps, pinned against + /// libva's own documented numbering. + /// + /// Four families reach a driver as a bare `as u8` / `as u32` of a vendored-parser + /// enum. Each cast is only correct because the parser's discriminants happen to be + /// the spec's, and "happen to be" is what a test is for: a vendored-parser bump + /// that renumbered any of them would decode to something plausible rather than + /// failing. + /// + /// The right-hand sides are transcribed from `va_dec_av1.h` and from libavcodec's + /// `av1.h`, not from the parser — comparing the parser against itself would pass + /// forever. + #[test] + fn every_enumerator_this_rung_casts_matches_the_numbering_libva_documents() { + use cros_codecs::codec::av1::parser::FrameRestorationType; + use cros_codecs::codec::av1::parser::FrameType; + use cros_codecs::codec::av1::parser::InterpolationFilter; + use cros_codecs::codec::av1::parser::TxMode; + use cros_codecs::codec::av1::parser::WarpModelType; + + // `VAAV1TransformationType` (measured off the header). + assert_eq!( + WarpModelType::Identity as u32, + VA_AV1_TRANSFORMATION_IDENTITY + ); + assert_eq!( + WarpModelType::Translation as u32, + VA_AV1_TRANSFORMATION_TRANSLATION + ); + assert_eq!(WarpModelType::RotZoom as u32, VA_AV1_TRANSFORMATION_ROTZOOM); + assert_eq!(WarpModelType::Affine as u32, VA_AV1_TRANSFORMATION_AFFINE); + + // `pic_info_fields.frame_type`, which the header documents inline: + // "0: KEY_FRAME; 1: INTER_FRAME; 2: INTRA_ONLY_FRAME; 3: SWITCH_FRAME". + assert_eq!(FrameType::KeyFrame as u8, 0); + assert_eq!(FrameType::InterFrame as u8, 1); + assert_eq!(FrameType::IntraOnlyFrame as u8, 2); + assert_eq!(FrameType::SwitchFrame as u8, 3); + + // `mode_control_fields.tx_mode` — "read_tx_mode, value range [0..2]", i.e. + // ONLY_4X4 / TX_MODE_LARGEST / TX_MODE_SELECT. + assert_eq!(TxMode::Only4x4 as u8, 0); + assert_eq!(TxMode::Largest as u8, 1); + assert_eq!(TxMode::Select as u8, 2); + + // `interp_filter` — "value range [0..4]", AV1 6.8.9's + // EIGHTTAP / EIGHTTAP_SMOOTH / EIGHTTAP_SHARP / BILINEAR / SWITCHABLE. + assert_eq!(InterpolationFilter::EightTap as u8, 0); + assert_eq!(InterpolationFilter::EightTapSmooth as u8, 1); + assert_eq!(InterpolationFilter::EightTapSharp as u8, 2); + assert_eq!(InterpolationFilter::Bilinear as u8, 3); + assert_eq!(InterpolationFilter::Switchable as u8, 4); + + // `loop_restoration_fields.*frame_restoration_type` — libavcodec's + // `AV1_RESTORE_NONE/WIENER/SGRPROJ/SWITCHABLE` = 0/1/2/3, which is what its + // `remap_lr_type[] = {NONE, SWITCHABLE, WIENER, SGRPROJ}` PRODUCES from the + // coded two-bit `lr_type`. The vendored parser applies the same + // `REMAP_LR_TYPE` as it reads, so [`crate::pic_av1`] casts and remaps + // nothing; if these four numbers moved, it would have to. + assert_eq!(FrameRestorationType::None as u8, 0); + assert_eq!(FrameRestorationType::Wiener as u8, 1); + assert_eq!(FrameRestorationType::Sgrproj as u8, 2); + assert_eq!(FrameRestorationType::Switchable as u8, 3); + } +} diff --git a/crates/pf-vaadec/src/va_h265.rs b/crates/pf-vaadec/src/va_h265.rs new file mode 100644 index 00000000..878e0454 --- /dev/null +++ b/crates/pf-vaadec/src/va_h265.rs @@ -0,0 +1,542 @@ +//! The libva decode buffer layouts for H.265, hand-declared — the HEVC twin of +//! [`crate::va`], measured the same way and pinned the same way. +//! +//! Sizes and offsets come from the committed `layout-probe.c` run against libva +//! **2.23.0** headers: `VAPictureHEVC` **28**, `VAPictureParameterBufferHEVC` **604**, +//! `VASliceParameterBufferHEVC` **264**, `VAIQMatrixBufferHEVC` **1016**. Every bit +//! position below was read back off a real header too, not counted by eye. +//! +//! # HEVC's reference plumbing is a THIRD convention +//! +//! This program has now met three different ways of saying which pictures a short-term +//! reference set contains, and they are not interchangeable: +//! +//! * **Vulkan** takes DPB *slot* indices in `RefPicSetStCurrBefore/After/LtCurr` — +//! writing reference-list positions there is what made HEVC unplayable on every +//! driver until it was root-caused. +//! * **DXVA** takes positions into `RefPicList[]` in identically named arrays. +//! * **VAAPI** takes neither: it marks membership as **flags on the DPB entries +//! themselves** (`VA_PICTURE_HEVC_RPS_ST_CURR_BEFORE` / `_AFTER` / `_LT_CURR`), and +//! the per-slice `RefPicList[2][15]` holds **indices into `ReferenceFrames`** rather +//! than pictures. +//! +//! Three spellings of one idea, one of which has already cost this project a +//! shipped defect — so the conversion states which it is writing, every time. +//! +//! # And the offset is a BYTE offset +//! +//! H.264's `slice_data_bit_offset` counts bits; HEVC's `slice_data_byte_offset` counts +//! bytes, over the same definition (from and including the NAL header byte, with +//! emulation-prevention bytes removed). `slice_data()` is byte-aligned by +//! `byte_alignment()`, so the parser's `header_bit_size / 8` is exact rather than +//! rounded — and the conversion asserts that rather than assuming it. + +/// Flags for [`VaPictureHEVC::flags`]. +pub const VA_PICTURE_HEVC_INVALID: u32 = 0x0000_0001; +pub const VA_PICTURE_HEVC_FIELD_PIC: u32 = 0x0000_0002; +pub const VA_PICTURE_HEVC_BOTTOM_FIELD: u32 = 0x0000_0004; +pub const VA_PICTURE_HEVC_LONG_TERM_REFERENCE: u32 = 0x0000_0008; +pub const VA_PICTURE_HEVC_RPS_ST_CURR_BEFORE: u32 = 0x0000_0010; +pub const VA_PICTURE_HEVC_RPS_ST_CURR_AFTER: u32 = 0x0000_0020; +pub const VA_PICTURE_HEVC_RPS_LT_CURR: u32 = 0x0000_0040; + +/// `ReferenceFrames` length — 15, not 16 as in H.264. +pub const REFERENCE_FRAMES_LEN_H265: usize = 15; + +/// `RefPicList[2][15]`'s inner length, and the value an unused entry carries +/// (`0xff`, libva's "no entry" for an index into `ReferenceFrames`). +pub const REF_PIC_LIST_LEN_H265: usize = 15; +pub const REF_PIC_LIST_UNUSED: u8 = 0xff; + +/// `VAPictureHEVC` — one DPB entry, or the current picture. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaPictureHEVC { + pub picture_id: u32, + pub pic_order_cnt: i32, + /// Long-term marking AND the picture's RPS membership, ORed together. + pub flags: u32, + pub va_reserved: [u32; 4], +} + +impl VaPictureHEVC { + pub const fn invalid() -> Self { + VaPictureHEVC { + picture_id: crate::va::VA_INVALID_SURFACE, + pic_order_cnt: 0, + flags: VA_PICTURE_HEVC_INVALID, + va_reserved: [0; 4], + } + } +} + +/// `VAPictureParameterBufferHEVC::pic_fields`, unpacked. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct PicFieldsH265 { + pub chroma_format_idc: u8, + pub separate_colour_plane_flag: bool, + pub pcm_enabled_flag: bool, + pub scaling_list_enabled_flag: bool, + pub transform_skip_enabled_flag: bool, + pub amp_enabled_flag: bool, + pub strong_intra_smoothing_enabled_flag: bool, + pub sign_data_hiding_enabled_flag: bool, + pub constrained_intra_pred_flag: bool, + pub cu_qp_delta_enabled_flag: bool, + pub weighted_pred_flag: bool, + pub weighted_bipred_flag: bool, + pub transquant_bypass_enabled_flag: bool, + pub tiles_enabled_flag: bool, + pub entropy_coding_sync_enabled_flag: bool, + pub pps_loop_filter_across_slices_enabled_flag: bool, + pub loop_filter_across_tiles_enabled_flag: bool, + pub pcm_loop_filter_disabled_flag: bool, + /// Derived, not a syntax element: the stream never reorders. + pub no_pic_reordering_flag: bool, + /// Derived: no picture uses bi-prediction. + pub no_bi_pred_flag: bool, +} + +impl PicFieldsH265 { + pub const fn pack(self) -> u32 { + (self.chroma_format_idc as u32 & 0x3) + | ((self.separate_colour_plane_flag as u32) << 2) + | ((self.pcm_enabled_flag as u32) << 3) + | ((self.scaling_list_enabled_flag as u32) << 4) + | ((self.transform_skip_enabled_flag as u32) << 5) + | ((self.amp_enabled_flag as u32) << 6) + | ((self.strong_intra_smoothing_enabled_flag as u32) << 7) + | ((self.sign_data_hiding_enabled_flag as u32) << 8) + | ((self.constrained_intra_pred_flag as u32) << 9) + | ((self.cu_qp_delta_enabled_flag as u32) << 10) + | ((self.weighted_pred_flag as u32) << 11) + | ((self.weighted_bipred_flag as u32) << 12) + | ((self.transquant_bypass_enabled_flag as u32) << 13) + | ((self.tiles_enabled_flag as u32) << 14) + | ((self.entropy_coding_sync_enabled_flag as u32) << 15) + | ((self.pps_loop_filter_across_slices_enabled_flag as u32) << 16) + | ((self.loop_filter_across_tiles_enabled_flag as u32) << 17) + | ((self.pcm_loop_filter_disabled_flag as u32) << 18) + | ((self.no_pic_reordering_flag as u32) << 19) + | ((self.no_bi_pred_flag as u32) << 20) + } +} + +/// `VAPictureParameterBufferHEVC::slice_parsing_fields`, unpacked. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct SliceParsingFieldsH265 { + pub lists_modification_present_flag: bool, + pub long_term_ref_pics_present_flag: bool, + pub sps_temporal_mvp_enabled_flag: bool, + pub cabac_init_present_flag: bool, + pub output_flag_present_flag: bool, + pub dependent_slice_segments_enabled_flag: bool, + pub pps_slice_chroma_qp_offsets_present_flag: bool, + pub sample_adaptive_offset_enabled_flag: bool, + pub deblocking_filter_override_enabled_flag: bool, + pub pps_disable_deblocking_filter_flag: bool, + pub slice_segment_header_extension_present_flag: bool, + pub rap_pic_flag: bool, + pub idr_pic_flag: bool, + pub intra_pic_flag: bool, +} + +impl SliceParsingFieldsH265 { + pub const fn pack(self) -> u32 { + (self.lists_modification_present_flag as u32) + | ((self.long_term_ref_pics_present_flag as u32) << 1) + | ((self.sps_temporal_mvp_enabled_flag as u32) << 2) + | ((self.cabac_init_present_flag as u32) << 3) + | ((self.output_flag_present_flag as u32) << 4) + | ((self.dependent_slice_segments_enabled_flag as u32) << 5) + | ((self.pps_slice_chroma_qp_offsets_present_flag as u32) << 6) + | ((self.sample_adaptive_offset_enabled_flag as u32) << 7) + | ((self.deblocking_filter_override_enabled_flag as u32) << 8) + | ((self.pps_disable_deblocking_filter_flag as u32) << 9) + | ((self.slice_segment_header_extension_present_flag as u32) << 10) + | ((self.rap_pic_flag as u32) << 11) + | ((self.idr_pic_flag as u32) << 12) + | ((self.intra_pic_flag as u32) << 13) + } +} + +/// `VASliceParameterBufferHEVC::LongSliceFlags`, unpacked. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct LongSliceFlagsH265 { + pub last_slice_of_pic: bool, + pub dependent_slice_segment_flag: bool, + /// 0 = B, 1 = P, 2 = I (H.265's own numbering, not H.264's). + pub slice_type: u8, + pub color_plane_id: u8, + pub slice_sao_luma_flag: bool, + pub slice_sao_chroma_flag: bool, + pub mvd_l1_zero_flag: bool, + pub cabac_init_flag: bool, + pub slice_temporal_mvp_enabled_flag: bool, + pub slice_deblocking_filter_disabled_flag: bool, + pub collocated_from_l0_flag: bool, + pub slice_loop_filter_across_slices_enabled_flag: bool, +} + +impl LongSliceFlagsH265 { + pub const fn pack(self) -> u32 { + (self.last_slice_of_pic as u32) + | ((self.dependent_slice_segment_flag as u32) << 1) + | ((self.slice_type as u32 & 0x3) << 2) + | ((self.color_plane_id as u32 & 0x3) << 4) + | ((self.slice_sao_luma_flag as u32) << 6) + | ((self.slice_sao_chroma_flag as u32) << 7) + | ((self.mvd_l1_zero_flag as u32) << 8) + | ((self.cabac_init_flag as u32) << 9) + | ((self.slice_temporal_mvp_enabled_flag as u32) << 10) + | ((self.slice_deblocking_filter_disabled_flag as u32) << 11) + | ((self.collocated_from_l0_flag as u32) << 12) + | ((self.slice_loop_filter_across_slices_enabled_flag as u32) << 13) + } +} + +/// `VAPictureParameterBufferHEVC`. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaPictureParameterBufferHEVC { + pub curr_pic: VaPictureHEVC, + /// The DPB — 15 entries, each carrying its own RPS membership flags. + pub reference_frames: [VaPictureHEVC; REFERENCE_FRAMES_LEN_H265], + /// LUMA SAMPLES, not macroblocks: HEVC states the picture size directly. + pub pic_width_in_luma_samples: u16, + pub pic_height_in_luma_samples: u16, + pub pic_fields: u32, + pub sps_max_dec_pic_buffering_minus1: u8, + pub bit_depth_luma_minus8: u8, + pub bit_depth_chroma_minus8: u8, + pub pcm_sample_bit_depth_luma_minus1: u8, + pub pcm_sample_bit_depth_chroma_minus1: u8, + pub log2_min_luma_coding_block_size_minus3: u8, + pub log2_diff_max_min_luma_coding_block_size: u8, + pub log2_min_transform_block_size_minus2: u8, + pub log2_diff_max_min_transform_block_size: u8, + pub log2_min_pcm_luma_coding_block_size_minus3: u8, + pub log2_diff_max_min_pcm_luma_coding_block_size: u8, + pub max_transform_hierarchy_depth_intra: u8, + pub max_transform_hierarchy_depth_inter: u8, + pub init_qp_minus26: i8, + pub diff_cu_qp_delta_depth: u8, + pub pps_cb_qp_offset: i8, + pub pps_cr_qp_offset: i8, + pub log2_parallel_merge_level_minus2: u8, + pub num_tile_columns_minus1: u8, + pub num_tile_rows_minus1: u8, + pub column_width_minus1: [u16; 19], + pub row_height_minus1: [u16; 21], + pub slice_parsing_fields: u32, + pub log2_max_pic_order_cnt_lsb_minus4: u8, + pub num_short_term_ref_pic_sets: u8, + pub num_long_term_ref_pic_sps: u8, + pub num_ref_idx_l0_default_active_minus1: u8, + pub num_ref_idx_l1_default_active_minus1: u8, + pub pps_beta_offset_div2: i8, + pub pps_tc_offset_div2: i8, + pub num_extra_slice_header_bits: u8, + /// Bit length of the short-term RPS coded in THIS slice header, or 0 when the + /// slice referenced an SPS set instead. + pub st_rps_bits: u32, + pub va_reserved: [u32; 8], +} + +/// `VAIQMatrixBufferHEVC` — four list sizes plus the two DC tables. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaIqMatrixBufferHEVC { + pub scaling_list4x4: [[u8; 16]; 6], + pub scaling_list8x8: [[u8; 64]; 6], + pub scaling_list16x16: [[u8; 64]; 6], + pub scaling_list32x32: [[u8; 64]; 2], + pub scaling_list_dc16x16: [u8; 6], + pub scaling_list_dc32x32: [u8; 2], + pub va_reserved: [u32; 4], +} + +/// `VASliceParameterBufferHEVC`. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaSliceParameterBufferHEVC { + pub slice_data_size: u32, + pub slice_data_offset: u32, + pub slice_data_flag: u32, + /// BYTES from and including the NAL header byte to `slice_data()`, with + /// emulation-prevention bytes removed (module docs). + pub slice_data_byte_offset: u32, + pub slice_segment_address: u32, + /// Indices into [`VaPictureParameterBufferHEVC::reference_frames`], NOT pictures + /// and NOT surfaces. `0xff` marks an unused entry. + pub ref_pic_list: [[u8; REF_PIC_LIST_LEN_H265]; 2], + pub long_slice_flags: u32, + pub collocated_ref_idx: u8, + pub num_ref_idx_l0_active_minus1: u8, + pub num_ref_idx_l1_active_minus1: u8, + pub slice_qp_delta: i8, + pub slice_cb_qp_offset: i8, + pub slice_cr_qp_offset: i8, + pub slice_beta_offset_div2: i8, + pub slice_tc_offset_div2: i8, + pub luma_log2_weight_denom: u8, + pub delta_chroma_log2_weight_denom: i8, + pub delta_luma_weight_l0: [i8; 15], + pub luma_offset_l0: [i8; 15], + pub delta_chroma_weight_l0: [[i8; 2]; 15], + pub chroma_offset_l0: [[i8; 2]; 15], + pub delta_luma_weight_l1: [i8; 15], + pub luma_offset_l1: [i8; 15], + pub delta_chroma_weight_l1: [[i8; 2]; 15], + pub chroma_offset_l1: [[i8; 2]; 15], + pub five_minus_max_num_merge_cand: u8, + pub num_entry_point_offsets: u16, + pub entry_offset_to_subset_array: u16, + pub slice_data_num_emu_prevn_bytes: u16, + /// `va_reserved[VA_PADDING_LOW - 2]`. + pub va_reserved: [u32; 2], +} + +impl VaSliceParameterBufferHEVC { + /// An all-zero record with both reference lists marked unused. + pub const fn zeroed() -> Self { + VaSliceParameterBufferHEVC { + slice_data_size: 0, + slice_data_offset: 0, + slice_data_flag: crate::va::VA_SLICE_DATA_FLAG_ALL, + slice_data_byte_offset: 0, + slice_segment_address: 0, + ref_pic_list: [[REF_PIC_LIST_UNUSED; REF_PIC_LIST_LEN_H265]; 2], + long_slice_flags: 0, + collocated_ref_idx: 0, + num_ref_idx_l0_active_minus1: 0, + num_ref_idx_l1_active_minus1: 0, + slice_qp_delta: 0, + slice_cb_qp_offset: 0, + slice_cr_qp_offset: 0, + slice_beta_offset_div2: 0, + slice_tc_offset_div2: 0, + luma_log2_weight_denom: 0, + delta_chroma_log2_weight_denom: 0, + delta_luma_weight_l0: [0; 15], + luma_offset_l0: [0; 15], + delta_chroma_weight_l0: [[0; 2]; 15], + chroma_offset_l0: [[0; 2]; 15], + delta_luma_weight_l1: [0; 15], + luma_offset_l1: [0; 15], + delta_chroma_weight_l1: [[0; 2]; 15], + chroma_offset_l1: [[0; 2]; 15], + five_minus_max_num_merge_cand: 0, + num_entry_point_offsets: 0, + entry_offset_to_subset_array: 0, + slice_data_num_emu_prevn_bytes: 0, + va_reserved: [0; 2], + } + } +} + +// --------------------------------------------------------------------------- +// Layout proofs — the probe's output, pinned (libva 2.23.0, x86_64-linux-gnu). +// --------------------------------------------------------------------------- + +const _: () = { + use std::mem::offset_of; + use std::mem::size_of; + + assert!(size_of::() == 28); + assert!(offset_of!(VaPictureHEVC, picture_id) == 0); + assert!(offset_of!(VaPictureHEVC, pic_order_cnt) == 4); + assert!(offset_of!(VaPictureHEVC, flags) == 8); + assert!(offset_of!(VaPictureHEVC, va_reserved) == 12); + + assert!(size_of::() == 604); + assert!(offset_of!(VaPictureParameterBufferHEVC, curr_pic) == 0); + assert!(offset_of!(VaPictureParameterBufferHEVC, reference_frames) == 28); + assert!(offset_of!(VaPictureParameterBufferHEVC, pic_width_in_luma_samples) == 448); + assert!(offset_of!(VaPictureParameterBufferHEVC, pic_height_in_luma_samples) == 450); + assert!(offset_of!(VaPictureParameterBufferHEVC, pic_fields) == 452); + assert!( + offset_of!( + VaPictureParameterBufferHEVC, + sps_max_dec_pic_buffering_minus1 + ) == 456 + ); + assert!(offset_of!(VaPictureParameterBufferHEVC, init_qp_minus26) == 469); + assert!(offset_of!(VaPictureParameterBufferHEVC, num_tile_columns_minus1) == 474); + assert!(offset_of!(VaPictureParameterBufferHEVC, column_width_minus1) == 476); + assert!(offset_of!(VaPictureParameterBufferHEVC, row_height_minus1) == 514); + assert!(offset_of!(VaPictureParameterBufferHEVC, slice_parsing_fields) == 556); + assert!( + offset_of!( + VaPictureParameterBufferHEVC, + log2_max_pic_order_cnt_lsb_minus4 + ) == 560 + ); + assert!(offset_of!(VaPictureParameterBufferHEVC, num_extra_slice_header_bits) == 567); + assert!(offset_of!(VaPictureParameterBufferHEVC, st_rps_bits) == 568); + assert!(offset_of!(VaPictureParameterBufferHEVC, va_reserved) == 572); + + assert!(size_of::() == 1016); + assert!(offset_of!(VaIqMatrixBufferHEVC, scaling_list4x4) == 0); + assert!(offset_of!(VaIqMatrixBufferHEVC, scaling_list8x8) == 96); + assert!(offset_of!(VaIqMatrixBufferHEVC, scaling_list16x16) == 480); + assert!(offset_of!(VaIqMatrixBufferHEVC, scaling_list32x32) == 864); + assert!(offset_of!(VaIqMatrixBufferHEVC, scaling_list_dc16x16) == 992); + assert!(offset_of!(VaIqMatrixBufferHEVC, scaling_list_dc32x32) == 998); + assert!(offset_of!(VaIqMatrixBufferHEVC, va_reserved) == 1000); + + assert!(size_of::() == 264); + assert!(offset_of!(VaSliceParameterBufferHEVC, slice_data_size) == 0); + assert!(offset_of!(VaSliceParameterBufferHEVC, slice_data_byte_offset) == 12); + assert!(offset_of!(VaSliceParameterBufferHEVC, slice_segment_address) == 16); + assert!(offset_of!(VaSliceParameterBufferHEVC, ref_pic_list) == 20); + assert!(offset_of!(VaSliceParameterBufferHEVC, long_slice_flags) == 52); + assert!(offset_of!(VaSliceParameterBufferHEVC, collocated_ref_idx) == 56); + assert!(offset_of!(VaSliceParameterBufferHEVC, luma_log2_weight_denom) == 64); + assert!(offset_of!(VaSliceParameterBufferHEVC, delta_luma_weight_l0) == 66); + assert!(offset_of!(VaSliceParameterBufferHEVC, luma_offset_l0) == 81); + assert!(offset_of!(VaSliceParameterBufferHEVC, delta_chroma_weight_l0) == 96); + assert!(offset_of!(VaSliceParameterBufferHEVC, chroma_offset_l0) == 126); + assert!(offset_of!(VaSliceParameterBufferHEVC, delta_luma_weight_l1) == 156); + assert!(offset_of!(VaSliceParameterBufferHEVC, chroma_offset_l1) == 216); + assert!(offset_of!(VaSliceParameterBufferHEVC, five_minus_max_num_merge_cand) == 246); + assert!(offset_of!(VaSliceParameterBufferHEVC, num_entry_point_offsets) == 248); + assert!(offset_of!(VaSliceParameterBufferHEVC, slice_data_num_emu_prevn_bytes) == 252); + assert!(offset_of!(VaSliceParameterBufferHEVC, va_reserved) == 256); +}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hevc_bit_fields_pack_where_the_probe_measured() { + assert_eq!( + PicFieldsH265 { + chroma_format_idc: 3, + ..Default::default() + } + .pack(), + 0x0000_0003 + ); + assert_eq!( + PicFieldsH265 { + no_bi_pred_flag: true, + ..Default::default() + } + .pack(), + 0x0010_0000 + ); + assert_eq!( + SliceParsingFieldsH265 { + intra_pic_flag: true, + ..Default::default() + } + .pack(), + 0x0000_2000 + ); + assert_eq!( + LongSliceFlagsH265 { + slice_type: 3, + ..Default::default() + } + .pack(), + 0x0000_000c + ); + assert_eq!( + LongSliceFlagsH265 { + slice_loop_filter_across_slices_enabled_flag: true, + ..Default::default() + } + .pack(), + 0x0000_2000 + ); + } + + /// Each field alone must light only its own bits — two probe vectors per word + /// would not catch a shift typo that overlapped two neighbours. + #[test] + fn every_hevc_pic_field_owns_a_distinct_bit_range() { + let mut seen = 0u32; + let mut check = |bits: u32| { + assert_ne!(bits, 0, "a field packed to nothing"); + assert_eq!(seen & bits, 0, "two fields share a bit: {bits:#010x}"); + seen |= bits; + }; + check( + PicFieldsH265 { + chroma_format_idc: 3, + ..Default::default() + } + .pack(), + ); + for set in [ + |f: &mut PicFieldsH265| f.separate_colour_plane_flag = true, + |f: &mut PicFieldsH265| f.pcm_enabled_flag = true, + |f: &mut PicFieldsH265| f.scaling_list_enabled_flag = true, + |f: &mut PicFieldsH265| f.transform_skip_enabled_flag = true, + |f: &mut PicFieldsH265| f.amp_enabled_flag = true, + |f: &mut PicFieldsH265| f.strong_intra_smoothing_enabled_flag = true, + |f: &mut PicFieldsH265| f.sign_data_hiding_enabled_flag = true, + |f: &mut PicFieldsH265| f.constrained_intra_pred_flag = true, + |f: &mut PicFieldsH265| f.cu_qp_delta_enabled_flag = true, + |f: &mut PicFieldsH265| f.weighted_pred_flag = true, + |f: &mut PicFieldsH265| f.weighted_bipred_flag = true, + |f: &mut PicFieldsH265| f.transquant_bypass_enabled_flag = true, + |f: &mut PicFieldsH265| f.tiles_enabled_flag = true, + |f: &mut PicFieldsH265| f.entropy_coding_sync_enabled_flag = true, + |f: &mut PicFieldsH265| f.pps_loop_filter_across_slices_enabled_flag = true, + |f: &mut PicFieldsH265| f.loop_filter_across_tiles_enabled_flag = true, + |f: &mut PicFieldsH265| f.pcm_loop_filter_disabled_flag = true, + |f: &mut PicFieldsH265| f.no_pic_reordering_flag = true, + |f: &mut PicFieldsH265| f.no_bi_pred_flag = true, + ] { + let mut f = PicFieldsH265::default(); + set(&mut f); + check(f.pack()); + } + // Nothing may reach into the 11 reserved bits. + assert_eq!(seen & !0x001f_ffff, 0); + } + + #[test] + fn every_hevc_slice_parsing_field_owns_a_distinct_bit_range() { + let mut seen = 0u32; + for set in [ + |f: &mut SliceParsingFieldsH265| f.lists_modification_present_flag = true, + |f: &mut SliceParsingFieldsH265| f.long_term_ref_pics_present_flag = true, + |f: &mut SliceParsingFieldsH265| f.sps_temporal_mvp_enabled_flag = true, + |f: &mut SliceParsingFieldsH265| f.cabac_init_present_flag = true, + |f: &mut SliceParsingFieldsH265| f.output_flag_present_flag = true, + |f: &mut SliceParsingFieldsH265| f.dependent_slice_segments_enabled_flag = true, + |f: &mut SliceParsingFieldsH265| f.pps_slice_chroma_qp_offsets_present_flag = true, + |f: &mut SliceParsingFieldsH265| f.sample_adaptive_offset_enabled_flag = true, + |f: &mut SliceParsingFieldsH265| f.deblocking_filter_override_enabled_flag = true, + |f: &mut SliceParsingFieldsH265| f.pps_disable_deblocking_filter_flag = true, + |f: &mut SliceParsingFieldsH265| f.slice_segment_header_extension_present_flag = true, + |f: &mut SliceParsingFieldsH265| f.rap_pic_flag = true, + |f: &mut SliceParsingFieldsH265| f.idr_pic_flag = true, + |f: &mut SliceParsingFieldsH265| f.intra_pic_flag = true, + ] { + let mut f = SliceParsingFieldsH265::default(); + set(&mut f); + let bits = f.pack(); + assert_ne!(bits, 0); + assert_eq!(seen & bits, 0, "two fields share a bit: {bits:#010x}"); + seen |= bits; + } + assert_eq!(seen & !0x0000_3fff, 0); + } + + #[test] + fn an_unused_hevc_reference_is_invalid_and_lists_are_0xff() { + let e = VaPictureHEVC::invalid(); + assert_eq!(e.flags, VA_PICTURE_HEVC_INVALID); + assert_eq!(e.picture_id, crate::va::VA_INVALID_SURFACE); + let s = VaSliceParameterBufferHEVC::zeroed(); + assert!(s + .ref_pic_list + .iter() + .all(|l| l.iter().all(|&i| i == REF_PIC_LIST_UNUSED))); + } +} diff --git a/crates/pf-vkdecode/Cargo.toml b/crates/pf-vkdecode/Cargo.toml new file mode 100644 index 00000000..3303bcb4 --- /dev/null +++ b/crates/pf-vkdecode/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "pf-vkdecode" +description = "Native Vulkan Video H.264 decode for the clients (M2): StdVideo parameter-set/picture conversion and DPB slot management over pf-bitstream's AuPlans — the CPU-testable half; session, memory and command recording follow in WP-B (design/client-native-decode.md §3.2)" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true + +[dependencies] +ash = "0.38" +# Direct dependency on the vendored parser crate, not just pf-bitstream: the parameter-set +# conversion consumes the parser's `Sps`/`Pps` types wholesale (pf-bitstream re-exports only +# `Level`/`SliceHeader`), and the same path means the one crate instance the workspace already +# builds — no duplicate types. +cros-codecs = { path = "../pf-bitstream/vendor/cros-codecs" } +pf-bitstream = { path = "../pf-bitstream" } +tracing = "0.1" + +[dev-dependencies] +# The GPU parity test hashes decoded frames against libavcodec goldens (already +# in the workspace lock via other crates). +sha2 = "0.10" + +[lints] +workspace = true diff --git a/crates/pf-vkdecode/src/caps.rs b/crates/pf-vkdecode/src/caps.rs new file mode 100644 index 00000000..90b4639c --- /dev/null +++ b/crates/pf-vkdecode/src/caps.rs @@ -0,0 +1,1187 @@ +//! H.264 decode capability query + derivation, plus the codec-agnostic pieces the +//! H.265 sibling ([`crate::caps_h265`]) reuses: the picture-format vocabulary, the +//! coincide/distinct/layered arrangement decision, and the profile chain every +//! Vulkan object of a session is created against. +//! +//! Split on purpose: [`query_h264_caps`] is the one THIN function that talks to the +//! driver (`vkGetPhysicalDeviceVideoCapabilitiesKHR` + the three video-format-property +//! enumerations) and only COPIES facts into [`RawH264Caps`]; [`derive_caps`] turns +//! those facts into the [`DecodeCaps`] the session/image/ring modules consume and is +//! a pure function over a hand-buildable struct — every mode/format decision is +//! unit-tested without a GPU (the RADV-vs-NVIDIA coincide/distinct split is exactly +//! the driver variance the risk register names). + +use ash::vk; +use ash::vk::native as hh; + +use crate::caps_av1::Av1ProfileChain; +use crate::caps_av1::Av1ProfileKey; +use crate::caps_h265::H265ProfileChain; +use crate::caps_h265::H265ProfileKey; +use crate::device::DecodeDevice; + +/// The 8-bit 4:2:0 semi-planar format every punktfunk H.264 session decodes to, +/// and the H.265 Main one ([`crate::caps_h265::output_format_for`] picks per SPS). +pub const NV12: vk::Format = vk::Format::G8_B8R8_2PLANE_420_UNORM; +/// 10-bit 4:2:0 (P010's Vulkan spelling): H.265 Main 10's picture format. +pub const P010: vk::Format = vk::Format::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16; +/// 8-bit 4:4:4 two-plane: H.265 RExt 4:4:4 8-bit, where the device advertises it. +pub const YUV444_8: vk::Format = vk::Format::G8_B8R8_2PLANE_444_UNORM; +/// 10-bit 4:4:4 two-plane: H.265 RExt 4:4:4 10-bit, where the device advertises it. +pub const YUV444_10: vk::Format = vk::Format::G10X6_B10X6R10X6_2PLANE_444_UNORM_3PACK16; + +/// EVERY picture format a pf-vkdecode session can deliver — this crate's whole +/// output vocabulary, in one place. +/// +/// It exists so a CONSUMER's per-format table (the presenter's CSC bit-depth and +/// MSB-packing map) can be pinned against the PRODUCER rather than against a +/// hand-copied list of its own: a fifth format added here (12-bit RExt, say) breaks +/// the consumer's test instead of silently rendering through that consumer's +/// fallback. [`plane_formats`] and [`crate::caps_h265::output_format_for`] are both +/// tested to agree with it, so the vocabulary can only grow in one edit. +pub const OUTPUT_FORMATS: [vk::Format; 4] = [NV12, P010, YUV444_8, YUV444_10]; + +/// The `R*`/`R*G*` per-plane view formats the presenter's sampler path needs for +/// one picture format, or `None` for a format this crate has no plane mapping for. +/// +/// Per-plane views exist only under `MUTABLE_FORMAT` and must be format-compatible +/// with the plane they alias (spec: "Compatible formats of planes of multi-planar +/// formats", table 49.1): the 8-bit two-plane families take `R8`/`R8G8`, the +/// 10-bit `3PACK16` families take `R10X6`/`R10X6G10X6` — sampling a 10-bit plane +/// through an `R8` view would silently read half the bits of every sample, which +/// is exactly the class of silent-wrongness this crate refuses to ship. +/// (Comparisons rather than a `match`: `vk::Format` is a newtype over `i32` whose +/// field is private to ash, so its constants are not structural-match patterns.) +pub fn plane_formats(format: vk::Format) -> Option<[vk::Format; 2]> { + if format == NV12 || format == YUV444_8 { + Some([vk::Format::R8_UNORM, vk::Format::R8G8_UNORM]) + } else if format == P010 || format == YUV444_10 { + Some([ + vk::Format::R10X6_UNORM_PACK16, + vk::Format::R10X6G10X6_UNORM_2PACK16, + ]) + } else { + None + } +} + +/// The usage the pools actually create with, per role — the format queries ask the +/// driver about EXACTLY these combinations (a query for less would validate an +/// image nobody creates): +/// +/// distinct-mode DPB images: reference-only, never sampled. +pub const DPB_USAGE: vk::ImageUsageFlags = vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR; +/// Distinct-mode output images: decode destination + presenter sampling. +pub const OUTPUT_USAGE: vk::ImageUsageFlags = vk::ImageUsageFlags::from_raw( + vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR.as_raw() | vk::ImageUsageFlags::SAMPLED.as_raw(), +); +/// Coincide-mode images: DPB + decode destination + presenter sampling in one. +pub const COINCIDE_USAGE: vk::ImageUsageFlags = vk::ImageUsageFlags::from_raw( + vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR.as_raw() + | vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR.as_raw() + | vk::ImageUsageFlags::SAMPLED.as_raw(), +); + +/// One `VkVideoFormatPropertiesKHR` entry as this crate consumes it: the format +/// plus the driver's advertised usage/create-flag envelope for it — creation must +/// stay INSIDE that envelope (finding of the adversarial round: the flags used to +/// be assumed, not honoured). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VideoFormat { + pub format: vk::Format, + /// `imageUsageFlags` the driver supports for this format under the queried + /// profile (a superset of the query's usage on a conformant driver). + pub image_usage: vk::ImageUsageFlags, + /// `imageCreateFlags` the driver allows — per-plane views require + /// `MUTABLE_FORMAT` to appear here. + /// + /// ⚠ This field is also the ONLY gate on `VK_IMAGE_CREATE_EXTENDED_USAGE_BIT`, + /// which is the spec's one escape hatch from `image_usage`: `supportedVideoFormat` + /// (VUID-VkImageCreateInfo-pNext-06811) admits a usage bit outside `image_usage` + /// only when `VkImageCreateInfo::flags` includes `EXTENDED_USAGE`, and admits that + /// flag only when it is "also set in `VkVideoFormatPropertiesKHR::imageCreateFlags`" + /// (or is `VIDEO_PROFILE_INDEPENDENT`, which needs `VK_KHR_video_maintenance1`). + /// So an EMPTY value here closes the escape hatch as well as the door — measured on + /// Intel Arc, where it is empty for every profile ([`crate::probe`] docs). + pub image_create_flags: vk::ImageCreateFlags, + /// `imageType` — the image type this format may be created with. Part of the + /// `supportedVideoFormat` match (VUID-06811 compares it for EQUALITY), so it is + /// recorded rather than assumed; every fleet driver reports `TYPE_2D`, which is + /// what [`crate::images`] creates. + pub image_type: vk::ImageType, + /// `imageTiling` — likewise compared for equality by VUID-06811; every fleet + /// driver reports `OPTIMAL`. + pub image_tiling: vk::ImageTiling, +} + +impl Default for VideoFormat { + /// The shape the pools create with (`TYPE_2D` + `OPTIMAL`), so a fixture that + /// names only the interesting fields still describes a creatable image. + fn default() -> Self { + Self { + format: vk::Format::UNDEFINED, + image_usage: vk::ImageUsageFlags::empty(), + image_create_flags: vk::ImageCreateFlags::empty(), + image_type: vk::ImageType::TYPE_2D, + image_tiling: vk::ImageTiling::OPTIMAL, + } + } +} + +/// Everything the thin query copies out of the driver, hand-buildable for tests. +/// +/// The three format lists correspond to the three REAL usage combinations the +/// pools create with ([`DPB_USAGE`], [`OUTPUT_USAGE`], [`COINCIDE_USAGE`] — the +/// presenter-facing ones include `SAMPLED`), in the exact shape the driver was +/// asked: a usage the implementation does not support yields an EMPTY list (the +/// thin query maps `VK_ERROR_FORMAT_NOT_SUPPORTED` / +/// `VK_ERROR_IMAGE_USAGE_NOT_SUPPORTED` for that usage to empty rather than +/// failing the whole probe). +#[derive(Debug, Clone, Default)] +pub struct RawH264Caps { + /// `VkVideoCapabilitiesKHR::flags`. + pub capability_flags: vk::VideoCapabilityFlagsKHR, + /// `VkVideoDecodeCapabilitiesKHR::flags` (the coincide/distinct advertisement). + pub decode_flags: vk::VideoDecodeCapabilityFlagsKHR, + pub min_bitstream_buffer_offset_alignment: u64, + pub min_bitstream_buffer_size_alignment: u64, + pub picture_access_granularity: vk::Extent2D, + pub min_coded_extent: vk::Extent2D, + pub max_coded_extent: vk::Extent2D, + pub max_dpb_slots: u32, + pub max_active_reference_pictures: u32, + /// `VkVideoDecodeH264CapabilitiesKHR::maxLevelIdc` (index-coded Std level). + pub max_level_idc: hh::StdVideoH264LevelIdc, + /// `VkVideoCapabilitiesKHR::stdHeaderVersion` — session creation echoes it back. + pub std_header_version: vk::ExtensionProperties, + /// Formats usable for DISTINCT-mode DPB images (queried with [`DPB_USAGE`]). + pub dpb_formats: Vec, + /// Formats usable for DISTINCT-mode outputs (queried with [`OUTPUT_USAGE`]). + pub output_formats: Vec, + /// Formats usable when DPB and output COINCIDE ([`COINCIDE_USAGE`]). + pub coincide_formats: Vec, +} + +/// A device's decode level ceiling, tagged with the codec whose Std code space it +/// is stated in. +/// +/// `StdVideoH264LevelIdc`, `StdVideoH265LevelIdc` and `StdVideoAV1Level` are all +/// `c_uint` aliases, so nothing stops one being assigned where another belongs — +/// the compiler is silent and the numbers even look plausible (H.264 level 4.1 and +/// H.265 level 4.1 are different code points; AV1 5.1 is 13 where H.265 5.1 is 12). +/// This is the confusion `DecodeProfile` was introduced to make unrepresentable for +/// profiles; the level ceiling gets the same treatment, so a caps derivation has to +/// SAY which codec's query it copied. +/// +/// The gate itself stays a numeric comparison against [`Self::code_point`]: within +/// ONE codec the Std code points ascend with the level, which is exactly what makes +/// "stream level > device ceiling ⇒ refuse" sound. Across codecs the comparison is +/// meaningless, which is why the value carries its codec. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MaxLevelIdc { + /// `VkVideoDecodeH264CapabilitiesKHR::maxLevelIdc`. + H264(hh::StdVideoH264LevelIdc), + /// `VkVideoDecodeH265CapabilitiesKHR::maxLevelIdc`. + H265(hh::StdVideoH265LevelIdc), + /// `VkVideoDecodeAV1CapabilitiesKHR::maxLevel`. Unlike the other two this code + /// space is the BITSTREAM's own: `StdVideoAV1Level` is index-coded exactly like + /// AV1's `seq_level_idx` (2.0 = 0, 2.1 = 1, … 7.3 = 23), so the decoder's gate + /// compares the sequence header's value against it directly. + Av1(hh::StdVideoAV1Level), +} + +impl MaxLevelIdc { + /// The raw Std code point, for the decoders' level gate and its error text. + /// Compare it only against a code point of the SAME codec (the variant says + /// which) — the tag is the whole point of the type. + pub fn code_point(self) -> u32 { + match self { + MaxLevelIdc::H264(level) | MaxLevelIdc::H265(level) | MaxLevelIdc::Av1(level) => level, + } + } +} + +impl std::fmt::Display for MaxLevelIdc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MaxLevelIdc::H264(level) => write!(f, "H.264 Std level {level}"), + MaxLevelIdc::H265(level) => write!(f, "H.265 Std level {level}"), + MaxLevelIdc::Av1(level) => write!(f, "AV1 Std level {level}"), + } + } +} + +/// The derived facts the rest of the crate keys off. One value per session profile; +/// rebuilt only when the stream renegotiates to a different profile. +#[derive(Debug, Clone)] +pub struct DecodeCaps { + /// Chosen DPB/output arrangement: `true` = the decode output IS the DPB image + /// (RADV's shape), `false` = separate DPB array + output images (NVIDIA's). + /// When a driver advertises both, coincide wins — half the images, and the + /// mode field data trusts most on the fleet's AMD boxes. + pub coincide: bool, + /// `true` when the driver does NOT advertise `SEPARATE_REFERENCE_IMAGES`: every + /// DPB slot must then be a layer of ONE image array. When separate references + /// are allowed this stays `false` and each slot gets its own image (simpler + /// lifetime story; nothing downstream requires the layered arrangement). + pub layered_dpb: bool, + /// Bitstream buffer alignments, normalized to at least 1 so ring math never + /// divides by the zero an uninitialized fixture would carry. + pub min_bitstream_offset_alignment: u64, + pub min_bitstream_size_alignment: u64, + pub picture_access_granularity: vk::Extent2D, + pub min_coded_extent: vk::Extent2D, + pub max_coded_extent: vk::Extent2D, + pub max_dpb_slots: u32, + pub max_active_references: u32, + /// The device's `maxLevelIdc` for this session's codec, codec-TAGGED. + pub max_level_idc: MaxLevelIdc, + /// DPB image format (== `output_format` in coincide mode). + pub dpb_format: vk::Format, + /// Decode-output image format. + pub output_format: vk::Format, + /// The per-plane view formats of [`Self::output_format`] ([`plane_formats`]) — + /// resolved at derivation so the pool never has to re-derive (or guess) them. + pub plane_view_formats: [vk::Format; 2], + pub std_header_version: vk::ExtensionProperties, +} + +impl DecodeCaps { + /// `coded` rounded up to the device's `pictureAccessGranularity` — the extent + /// pool IMAGES are created at (the per-picture `codedExtent` stays the stream's + /// coded size; only the backing store rounds up). A zero granularity axis (an + /// uninitialized fixture) degrades to 1. + pub fn aligned_extent(&self, coded: vk::Extent2D) -> vk::Extent2D { + let round = |value: u32, granularity: u32| -> u32 { + let granularity = granularity.max(1); + value.div_ceil(granularity) * granularity + }; + vk::Extent2D { + width: round(coded.width, self.picture_access_granularity.width), + height: round(coded.height, self.picture_access_granularity.height), + } + } +} + +/// Raw caps that do not add up to a usable decoder. All of these are device gaps +/// the caller demotes on (the ladder's next rung), not stream conditions. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CapsError { + /// The driver advertises neither COINCIDE nor DISTINCT — no way to arrange a + /// DPB at all (a broken driver; the spec requires at least one). + NoDecodeMode, + /// The mode's format list does not contain the picture format the stream + /// needs. `mode` names which list, `wanted` the format: [`NV12`] for H.264 + /// and H.265 Main, [`P010`] for Main 10, the 4:4:4 pair for RExt streams — + /// the Main-10-on-an-8-bit-only-device and 4:4:4-on-a-4:2:0-only-device + /// refusals both land here, BEFORE any session exists. + NoFormat { + mode: &'static str, + wanted: vk::Format, + }, + /// The picture format the stream needs has no per-plane view mapping in this + /// crate ([`plane_formats`]) — unreachable for the four formats the envelope + /// admits; a guard against a future format arriving without its plane views. + NoPlaneMapping { format: vk::Format }, + /// The driver's entry for the wanted format in `mode` does not advertise every + /// usage bit the pool would create with (`missing` names the gap) — creating + /// anyway would be a silent VUID violation. + UsageUnsupported { + mode: &'static str, + /// The picture format whose entry fell short — NOT always NV12 (a Main 10 + /// stream is refused about P010), which is what this used to say regardless. + format: vk::Format, + missing: vk::ImageUsageFlags, + }, + /// The presenter-facing entry for `mode` does not allow `MUTABLE_FORMAT`, so + /// the per-plane views the presenter samples through ([`plane_formats`]) + /// cannot exist on this device. + NoMutableFormat { + mode: &'static str, + format: vk::Format, + }, + /// The driver forces COINCIDE mode AND a layered DPB (one image array, no + /// `SEPARATE_REFERENCE_IMAGES`): the picture-pool model — a re-activated slot + /// binding a fresh free image, so delivered pictures are never decode targets + /// — cannot exist when every slot is a fixed layer of one array. No fleet + /// device has this shape (NVIDIA = distinct, RADV = separate reference + /// images); a device that does demotes to the next decoder rung rather than + /// getting a degraded copy path built for it. + CoincideLayeredDpb, +} + +impl std::fmt::Display for CapsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CapsError::NoDecodeMode => { + write!( + f, + "driver advertises neither DPB_AND_OUTPUT_COINCIDE nor DISTINCT" + ) + } + CapsError::NoFormat { mode, wanted } => { + write!( + f, + "no {wanted:?} in the {mode} video format properties for this profile" + ) + } + CapsError::NoPlaneMapping { format } => { + write!(f, "no per-plane view mapping for {format:?}") + } + CapsError::UsageUnsupported { + mode, + format, + missing, + } => { + write!( + f, + "the {mode} {format:?} entry does not advertise usage {missing:?}" + )?; + // Name the CONSEQUENCE for the one missing bit that is not a passing + // driver quirk. Without `SAMPLED` nothing in a shader can read the + // decoded picture, so the zero-copy path this rung exists for cannot be + // built here at all — a fact worth stating in the log line rather than + // leaving a field reporter to work out from a flag name. Measured on + // Intel Arc (Windows 101.8861), where the whole advertised envelope is + // TRANSFER_SRC|DECODE_DST|DECODE_DPB with no image create flags: + // `punktfunk-session --probe-decode` prints the driver's own answer. + if missing.contains(vk::ImageUsageFlags::SAMPLED) { + write!( + f, + " — no shader can read this device's decoded pictures, so the \ + zero-copy path cannot exist on it (see --probe-decode)" + )?; + } + Ok(()) + } + CapsError::NoMutableFormat { mode, format } => { + write!( + f, + "the {mode} {format:?} entry does not allow MUTABLE_FORMAT (per-plane views)" + ) + } + CapsError::CoincideLayeredDpb => { + write!( + f, + "coincide mode with a layered DPB (no SEPARATE_REFERENCE_IMAGES) — \ + the picture-pool model needs per-slot images; demote this device" + ) + } + } + } +} + +impl std::error::Error for CapsError {} + +/// Derive the session-shaping facts from one raw H.264 query. Pure — the whole +/// coincide/distinct/layered decision table lives in `derive_arrangement` (shared +/// with the H.265 side) and in the tests below. H.264 in this program is 8-bit +/// 4:2:0, so the wanted picture format is always [`NV12`]. +pub fn derive_caps(raw: &RawH264Caps) -> Result { + let arrangement = derive_arrangement( + raw.capability_flags, + raw.decode_flags, + NV12, + &raw.dpb_formats, + &raw.output_formats, + &raw.coincide_formats, + )?; + Ok(arrangement.into_caps( + raw.min_bitstream_buffer_offset_alignment, + raw.min_bitstream_buffer_size_alignment, + raw.picture_access_granularity, + raw.min_coded_extent, + raw.max_coded_extent, + raw.max_dpb_slots, + raw.max_active_reference_pictures, + MaxLevelIdc::H264(raw.max_level_idc), + raw.std_header_version, + )) +} + +/// The codec-agnostic half of derivation: which DPB arrangement this device can +/// host, and which format lists satisfy the picture format `wanted`. +pub(crate) struct Arrangement { + coincide: bool, + layered_dpb: bool, + dpb_format: vk::Format, + output_format: vk::Format, + plane_view_formats: [vk::Format; 2], +} + +impl Arrangement { + /// Fold in the codec-specific numbers the raw query carried. (One function + /// rather than a shared raw-caps struct: the two raw structs differ only in + /// which codec's `maxLevelIdc` they copied, and pinning that difference in the + /// TYPE is worth more than saving these arguments — hence [`MaxLevelIdc`], + /// which each codec's derivation has to name its own variant of.) + #[allow(clippy::too_many_arguments)] + pub(crate) fn into_caps( + self, + min_bitstream_offset_alignment: u64, + min_bitstream_size_alignment: u64, + picture_access_granularity: vk::Extent2D, + min_coded_extent: vk::Extent2D, + max_coded_extent: vk::Extent2D, + max_dpb_slots: u32, + max_active_references: u32, + max_level_idc: MaxLevelIdc, + std_header_version: vk::ExtensionProperties, + ) -> DecodeCaps { + DecodeCaps { + coincide: self.coincide, + layered_dpb: self.layered_dpb, + min_bitstream_offset_alignment: min_bitstream_offset_alignment.max(1), + min_bitstream_size_alignment: min_bitstream_size_alignment.max(1), + picture_access_granularity, + min_coded_extent, + max_coded_extent, + max_dpb_slots, + max_active_references, + max_level_idc, + dpb_format: self.dpb_format, + output_format: self.output_format, + plane_view_formats: self.plane_view_formats, + std_header_version, + } + } +} + +/// Decide the DPB arrangement and validate `wanted` against the format lists of +/// the roles that arrangement creates images in. Pure; shared by both codecs — the +/// only codec-dependent input is `wanted`, which the H.265 side derives from the +/// SPS's chroma format and bit depth ([`crate::caps_h265::output_format_for`]). +pub(crate) fn derive_arrangement( + capability_flags: vk::VideoCapabilityFlagsKHR, + decode_flags: vk::VideoDecodeCapabilityFlagsKHR, + wanted: vk::Format, + dpb_formats: &[VideoFormat], + output_formats: &[VideoFormat], + coincide_formats: &[VideoFormat], +) -> Result { + let coincide = + decode_flags.contains(vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_COINCIDE); + let distinct = + decode_flags.contains(vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_DISTINCT); + if !coincide && !distinct { + return Err(CapsError::NoDecodeMode); + } + + let layered_dpb = + !capability_flags.contains(vk::VideoCapabilityFlagsKHR::SEPARATE_REFERENCE_IMAGES); + // Coincide preferred when both are offered (struct docs). Each picked entry is + // validated against the EXACT usage/create-flags its pool will use: presenter- + // facing images (coincide pool, distinct outputs) additionally need + // MUTABLE_FORMAT for their per-plane views; the distinct DPB needs neither + // sampling nor plane views. + let (dpb_format, output_format) = if coincide { + if layered_dpb { + // The picture-pool model needs per-slot images (a slot re-binds a + // fresh image at activation); one fixed layer per slot cannot do that. + return Err(CapsError::CoincideLayeredDpb); + } + let mode = "coincide (DPB|DST|SAMPLED)"; + let entry = pick_format(coincide_formats, wanted, mode)?; + require_usage(&entry, COINCIDE_USAGE, mode)?; + require_mutable(&entry, mode)?; + (entry.format, entry.format) + } else { + let dpb = pick_format(dpb_formats, wanted, "DPB")?; + require_usage(&dpb, DPB_USAGE, "DPB")?; + let out_mode = "output (DST|SAMPLED)"; + let output = pick_format(output_formats, wanted, out_mode)?; + require_usage(&output, OUTPUT_USAGE, out_mode)?; + require_mutable(&output, out_mode)?; + (dpb.format, output.format) + }; + let plane_view_formats = plane_formats(output_format).ok_or(CapsError::NoPlaneMapping { + format: output_format, + })?; + + Ok(Arrangement { + coincide, + layered_dpb, + dpb_format, + output_format, + plane_view_formats, + }) +} + +fn pick_format( + formats: &[VideoFormat], + wanted: vk::Format, + mode: &'static str, +) -> Result { + formats + .iter() + .copied() + .find(|f| f.format == wanted) + .ok_or(CapsError::NoFormat { mode, wanted }) +} + +/// The pool's creation usage must sit inside the driver's advertised envelope. +fn require_usage( + entry: &VideoFormat, + usage: vk::ImageUsageFlags, + mode: &'static str, +) -> Result<(), CapsError> { + let missing = usage & !entry.image_usage; + if missing.is_empty() { + Ok(()) + } else { + // The entry's OWN format, not the caller's `wanted`: they are equal here (the + // entry was picked by format), and taking it from the driver's record keeps the + // message describing what the driver actually said. + Err(CapsError::UsageUnsupported { + mode, + format: entry.format, + missing, + }) + } +} + +fn require_mutable(entry: &VideoFormat, mode: &'static str) -> Result<(), CapsError> { + if entry + .image_create_flags + .contains(vk::ImageCreateFlags::MUTABLE_FORMAT) + { + Ok(()) + } else { + Err(CapsError::NoMutableFormat { + mode, + format: entry.format, + }) + } +} + +/// A complete H.264 decode profile chain in one movable value, mirroring the +/// encoder's `RgbProfileStack`: profile identity in Vulkan is BY VALUE, so every +/// consumer (caps query, session create, image/buffer create, query pool create) +/// rebuilds a structurally identical chain rather than sharing pointers. +/// +/// [`Self::wire`] links `profile.p_next` to this struct's OWN `h264` field; the +/// value must not move between `wire()` and the last use of the returned reference +/// — **or of any raw pointer taken from it**, which is the half that does not come +/// for free. `wire` borrows `self` for the reference's life, so wherever the +/// reference is passed on AS a reference the borrow checker does pin the chain: a +/// `profiles(std::slice::from_ref(profile))` builder carries the borrow in its own +/// lifetime parameter, and so does handing `profile` straight to an entry point. +/// Where a `*const` is taken instead, the borrow ENDS at that line and nothing but +/// inspection keeps the chain still — [`crate::decoder`]'s query pool must do +/// exactly that (`push_next` there would clobber the profile's own `p_next`), so it +/// holds the reference across the call in a helper's SIGNATURE +/// (`OpRing::create_status_query_pool`) rather than relying on this sentence. +pub(crate) struct H264ProfileChain { + h264: vk::VideoDecodeH264ProfileInfoKHR<'static>, + profile: vk::VideoProfileInfoKHR<'static>, +} + +impl H264ProfileChain { + /// Build the (unwired) chain for one SPS profile. `std_profile_idc` is the + /// value WP-A's conversion validated (66/77/100/244 pass-through); H.264 here + /// is 8-bit 4:2:0 progressive by the program envelope. + pub(crate) fn new(std_profile_idc: hh::StdVideoH264ProfileIdc) -> Self { + Self { + h264: vk::VideoDecodeH264ProfileInfoKHR::default() + .std_profile_idc(std_profile_idc) + .picture_layout(vk::VideoDecodeH264PictureLayoutFlagsKHR::PROGRESSIVE), + profile: vk::VideoProfileInfoKHR::default() + .video_codec_operation(vk::VideoCodecOperationFlagsKHR::DECODE_H264) + .chroma_subsampling(vk::VideoChromaSubsamplingFlagsKHR::TYPE_420) + .luma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8) + .chroma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8), + } + } + + /// Wire the internal `p_next` chain and hand out the profile root. Do not move + /// `self` while the returned reference (or any pointer taken from it) lives. + pub(crate) fn wire(&mut self) -> &vk::VideoProfileInfoKHR<'static> { + self.profile.p_next = (&self.h264 as *const vk::VideoDecodeH264ProfileInfoKHR<'_>).cast(); + &self.profile + } +} + +/// Which codec profile a session — and therefore every image, buffer and query +/// pool created for it — is built against. A plain `Copy` descriptor rather than a +/// chain, because profile identity in Vulkan is BY VALUE: each consumer rebuilds +/// its own structurally identical chain from this, and nothing shares pointers. +/// +/// It is also the reason this type exists at all: `StdVideoH264ProfileIdc`, +/// `StdVideoH265ProfileIdc` and `StdVideoAV1Profile` are ALL `c_uint`, so a bare +/// idc parameter would let one codec's profile silently build another's chain — +/// the images and the session would then disagree about the profile and the driver +/// would reject (or worse, accept) at submit time. The enum makes that mistake +/// unrepresentable. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum DecodeProfile { + H264(hh::StdVideoH264ProfileIdc), + H265(H265ProfileKey), + /// AV1's key additionally carries `filmGrainSupport`, which is part of the + /// profile — so an image pool built for a grain stream is a different pool + /// from one built for a grain-less one, by construction + /// ([`crate::caps_av1`] module docs). + Av1(Av1ProfileKey), +} + +impl DecodeProfile { + /// A fresh, UNWIRED chain for this profile. Call [`ProfileChain::wire`] on the + /// returned value and keep it immobile for as long as the wired pointers live. + pub(crate) fn chain(self) -> ProfileChain { + match self { + DecodeProfile::H264(idc) => ProfileChain::H264(H264ProfileChain::new(idc)), + DecodeProfile::H265(key) => ProfileChain::H265(H265ProfileChain::new(key)), + DecodeProfile::Av1(key) => ProfileChain::Av1(Av1ProfileChain::new(key)), + } + } +} + +/// One codec's profile chain, type-erased for the shared creation paths (images, +/// bitstream ring, query pool). Same immobility contract as the three variants. +pub(crate) enum ProfileChain { + H264(H264ProfileChain), + H265(H265ProfileChain), + Av1(Av1ProfileChain), +} + +impl ProfileChain { + /// Wire the chain and hand out the profile root. Do not move `self` while the + /// returned reference (or any pointer taken from it) lives. + pub(crate) fn wire(&mut self) -> &vk::VideoProfileInfoKHR<'static> { + match self { + ProfileChain::H264(chain) => chain.wire(), + ProfileChain::H265(chain) => chain.wire(), + ProfileChain::Av1(chain) => chain.wire(), + } + } +} + +/// The one function that asks the driver: video capabilities (with the decode + +/// H.264 capability structs chained) plus the three format-property enumerations. +/// Copies facts out and returns; derivation happens in [`derive_caps`]. +/// +/// # Safety +/// +/// `dev` wraps live handles per the [`crate::DeviceHandles`] contract (this calls +/// instance-level functions against its physical device). +pub(crate) unsafe fn query_h264_caps( + dev: &DecodeDevice, + std_profile_idc: hh::StdVideoH264ProfileIdc, +) -> Result { + let mut chain = H264ProfileChain::new(std_profile_idc); + let profile = chain.wire(); + + let mut h264_caps = vk::VideoDecodeH264CapabilitiesKHR::default(); + let mut decode_caps = vk::VideoDecodeCapabilitiesKHR::default(); + // ⚠ ORDER IS LOAD-BEARING — see the measured Intel Arc swap in + // [`crate::caps_h265::query_h265_caps`]. `push_next` prepends, so pushing the codec + // struct FIRST leaves VkVideoDecodeCapabilitiesKHR directly after the base struct. + let mut caps = vk::VideoCapabilitiesKHR::default() + .push_next(&mut h264_caps) + .push_next(&mut decode_caps); + // SAFETY: physical device is live (DeviceHandles contract); `profile` roots a + // fully wired, immovable chain; `caps` chains driver-fillable structs that all + // outlive the call. + let r = unsafe { + (dev.video_queue_instance() + .fp() + .get_physical_device_video_capabilities_khr)( + dev.physical_device(), profile, &mut caps + ) + }; + if r != vk::Result::SUCCESS { + return Err(r); + } + // Copy everything out before the chained &mut borrows end (encoder precedent). + let capability_flags = caps.flags; + let min_bitstream_buffer_offset_alignment = caps.min_bitstream_buffer_offset_alignment; + let min_bitstream_buffer_size_alignment = caps.min_bitstream_buffer_size_alignment; + let picture_access_granularity = caps.picture_access_granularity; + let min_coded_extent = caps.min_coded_extent; + let max_coded_extent = caps.max_coded_extent; + let max_dpb_slots = caps.max_dpb_slots; + let max_active_reference_pictures = caps.max_active_reference_pictures; + let std_header_version = caps.std_header_version; + let decode_flags = decode_caps.flags; + let max_level_idc = h264_caps.max_level_idc; + + // The three queries carry the REAL creation usages (SAMPLED included for the + // presenter-facing roles) so the answers validate the images the pools build. + let profile = DecodeProfile::H264(std_profile_idc); + // SAFETY: same liveness as above; the helper wires its own chain (this and + // the two calls below). + let dpb_formats = unsafe { query_formats(dev, profile, DPB_USAGE)? }; + // SAFETY: as above. + let output_formats = unsafe { query_formats(dev, profile, OUTPUT_USAGE)? }; + // SAFETY: as above. + let coincide_formats = unsafe { query_formats(dev, profile, COINCIDE_USAGE)? }; + + Ok(RawH264Caps { + capability_flags, + decode_flags, + min_bitstream_buffer_offset_alignment, + min_bitstream_buffer_size_alignment, + picture_access_granularity, + min_coded_extent, + max_coded_extent, + max_dpb_slots, + max_active_reference_pictures, + max_level_idc, + std_header_version, + dpb_formats, + output_formats, + coincide_formats, + }) +} + +/// Enumerate the video format properties for one usage combination. A usage the +/// implementation rejects outright maps to an EMPTY list (that is the driver saying +/// "not this arrangement", which [`derive_caps`] then routes around). +/// +/// # Safety +/// +/// As [`query_h264_caps`]. +pub(crate) unsafe fn query_formats( + dev: &DecodeDevice, + decode_profile: DecodeProfile, + usage: vk::ImageUsageFlags, +) -> Result, vk::Result> { + // SAFETY: the caller's DeviceHandles contract makes these two live, which is + // exactly what the physical-device form needs. + unsafe { + query_formats_on( + dev.video_queue_instance(), + dev.physical_device(), + decode_profile, + usage, + ) + } +} + +/// [`query_formats`] against a bare physical device — no `VkDevice` in sight. +/// +/// Split out so [`crate::probe`] enumerates through the SAME code the session's caps +/// query runs, rather than a second copy that would drift (the probe's whole value is +/// that its answer is the one derivation will see). `vkGetPhysicalDeviceVideoFormat- +/// PropertiesKHR` is an instance-level command over a physical device, so nothing here +/// ever needed the logical device the old signature demanded. +/// +/// # Safety +/// +/// `video_queue_instance` must be loaded against a live `VkInstance`, and +/// `physical_device` must be one of that instance's physical devices. +pub(crate) unsafe fn query_formats_on( + video_queue_instance: &ash::khr::video_queue::Instance, + physical_device: vk::PhysicalDevice, + decode_profile: DecodeProfile, + usage: vk::ImageUsageFlags, +) -> Result, vk::Result> { + let mut chain = decode_profile.chain(); + let profile = chain.wire(); + let mut profile_list = + vk::VideoProfileListInfoKHR::default().profiles(std::slice::from_ref(profile)); + let info = vk::PhysicalDeviceVideoFormatInfoKHR::default() + .image_usage(usage) + .push_next(&mut profile_list); + + let fp = video_queue_instance + .fp() + .get_physical_device_video_format_properties_khr; + let mut count = 0u32; + // SAFETY: live physical device; `info` roots a wired chain outliving the call; + // null properties pointer is the spec's count-query form. + let r = unsafe { fp(physical_device, &info, &mut count, std::ptr::null_mut()) }; + match r { + vk::Result::SUCCESS => {} + // "This usage/profile combination has no formats" — an arrangement gap, + // not a failure (derive_caps decides whether a usable mode remains). + vk::Result::ERROR_FORMAT_NOT_SUPPORTED + | vk::Result::ERROR_IMAGE_USAGE_NOT_SUPPORTED_KHR => return Ok(Vec::new()), + err => return Err(err), + } + let mut props = vec![vk::VideoFormatPropertiesKHR::default(); count as usize]; + // SAFETY: as above, with a properties array of exactly the driver-reported count. + let r = unsafe { fp(physical_device, &info, &mut count, props.as_mut_ptr()) }; + if r != vk::Result::SUCCESS && r != vk::Result::INCOMPLETE { + return Err(r); + } + props.truncate(count as usize); + Ok(props + .iter() + .map(|p| VideoFormat { + format: p.format, + image_usage: p.image_usage_flags, + image_create_flags: p.image_create_flags, + image_type: p.image_type, + image_tiling: p.image_tiling, + }) + .collect()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A format entry advertising `usage` plus the mutable-format allowance. + fn entry(format: vk::Format, usage: vk::ImageUsageFlags) -> VideoFormat { + VideoFormat { + format, + image_usage: usage, + image_create_flags: vk::ImageCreateFlags::MUTABLE_FORMAT + | vk::ImageCreateFlags::ALIAS + | vk::ImageCreateFlags::EXTENDED_USAGE, + ..Default::default() + } + } + + /// A raw-caps fixture in RADV's shape: coincide advertised, separate reference + /// images allowed, sane alignments. + fn radv_like() -> RawH264Caps { + RawH264Caps { + capability_flags: vk::VideoCapabilityFlagsKHR::SEPARATE_REFERENCE_IMAGES, + decode_flags: vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_COINCIDE, + min_bitstream_buffer_offset_alignment: 128, + min_bitstream_buffer_size_alignment: 128, + picture_access_granularity: vk::Extent2D { + width: 1, + height: 1, + }, + min_coded_extent: vk::Extent2D { + width: 16, + height: 16, + }, + max_coded_extent: vk::Extent2D { + width: 8192, + height: 8192, + }, + max_dpb_slots: 17, + max_active_reference_pictures: 16, + max_level_idc: hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_6_2, + std_header_version: vk::ExtensionProperties::default(), + dpb_formats: vec![], + output_formats: vec![], + coincide_formats: vec![entry(NV12, COINCIDE_USAGE), entry(P010, COINCIDE_USAGE)], + } + } + + /// NVIDIA's shape: distinct only, NO separate reference images (layered DPB + /// array), and only the distinct-mode format lists populated. The DPB entry + /// deliberately advertises NEITHER sampling nor mutable formats — reference + /// arrays need neither, and requiring them there would fail real devices. + fn nvidia_like() -> RawH264Caps { + RawH264Caps { + capability_flags: vk::VideoCapabilityFlagsKHR::empty(), + decode_flags: vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_DISTINCT, + dpb_formats: vec![VideoFormat { + format: NV12, + image_usage: DPB_USAGE, + image_create_flags: vk::ImageCreateFlags::empty(), + ..Default::default() + }], + output_formats: vec![entry(NV12, OUTPUT_USAGE)], + coincide_formats: vec![], + ..radv_like() + } + } + + /// [`OUTPUT_FORMATS`] is the vocabulary a CONSUMER pins its own per-format + /// table against, so it has to be the whole of what this crate can deliver — + /// no more (a format listed here but unmappable would fail a pool build) and + /// no less (a format produced but unlisted is exactly the silent + /// wrong-colour-math case the listing exists to stop). + #[test] + fn the_output_format_vocabulary_is_the_whole_of_what_this_crate_delivers() { + for format in OUTPUT_FORMATS { + assert!( + plane_formats(format).is_some(), + "{format:?} is advertised as an output but has no plane views" + ); + } + // Every (chroma, depth) pair the H.265 envelope admits resolves INTO the + // vocabulary — the one producer that picks a format from stream facts. + for chroma in 0u8..=4 { + for depth in 0u8..=4 { + if let Some(f) = crate::caps_h265::output_format_for(chroma, depth) { + assert!( + OUTPUT_FORMATS.contains(&f), + "output_format_for({chroma}, {depth}) = {f:?} is outside \ + OUTPUT_FORMATS" + ); + } + } + } + // H.264 is the 8-bit 4:2:0 envelope — its one format is in there too. + assert!(OUTPUT_FORMATS.contains(&NV12)); + } + + #[test] + fn a_coincide_device_derives_coincide_with_one_shared_format() { + let caps = derive_caps(&radv_like()).unwrap(); + assert!(caps.coincide); + assert!( + !caps.layered_dpb, + "separate reference images advertised — per-slot images" + ); + assert_eq!(caps.dpb_format, NV12); + assert_eq!(caps.output_format, NV12); + assert_eq!(caps.max_dpb_slots, 17); + assert_eq!(caps.min_bitstream_offset_alignment, 128); + assert_eq!( + caps.max_level_idc, + MaxLevelIdc::H264(hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_6_2), + "an H.264 query yields an H.264-tagged ceiling — the tag is what keeps \ + the decoders' numeric level gate comparing like with like" + ); + } + + #[test] + fn a_distinct_device_derives_distinct_with_a_layered_dpb() { + let caps = derive_caps(&nvidia_like()).unwrap(); + assert!(!caps.coincide); + assert!( + caps.layered_dpb, + "no SEPARATE_REFERENCE_IMAGES — one image array carries every slot" + ); + assert_eq!(caps.dpb_format, NV12); + assert_eq!(caps.output_format, NV12); + } + + #[test] + fn a_device_advertising_both_modes_prefers_coincide() { + let mut raw = radv_like(); + raw.decode_flags = vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_COINCIDE + | vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_DISTINCT; + raw.dpb_formats = vec![entry(NV12, DPB_USAGE)]; + raw.output_formats = vec![entry(NV12, OUTPUT_USAGE)]; + let caps = derive_caps(&raw).unwrap(); + assert!(caps.coincide, "coincide wins when both are offered"); + } + + #[test] + fn no_mode_and_no_nv12_are_distinct_hard_errors() { + let mut raw = radv_like(); + raw.decode_flags = vk::VideoDecodeCapabilityFlagsKHR::empty(); + assert_eq!(derive_caps(&raw).unwrap_err(), CapsError::NoDecodeMode); + + let mut raw = radv_like(); + raw.coincide_formats = vec![entry(P010, COINCIDE_USAGE)]; + assert_eq!( + derive_caps(&raw).unwrap_err(), + CapsError::NoFormat { + mode: "coincide (DPB|DST|SAMPLED)", + wanted: NV12 + } + ); + + // Distinct mode reports which HALF is missing NV12. + let mut raw = nvidia_like(); + raw.output_formats = vec![]; + assert_eq!( + derive_caps(&raw).unwrap_err(), + CapsError::NoFormat { + mode: "output (DST|SAMPLED)", + wanted: NV12 + } + ); + } + + #[test] + fn an_advertised_usage_missing_a_creation_bit_is_an_error_naming_the_gap() { + // A coincide entry that supports decode but NOT sampling: the presenter + // cannot read it, so derivation must refuse rather than create anyway. + let mut raw = radv_like(); + raw.coincide_formats = vec![entry( + NV12, + vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR | vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR, + )]; + assert_eq!( + derive_caps(&raw).unwrap_err(), + CapsError::UsageUnsupported { + mode: "coincide (DPB|DST|SAMPLED)", + format: NV12, + missing: vk::ImageUsageFlags::SAMPLED + } + ); + // The missing bit is SAMPLED, so the message says what that COSTS — a field + // report carrying this line should not need a second round trip to learn that + // the device cannot host the rung at all. + assert!( + derive_caps(&raw) + .unwrap_err() + .to_string() + .contains("zero-copy path cannot exist"), + "a missing SAMPLED must name its consequence: {}", + derive_caps(&raw).unwrap_err() + ); + + // Same on the distinct output half. + let mut raw = nvidia_like(); + raw.output_formats = vec![entry(NV12, vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR)]; + assert_eq!( + derive_caps(&raw).unwrap_err(), + CapsError::UsageUnsupported { + mode: "output (DST|SAMPLED)", + format: NV12, + missing: vk::ImageUsageFlags::SAMPLED + } + ); + + // A 10-bit stream is refused about P010, not about NV12 — the message used to + // say "NV12" whatever the stream was, which sends a reader looking at the wrong + // format's support. + let mut raw = radv_like(); + raw.coincide_formats = vec![entry( + P010, + vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR | vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR, + )]; + let err = crate::caps_h265::derive_caps_h265( + &crate::caps_h265::RawH265Caps { + capability_flags: raw.capability_flags, + decode_flags: raw.decode_flags, + coincide_formats: raw.coincide_formats.clone(), + ..Default::default() + }, + P010, + ) + .unwrap_err(); + assert_eq!( + err, + CapsError::UsageUnsupported { + mode: "coincide (DPB|DST|SAMPLED)", + format: P010, + missing: vk::ImageUsageFlags::SAMPLED + } + ); + assert!(err.to_string().contains("G10X6"), "{err}"); + } + + #[test] + fn a_presenter_facing_entry_without_mutable_format_is_refused() { + let mut raw = radv_like(); + raw.coincide_formats = vec![VideoFormat { + format: NV12, + image_usage: COINCIDE_USAGE, + image_create_flags: vk::ImageCreateFlags::empty(), + ..Default::default() + }]; + assert_eq!( + derive_caps(&raw).unwrap_err(), + CapsError::NoMutableFormat { + mode: "coincide (DPB|DST|SAMPLED)", + format: NV12, + } + ); + + // The distinct DPB entry needs NO mutable-format allowance (nvidia_like's + // DPB entry has empty create flags and derives fine). + assert!(derive_caps(&nvidia_like()).is_ok()); + } + + #[test] + fn extents_round_up_to_the_picture_access_granularity() { + let mut raw = radv_like(); + raw.picture_access_granularity = vk::Extent2D { + width: 64, + height: 16, + }; + let caps = derive_caps(&raw).unwrap(); + // 1920x1080: width already aligned, height rounds to 1088 — the exact + // padded shape the old smeared-rows class came from, now explicit. + let aligned = caps.aligned_extent(vk::Extent2D { + width: 1920, + height: 1080, + }); + assert_eq!((aligned.width, aligned.height), (1920, 1088)); + + // Granularity 1 is the identity; a zero axis degrades to 1, not a panic. + let mut raw = radv_like(); + raw.picture_access_granularity = vk::Extent2D { + width: 0, + height: 1, + }; + let caps = derive_caps(&raw).unwrap(); + let aligned = caps.aligned_extent(vk::Extent2D { + width: 321, + height: 241, + }); + assert_eq!((aligned.width, aligned.height), (321, 241)); + } + + #[test] + fn coincide_with_a_layered_dpb_is_unsupported_not_worked_around() { + // A driver forcing coincide AND a single layered DPB array: the pool + // model (fresh image per activation) cannot exist there, and no fleet + // device has this shape — refuse so the ladder demotes. + let mut raw = radv_like(); + raw.capability_flags = vk::VideoCapabilityFlagsKHR::empty(); + assert_eq!( + derive_caps(&raw).unwrap_err(), + CapsError::CoincideLayeredDpb + ); + } + + #[test] + fn zero_alignments_normalize_to_one_so_ring_math_never_divides_by_zero() { + let mut raw = radv_like(); + raw.min_bitstream_buffer_offset_alignment = 0; + raw.min_bitstream_buffer_size_alignment = 0; + let caps = derive_caps(&raw).unwrap(); + assert_eq!(caps.min_bitstream_offset_alignment, 1); + assert_eq!(caps.min_bitstream_size_alignment, 1); + } + + #[test] + fn plane_view_formats_follow_the_picture_format_bit_depth() { + // The 8-bit families sample through R8/R8G8; the 10-bit 3PACK16 families + // MUST use the R10X6 pair — an R8 view over a 10-bit plane reads half of + // every sample and produces a plausible-looking wrong picture. + assert_eq!( + plane_formats(NV12), + Some([vk::Format::R8_UNORM, vk::Format::R8G8_UNORM]) + ); + assert_eq!(plane_formats(YUV444_8), plane_formats(NV12)); + assert_eq!( + plane_formats(P010), + Some([ + vk::Format::R10X6_UNORM_PACK16, + vk::Format::R10X6G10X6_UNORM_2PACK16 + ]) + ); + assert_eq!(plane_formats(YUV444_10), plane_formats(P010)); + // Anything else has no mapping — derivation refuses rather than guesses. + assert_eq!(plane_formats(vk::Format::R8G8B8A8_UNORM), None); + + // And the derived caps carry the resolved pair, so pools never re-derive. + let caps = derive_caps(&radv_like()).unwrap(); + assert_eq!( + caps.plane_view_formats, + [vk::Format::R8_UNORM, vk::Format::R8G8_UNORM] + ); + } + + #[test] + fn the_profile_chain_wires_h264_behind_the_root_profile() { + let mut chain = + H264ProfileChain::new(hh::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_MAIN); + let profile = chain.wire(); + assert_eq!( + profile.video_codec_operation, + vk::VideoCodecOperationFlagsKHR::DECODE_H264 + ); + assert!(!profile.p_next.is_null()); + // SAFETY: wire() pointed p_next at chain's own h264 field, which lives for + // this whole scope and is a valid VideoDecodeH264ProfileInfoKHR. + let h264 = unsafe { + &*profile + .p_next + .cast::>() + }; + assert_eq!( + h264.std_profile_idc, + hh::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_MAIN + ); + assert_eq!( + h264.picture_layout, + vk::VideoDecodeH264PictureLayoutFlagsKHR::PROGRESSIVE + ); + } +} diff --git a/crates/pf-vkdecode/src/caps_av1.rs b/crates/pf-vkdecode/src/caps_av1.rs new file mode 100644 index 00000000..c3b88b9a --- /dev/null +++ b/crates/pf-vkdecode/src/caps_av1.rs @@ -0,0 +1,718 @@ +//! AV1 decode capability query + derivation — [`crate::caps_h265`] one codec over. +//! +//! Same split as the other two: `query_av1_caps` is the one THIN function that +//! talks to the driver and only COPIES facts into [`RawAv1Caps`]; +//! [`derive_caps_av1`] is pure over a hand-buildable struct and shares the whole +//! coincide/distinct/layered decision table with H.264 and H.265 +//! (`derive_arrangement`, in [`crate::caps`]). +//! +//! What AV1 adds to the H.265 shape is FILM GRAIN, and it is not a detail. Grain +//! synthesis is part of the DECODE PROFILE — `VkVideoDecodeAV1ProfileInfoKHR` +//! carries `filmGrainSupport` beside `stdProfile`, and profile identity in Vulkan +//! is BY VALUE across the caps query, the session, every profile-listed +//! image/buffer and the query pool. So a session for a stream whose sequence +//! header enables grain is a DIFFERENT profile from one that does not, and a +//! device that cannot host the grain-enabled profile answers the caps query with a +//! `VK_ERROR_VIDEO_PROFILE_OPERATION_NOT_SUPPORTED_KHR`-class result. +//! +//! That refusal is the whole point, and it is why [`Av1ProfileKey`] carries the +//! flag rather than the decoder passing `VK_FALSE` and hoping: a decoder that +//! silently asked for a grain-less profile would decode the stream's pictures +//! correctly and then present them WITHOUT the grain the encoder relied on — a +//! plausible-looking, measurably wrong picture, which is the class this crate +//! exists to refuse. The stream's grain is either synthesized by the hardware or +//! the device demotes to the next decoder rung. +//! +//! The picture format is the stream's, as in H.265: 4:2:0 8-bit → NV12, 4:2:0 +//! 10-bit → P010, 4:4:4 (AV1 High) → the two-plane 4:4:4 formats. Monochrome, +//! 4:2:2 and 12-bit are refused BEFORE a session exists — this crate has no +//! output plumbing for any of them ([`crate::OUTPUT_FORMATS`] is the whole +//! vocabulary). + +use ash::vk; +use ash::vk::native as hh; + +use crate::caps::derive_arrangement; +use crate::caps::CapsError; +use crate::caps::DecodeCaps; +use crate::caps::DecodeProfile; +use crate::caps::MaxLevelIdc; +use crate::caps::VideoFormat; +use crate::caps::COINCIDE_USAGE; +use crate::caps::DPB_USAGE; +use crate::caps::OUTPUT_USAGE; +use crate::caps_h265::output_format_for; +use crate::device::DecodeDevice; +use crate::params_av1::ParamsAv1Error; +use crate::params_av1::STD_PROFILE_HIGH; +use crate::params_av1::STD_PROFILE_MAIN; +use crate::params_av1::STD_PROFILE_PROFESSIONAL; + +/// The stream facts that identify an AV1 decode profile, as Vulkan states them. +/// +/// Every one of these is a `VkVideoProfileInfoKHR`/`VkVideoDecodeAV1ProfileInfoKHR` +/// field, and profile identity in Vulkan is BY VALUE — so this small `Copy` key is +/// what gets passed around, and each consumer rebuilds a structurally identical +/// chain from it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Av1ProfileKey { + /// `StdVideoAV1Profile`: Main (0), High (1), Professional (2). + pub std_profile: hh::StdVideoAV1Profile, + pub chroma_subsampling: vk::VideoChromaSubsamplingFlagsKHR, + pub luma_bit_depth: vk::VideoComponentBitDepthFlagsKHR, + pub chroma_bit_depth: vk::VideoComponentBitDepthFlagsKHR, + /// `VkVideoDecodeAV1ProfileInfoKHR::filmGrainSupport` — the SEQUENCE's + /// `film_grain_params_present`, not any one frame's `apply_grain`. A session + /// is created against one profile and lives across frames, so the sequence + /// flag is the only honest answer; a frame cannot apply grain a sequence + /// never declared (`crate::pic_av1`'s `pFilmGrain` gate requires both). + pub film_grain: bool, +} + +impl Av1ProfileKey { + /// Build the key from one sequence header's facts: `seq_profile`, the + /// sampling in the planner's `chroma_format_idc` vocabulary, the bit depth in + /// BITS (8/10/12, as [`pf_bitstream::av1::PicturePlan::bit_depth`] states it) + /// and whether the sequence enables film grain. + /// + /// Every combination this crate has no picture format for is refused HERE, + /// before any query or session: monochrome and 4:2:2 (and the 4:4:0 shape the + /// planner reports as 4) have no two-plane format in [`crate::OUTPUT_FORMATS`], + /// and 12-bit has none either. + pub fn from_stream( + seq_profile: u8, + chroma_format_idc: u8, + bit_depth: u8, + film_grain: bool, + ) -> Result { + let std_profile = match seq_profile { + 0 => STD_PROFILE_MAIN, + 1 => STD_PROFILE_HIGH, + 2 => STD_PROFILE_PROFESSIONAL, + other => return Err(ParamsAv1Error::UnsupportedProfile(other)), + }; + let chroma_subsampling = match chroma_format_idc { + 1 => vk::VideoChromaSubsamplingFlagsKHR::TYPE_420, + 3 => vk::VideoChromaSubsamplingFlagsKHR::TYPE_444, + // Monochrome IS expressible as a Vulkan profile + // (`VideoChromaSubsamplingFlagsKHR::MONOCHROME`) and this crate still + // refuses it: every picture format it delivers is two-plane, and the + // presenter samples both planes. Refused rather than half-supported. + other => return Err(ParamsAv1Error::UnsupportedChromaFormat(other)), + }; + let depth = match bit_depth { + 8 => vk::VideoComponentBitDepthFlagsKHR::TYPE_8, + 10 => vk::VideoComponentBitDepthFlagsKHR::TYPE_10, + other => return Err(ParamsAv1Error::UnsupportedBitDepth(other)), + }; + // AV1 codes ONE bit depth for the whole sequence — there is no separate + // chroma depth to disagree with luma (the H.265 gate's extra clause has no + // counterpart here). + Ok(Self { + std_profile, + chroma_subsampling, + luma_bit_depth: depth, + chroma_bit_depth: depth, + film_grain, + }) + } + + /// The key for a stream whose shape the SESSION already negotiated but whose + /// sequence header has not arrived — the construction-time probe's entry point + /// ([`crate::VkAv1Decoder::probe_stream_support`]). + /// + /// `seq_profile` is the one thing the negotiation does not carry, so it is + /// derived from the pair: 4:2:0 → Main, 4:4:4 → High (4:4:4 is only + /// expressible in High or Professional, and a punktfunk host encodes it as + /// High). Everything else goes to Professional, which cannot rescue a + /// combination [`Self::from_stream`] refuses — ONE gate produces the error. + pub fn from_negotiated( + chroma_format_idc: u8, + bit_depth: u8, + film_grain: bool, + ) -> Result { + let seq_profile = match chroma_format_idc { + 1 => 0, + 3 => 1, + _ => 2, + }; + Self::from_stream(seq_profile, chroma_format_idc, bit_depth, film_grain) + } + + /// The picture format a session on this profile decodes to, or `None` for a + /// combination outside the envelope (unreachable off [`Self::from_stream`], + /// which already gated it). + /// + /// Resolved through [`output_format_for`], the crate's one (sampling, depth) → + /// format map, so the AV1 rung can only ever deliver formats + /// [`crate::OUTPUT_FORMATS`] already names. + pub fn output_format(&self) -> Option { + let ten_bit = self.luma_bit_depth == vk::VideoComponentBitDepthFlagsKHR::TYPE_10; + let depth_minus8 = if ten_bit { 2 } else { 0 }; + if self.chroma_subsampling == vk::VideoChromaSubsamplingFlagsKHR::TYPE_420 { + output_format_for(1, depth_minus8) + } else if self.chroma_subsampling == vk::VideoChromaSubsamplingFlagsKHR::TYPE_444 { + output_format_for(3, depth_minus8) + } else { + None + } + } +} + +/// A complete AV1 decode profile chain in one movable value — +/// [`crate::caps_h265::H265ProfileChain`]'s twin. +/// +/// [`Self::wire`] links `profile.p_next` to this struct's OWN `av1` field; the +/// value must not move between `wire()` and the last use of the returned reference. +pub(crate) struct Av1ProfileChain { + av1: vk::VideoDecodeAV1ProfileInfoKHR<'static>, + profile: vk::VideoProfileInfoKHR<'static>, +} + +impl Av1ProfileChain { + /// Build the (unwired) chain for one stream profile. + pub(crate) fn new(key: Av1ProfileKey) -> Self { + Self { + av1: vk::VideoDecodeAV1ProfileInfoKHR::default() + .std_profile(key.std_profile) + // Stated from the SEQUENCE, never softened to false to make a + // query pass: a grain-less profile decodes a grain stream into + // pictures the encoder never intended (module docs). + .film_grain_support(key.film_grain), + profile: vk::VideoProfileInfoKHR::default() + .video_codec_operation(vk::VideoCodecOperationFlagsKHR::DECODE_AV1) + .chroma_subsampling(key.chroma_subsampling) + .luma_bit_depth(key.luma_bit_depth) + .chroma_bit_depth(key.chroma_bit_depth), + } + } + + /// Wire the internal `p_next` chain and hand out the profile root. Do not move + /// `self` while the returned reference (or any pointer taken from it) lives. + pub(crate) fn wire(&mut self) -> &vk::VideoProfileInfoKHR<'static> { + self.profile.p_next = (&self.av1 as *const vk::VideoDecodeAV1ProfileInfoKHR<'_>).cast(); + &self.profile + } +} + +/// Everything the thin AV1 query copies out of the driver, hand-buildable for +/// tests. Field-for-field [`crate::RawH265Caps`], except `max_level` carries an +/// AV1 Std level code point. +#[derive(Debug, Clone, Default)] +pub struct RawAv1Caps { + /// `VkVideoCapabilitiesKHR::flags`. + pub capability_flags: vk::VideoCapabilityFlagsKHR, + /// `VkVideoDecodeCapabilitiesKHR::flags` (the coincide/distinct advertisement). + pub decode_flags: vk::VideoDecodeCapabilityFlagsKHR, + pub min_bitstream_buffer_offset_alignment: u64, + pub min_bitstream_buffer_size_alignment: u64, + pub picture_access_granularity: vk::Extent2D, + pub min_coded_extent: vk::Extent2D, + pub max_coded_extent: vk::Extent2D, + pub max_dpb_slots: u32, + pub max_active_reference_pictures: u32, + /// `VkVideoDecodeAV1CapabilitiesKHR::maxLevel` (index-coded Std level — the + /// SAME numbering as the bitstream's `seq_level_idx`, which is what makes the + /// decoder's level gate a plain comparison). + pub max_level: hh::StdVideoAV1Level, + /// `VkVideoCapabilitiesKHR::stdHeaderVersion` — session creation echoes it back. + pub std_header_version: vk::ExtensionProperties, + /// Formats usable for DISTINCT-mode DPB images (queried with [`DPB_USAGE`]). + pub dpb_formats: Vec, + /// Formats usable for DISTINCT-mode outputs (queried with [`OUTPUT_USAGE`]). + pub output_formats: Vec, + /// Formats usable when DPB and output COINCIDE ([`COINCIDE_USAGE`]). + pub coincide_formats: Vec, +} + +/// Derive the session-shaping facts from one raw AV1 query, for a stream whose +/// sequence header asks for `wanted` ([`Av1ProfileKey::output_format`]). +/// +/// The refusal semantics are the H.265 ones, unchanged: a device advertising AV1 +/// decode but listing no [`crate::P010`] entry under a 10-bit profile yields +/// [`CapsError::NoFormat`] here, with the mode and the format named, and NOTHING +/// is created — a clean pre-session demote to the next ladder rung, never a silent +/// fallback to a format that would lose bits. +pub fn derive_caps_av1(raw: &RawAv1Caps, wanted: vk::Format) -> Result { + let arrangement = derive_arrangement( + raw.capability_flags, + raw.decode_flags, + wanted, + &raw.dpb_formats, + &raw.output_formats, + &raw.coincide_formats, + )?; + Ok(arrangement.into_caps( + raw.min_bitstream_buffer_offset_alignment, + raw.min_bitstream_buffer_size_alignment, + raw.picture_access_granularity, + raw.min_coded_extent, + raw.max_coded_extent, + raw.max_dpb_slots, + raw.max_active_reference_pictures, + MaxLevelIdc::Av1(raw.max_level), + raw.std_header_version, + )) +} + +/// The one function that asks the driver about an AV1 profile: video capabilities +/// (with the decode + AV1 capability structs chained) plus the three +/// format-property enumerations. Copies facts out and returns; derivation happens +/// in [`derive_caps_av1`]. +/// +/// A device that cannot host the profile AT ALL — most importantly the +/// film-grain-enabled one — fails the FIRST call here with a profile-unsupported +/// result, before anything is created. The caller turns that into the ladder's +/// named demote (see [`crate::VkAv1Decoder::probe_stream_support`]). +/// +/// # Safety +/// +/// `dev` wraps live handles per the [`crate::DeviceHandles`] contract (this calls +/// instance-level functions against its physical device). +pub(crate) unsafe fn query_av1_caps( + dev: &DecodeDevice, + key: Av1ProfileKey, +) -> Result { + let mut chain = Av1ProfileChain::new(key); + let profile = chain.wire(); + + let mut av1_caps = vk::VideoDecodeAV1CapabilitiesKHR::default(); + let mut decode_caps = vk::VideoDecodeCapabilitiesKHR::default(); + // ⚠ ORDER IS LOAD-BEARING — see the measured Intel Arc swap in + // [`crate::caps_h265::query_h265_caps`]. `push_next` prepends, so pushing the codec + // struct FIRST leaves VkVideoDecodeCapabilitiesKHR directly after the base struct. + let mut caps = vk::VideoCapabilitiesKHR::default() + .push_next(&mut av1_caps) + .push_next(&mut decode_caps); + // SAFETY: physical device is live (DeviceHandles contract); `profile` roots a + // fully wired, immovable chain; `caps` chains driver-fillable structs that all + // outlive the call. + let r = unsafe { + (dev.video_queue_instance() + .fp() + .get_physical_device_video_capabilities_khr)( + dev.physical_device(), profile, &mut caps + ) + }; + if r != vk::Result::SUCCESS { + return Err(r); + } + // Copy everything out before the chained &mut borrows end (encoder precedent). + let capability_flags = caps.flags; + let min_bitstream_buffer_offset_alignment = caps.min_bitstream_buffer_offset_alignment; + let min_bitstream_buffer_size_alignment = caps.min_bitstream_buffer_size_alignment; + let picture_access_granularity = caps.picture_access_granularity; + let min_coded_extent = caps.min_coded_extent; + let max_coded_extent = caps.max_coded_extent; + let max_dpb_slots = caps.max_dpb_slots; + let max_active_reference_pictures = caps.max_active_reference_pictures; + let std_header_version = caps.std_header_version; + let decode_flags = decode_caps.flags; + let max_level = av1_caps.max_level; + + // The three queries carry the REAL creation usages (SAMPLED included for the + // presenter-facing roles) so the answers validate the images the pools build. + let decode_profile = DecodeProfile::Av1(key); + // SAFETY: same liveness as above; the helper wires its own chain (this and + // the two calls below). + let dpb_formats = unsafe { crate::caps::query_formats(dev, decode_profile, DPB_USAGE)? }; + // SAFETY: as above. + let output_formats = unsafe { crate::caps::query_formats(dev, decode_profile, OUTPUT_USAGE)? }; + // SAFETY: as above. + let coincide_formats = + unsafe { crate::caps::query_formats(dev, decode_profile, COINCIDE_USAGE)? }; + + Ok(RawAv1Caps { + capability_flags, + decode_flags, + min_bitstream_buffer_offset_alignment, + min_bitstream_buffer_size_alignment, + picture_access_granularity, + min_coded_extent, + max_coded_extent, + max_dpb_slots, + max_active_reference_pictures, + max_level, + std_header_version, + dpb_formats, + output_formats, + coincide_formats, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::caps::NV12; + use crate::caps::P010; + use crate::caps::YUV444_10; + use crate::caps::YUV444_8; + + /// A format entry advertising `usage` plus the mutable-format allowance. + fn entry(format: vk::Format, usage: vk::ImageUsageFlags) -> VideoFormat { + VideoFormat { + format, + image_usage: usage, + image_create_flags: vk::ImageCreateFlags::MUTABLE_FORMAT, + ..Default::default() + } + } + + /// RADV's shape (coincide, separate reference images) advertising exactly the + /// formats in `coincide`. + fn coincide_device(coincide: Vec) -> RawAv1Caps { + RawAv1Caps { + capability_flags: vk::VideoCapabilityFlagsKHR::SEPARATE_REFERENCE_IMAGES, + decode_flags: vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_COINCIDE, + min_bitstream_buffer_offset_alignment: 256, + min_bitstream_buffer_size_alignment: 256, + picture_access_granularity: vk::Extent2D { + width: 1, + height: 1, + }, + min_coded_extent: vk::Extent2D { + width: 16, + height: 16, + }, + max_coded_extent: vk::Extent2D { + width: 8192, + height: 8192, + }, + max_dpb_slots: 9, + max_active_reference_pictures: 8, + max_level: hh::StdVideoAV1Level_STD_VIDEO_AV1_LEVEL_5_1, + coincide_formats: coincide, + ..Default::default() + } + } + + #[test] + fn the_profile_is_built_from_the_sequences_sampling_depth_and_grain_flag() { + // Main 4:2:0 8-bit → NV12. + let main = Av1ProfileKey::from_stream(0, 1, 8, false).unwrap(); + assert_eq!(main.std_profile, STD_PROFILE_MAIN); + assert_eq!( + main.chroma_subsampling, + vk::VideoChromaSubsamplingFlagsKHR::TYPE_420 + ); + assert_eq!( + main.luma_bit_depth, + vk::VideoComponentBitDepthFlagsKHR::TYPE_8 + ); + assert_eq!(main.chroma_bit_depth, main.luma_bit_depth); + assert!(!main.film_grain); + assert_eq!(main.output_format(), Some(NV12)); + + // Main 4:2:0 10-bit → P010, and the profile SAYS 10-bit (a profile + // claiming 8 would have the driver hand back an 8-bit surface). + let main10 = Av1ProfileKey::from_stream(0, 1, 10, false).unwrap(); + assert_eq!( + main10.luma_bit_depth, + vk::VideoComponentBitDepthFlagsKHR::TYPE_10 + ); + assert_eq!(main10.output_format(), Some(P010)); + + // High is AV1's 4:4:4 profile, both depths. + let high8 = Av1ProfileKey::from_stream(1, 3, 8, false).unwrap(); + assert_eq!(high8.std_profile, STD_PROFILE_HIGH); + assert_eq!( + high8.chroma_subsampling, + vk::VideoChromaSubsamplingFlagsKHR::TYPE_444 + ); + assert_eq!(high8.output_format(), Some(YUV444_8)); + assert_eq!( + Av1ProfileKey::from_stream(1, 3, 10, false) + .unwrap() + .output_format(), + Some(YUV444_10) + ); + + // The grain flag is part of the PROFILE, so two otherwise identical + // streams are two different profiles — which is exactly what makes the + // caps query a real film-grain probe rather than a formality. + let grainy = Av1ProfileKey::from_stream(0, 1, 8, true).unwrap(); + assert_ne!(grainy, main); + assert!(grainy.film_grain); + assert_eq!( + grainy.output_format(), + main.output_format(), + "grain changes the profile, never the picture format" + ); + } + + #[test] + fn sequence_facts_outside_the_envelope_are_refused_by_the_profile_builder() { + assert_eq!( + Av1ProfileKey::from_stream(3, 1, 8, false).unwrap_err(), + ParamsAv1Error::UnsupportedProfile(3), + "there is no AV1 seq_profile 3" + ); + assert_eq!( + Av1ProfileKey::from_stream(0, 0, 8, false).unwrap_err(), + ParamsAv1Error::UnsupportedChromaFormat(0), + "monochrome has no two-plane picture format here" + ); + assert_eq!( + Av1ProfileKey::from_stream(2, 2, 8, false).unwrap_err(), + ParamsAv1Error::UnsupportedChromaFormat(2), + "4:2:2 is legal AV1 Professional with no punktfunk output plumbing" + ); + assert_eq!( + Av1ProfileKey::from_stream(2, 4, 8, false).unwrap_err(), + ParamsAv1Error::UnsupportedChromaFormat(4), + "the planner's 4:4:0 sentinel is refused, not read as 4:4:4" + ); + assert_eq!( + Av1ProfileKey::from_stream(2, 1, 12, false).unwrap_err(), + ParamsAv1Error::UnsupportedBitDepth(12) + ); + } + + /// The negotiated-facts constructor: the client knows the sampling, depth and + /// grain flag from the host's Welcome long before the first sequence header, + /// and that is enough to PROBE the device before it commits to this rung. + #[test] + fn the_negotiated_shape_picks_the_profile_a_host_encodes_it_with() { + assert_eq!( + Av1ProfileKey::from_negotiated(1, 8, false).unwrap(), + Av1ProfileKey::from_stream(0, 1, 8, false).unwrap() + ); + assert_eq!( + Av1ProfileKey::from_negotiated(1, 10, false).unwrap(), + Av1ProfileKey::from_stream(0, 1, 10, false).unwrap() + ); + assert_eq!( + Av1ProfileKey::from_negotiated(3, 8, true).unwrap(), + Av1ProfileKey::from_stream(1, 3, 8, true).unwrap() + ); + // It never admits what `from_stream` refuses: outside-envelope shapes come + // back typed, so the probe REFUSES rather than guessing a profile. + assert_eq!( + Av1ProfileKey::from_negotiated(0, 8, false).unwrap_err(), + ParamsAv1Error::UnsupportedChromaFormat(0) + ); + assert_eq!( + Av1ProfileKey::from_negotiated(1, 12, false).unwrap_err(), + ParamsAv1Error::UnsupportedBitDepth(12) + ); + } + + #[test] + fn the_av1_profile_chain_wires_the_codec_struct_behind_the_root_profile() { + let key = Av1ProfileKey::from_stream(0, 1, 10, true).unwrap(); + let mut chain = Av1ProfileChain::new(key); + let profile = chain.wire(); + assert_eq!( + profile.video_codec_operation, + vk::VideoCodecOperationFlagsKHR::DECODE_AV1 + ); + assert_eq!( + profile.chroma_subsampling, + vk::VideoChromaSubsamplingFlagsKHR::TYPE_420 + ); + assert_eq!( + profile.luma_bit_depth, + vk::VideoComponentBitDepthFlagsKHR::TYPE_10 + ); + assert!(!profile.p_next.is_null()); + // SAFETY: wire() pointed p_next at chain's own av1 field, which lives for + // this whole scope and is a valid VideoDecodeAV1ProfileInfoKHR. + let av1 = unsafe { + &*profile + .p_next + .cast::>() + }; + assert_eq!(av1.std_profile, STD_PROFILE_MAIN); + assert_eq!( + av1.film_grain_support, + vk::TRUE, + "the query the device answers is the GRAIN-enabled one" + ); + + // A grain-less key states VK_FALSE — the two queries are genuinely + // different questions, which is the whole mechanism. + let plain = Av1ProfileKey::from_stream(0, 1, 10, false).unwrap(); + let mut chain = Av1ProfileChain::new(plain); + let profile = chain.wire(); + // SAFETY: as above. + let av1 = unsafe { + &*profile + .p_next + .cast::>() + }; + assert_eq!(av1.film_grain_support, vk::FALSE); + + // The type-erased dispatch builds the SAME chain (the profile-idc + // confusion `DecodeProfile` exists to prevent would show up right here). + let mut erased = DecodeProfile::Av1(key).chain(); + let profile = erased.wire(); + assert_eq!( + profile.video_codec_operation, + vk::VideoCodecOperationFlagsKHR::DECODE_AV1 + ); + // SAFETY: as above — the erased chain wires its own AV1 struct. + let av1 = unsafe { + &*profile + .p_next + .cast::>() + }; + assert_eq!(av1.film_grain_support, vk::TRUE); + } + + #[test] + fn a_main_stream_derives_nv12_on_a_coincide_device() { + let raw = coincide_device(vec![entry(NV12, COINCIDE_USAGE)]); + let caps = derive_caps_av1(&raw, NV12).unwrap(); + assert!(caps.coincide); + assert!(!caps.layered_dpb); + assert_eq!(caps.output_format, NV12); + assert_eq!(caps.dpb_format, NV12); + assert_eq!( + caps.plane_view_formats, + [vk::Format::R8_UNORM, vk::Format::R8G8_UNORM] + ); + assert_eq!(caps.max_dpb_slots, 9); + assert_eq!(caps.min_bitstream_offset_alignment, 256); + } + + #[test] + fn the_level_ceiling_derived_here_is_tagged_av1_not_another_codec() { + // All three `StdVideo*LevelIdc` types are `c_uint`, and the three code + // spaces disagree (AV1 5.1 is 13, H.265 5.1 is 12, H.264 5.1 is 51). The + // tag is what makes the decoder's numeric gate honest. + let raw = coincide_device(vec![entry(NV12, COINCIDE_USAGE)]); + let caps = derive_caps_av1(&raw, NV12).unwrap(); + assert_eq!( + caps.max_level_idc, + MaxLevelIdc::Av1(hh::StdVideoAV1Level_STD_VIDEO_AV1_LEVEL_5_1) + ); + assert_eq!( + caps.max_level_idc.code_point(), + hh::StdVideoAV1Level_STD_VIDEO_AV1_LEVEL_5_1, + "the gate still compares the raw code point" + ); + assert_ne!( + caps.max_level_idc, + MaxLevelIdc::H265(hh::StdVideoAV1Level_STD_VIDEO_AV1_LEVEL_5_1), + "same number, different codec — not the same ceiling" + ); + } + + #[test] + fn a_ten_bit_stream_on_an_eight_bit_only_device_is_refused_before_any_session() { + // The device decodes AV1 and advertises NV12 — but the stream is 10-bit + // and there is no P010 entry. Refuse by name; do NOT fall back to NV12 + // (that would decode 10-bit content into an 8-bit surface). + let raw = coincide_device(vec![entry(NV12, COINCIDE_USAGE)]); + assert_eq!( + derive_caps_av1(&raw, P010).unwrap_err(), + CapsError::NoFormat { + mode: "coincide (DPB|DST|SAMPLED)", + wanted: P010 + } + ); + + // With the P010 entry present it derives, plane views and all. + let raw = coincide_device(vec![ + entry(NV12, COINCIDE_USAGE), + entry(P010, COINCIDE_USAGE), + ]); + let caps = derive_caps_av1(&raw, P010).unwrap(); + assert_eq!(caps.output_format, P010); + assert_eq!( + caps.plane_view_formats, + [ + vk::Format::R10X6_UNORM_PACK16, + vk::Format::R10X6G10X6_UNORM_2PACK16 + ] + ); + } + + #[test] + fn a_distinct_device_missing_the_format_on_one_half_names_that_half() { + // NVIDIA's shape: distinct only, layered DPB. The DPB half advertises + // P010, the OUTPUT half does not — the refusal must say which. + let raw = RawAv1Caps { + capability_flags: vk::VideoCapabilityFlagsKHR::empty(), + decode_flags: vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_DISTINCT, + dpb_formats: vec![VideoFormat { + format: P010, + image_usage: DPB_USAGE, + image_create_flags: vk::ImageCreateFlags::empty(), + ..Default::default() + }], + output_formats: vec![entry(NV12, OUTPUT_USAGE)], + ..coincide_device(vec![]) + }; + assert_eq!( + derive_caps_av1(&raw, P010).unwrap_err(), + CapsError::NoFormat { + mode: "output (DST|SAMPLED)", + wanted: P010 + } + ); + + // With both halves carrying it, distinct derives (the DPB entry needs + // neither SAMPLED nor MUTABLE_FORMAT — reference images are never sampled). + let raw = RawAv1Caps { + output_formats: vec![entry(P010, OUTPUT_USAGE)], + ..raw + }; + let caps = derive_caps_av1(&raw, P010).unwrap(); + assert!(!caps.coincide); + assert!(caps.layered_dpb); + assert_eq!(caps.output_format, P010); + } + + #[test] + fn an_av1_entry_missing_a_creation_usage_bit_is_refused_naming_the_gap() { + // The Intel-refusal shape, one codec over: the format is listed but not + // for SAMPLED, so the presenter could never read it. + let raw = coincide_device(vec![entry( + NV12, + vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR | vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR, + )]); + assert_eq!( + derive_caps_av1(&raw, NV12).unwrap_err(), + CapsError::UsageUnsupported { + mode: "coincide (DPB|DST|SAMPLED)", + format: NV12, + missing: vk::ImageUsageFlags::SAMPLED + } + ); + + // And a presenter-facing entry without MUTABLE_FORMAT has no plane views. + let raw = coincide_device(vec![VideoFormat { + format: NV12, + image_usage: COINCIDE_USAGE, + image_create_flags: vk::ImageCreateFlags::empty(), + ..Default::default() + }]); + assert_eq!( + derive_caps_av1(&raw, NV12).unwrap_err(), + CapsError::NoMutableFormat { + mode: "coincide (DPB|DST|SAMPLED)", + format: NV12, + } + ); + } + + #[test] + fn an_av1_device_with_no_decode_mode_at_all_is_a_hard_error() { + let mut raw = coincide_device(vec![entry(NV12, COINCIDE_USAGE)]); + raw.decode_flags = vk::VideoDecodeCapabilityFlagsKHR::empty(); + assert_eq!( + derive_caps_av1(&raw, NV12).unwrap_err(), + CapsError::NoDecodeMode + ); + + // Coincide with a layered DPB stays unsupported here too (the picture-pool + // model needs per-slot images, whatever the codec). + let mut raw = coincide_device(vec![entry(NV12, COINCIDE_USAGE)]); + raw.capability_flags = vk::VideoCapabilityFlagsKHR::empty(); + assert_eq!( + derive_caps_av1(&raw, NV12).unwrap_err(), + CapsError::CoincideLayeredDpb + ); + } +} diff --git a/crates/pf-vkdecode/src/caps_h265.rs b/crates/pf-vkdecode/src/caps_h265.rs new file mode 100644 index 00000000..7231d17b --- /dev/null +++ b/crates/pf-vkdecode/src/caps_h265.rs @@ -0,0 +1,802 @@ +//! H.265 decode capability query + derivation — [`crate::caps`] one codec over. +//! +//! Same split as the H.264 side: `query_h265_caps` is the one THIN function that +//! talks to the driver and only COPIES facts into [`RawH265Caps`]; +//! [`derive_caps_h265`] is pure over a hand-buildable struct and shares the whole +//! coincide/distinct/layered decision table with H.264 (`derive_arrangement`, in +//! [`crate::caps`]). +//! +//! What H.265 adds is that the PICTURE FORMAT is no longer a constant. An H.264 +//! session in this program is 8-bit 4:2:0 by envelope, so NV12 is a compile-time +//! fact; an H.265 stream carries its own chroma format and bit depth in the SPS +//! (Main → NV12, Main 10 → P010, RExt 4:4:4 → the two-plane 4:4:4 formats), and +//! those SAME facts must also be stated in the `VkVideoProfileInfoKHR` the session, +//! images, buffers and query pool are all created against. Both therefore come off +//! one [`H265ProfileKey`] built from the stream, and a device that cannot host the +//! combination is refused BEFORE a session exists — the established +//! pre-session-refusal posture (the Intel `DST|SAMPLED`-without-`SAMPLED` case): +//! the ladder demotes to the next decoder rung with a named reason rather than +//! creating images the driver never advertised. + +use ash::vk; +use ash::vk::native as hh; + +use crate::caps::derive_arrangement; +use crate::caps::CapsError; +use crate::caps::DecodeCaps; +use crate::caps::DecodeProfile; +use crate::caps::MaxLevelIdc; +use crate::caps::VideoFormat; +use crate::caps::COINCIDE_USAGE; +use crate::caps::DPB_USAGE; +use crate::caps::NV12; +use crate::caps::OUTPUT_USAGE; +use crate::caps::P010; +use crate::caps::YUV444_10; +use crate::caps::YUV444_8; +use crate::device::DecodeDevice; +use crate::params_h265::profile_to_std; +use crate::params_h265::H265ParamsError; + +/// The stream facts that identify an H.265 decode profile, as Vulkan states them. +/// +/// Every one of these is a `VkVideoProfileInfoKHR` field, and profile identity in +/// Vulkan is BY VALUE across the caps query, the session, every profile-listed +/// image/buffer and the query pool — so this small `Copy` key is what gets passed +/// around, and each consumer rebuilds a structurally identical chain from it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct H265ProfileKey { + /// `StdVideoH265ProfileIdc`: Main (1), Main 10 (2), Main Still Picture (3), + /// Format Range Extensions (4) — the four Vulkan expresses. + pub std_profile_idc: hh::StdVideoH265ProfileIdc, + pub chroma_subsampling: vk::VideoChromaSubsamplingFlagsKHR, + pub luma_bit_depth: vk::VideoComponentBitDepthFlagsKHR, + pub chroma_bit_depth: vk::VideoComponentBitDepthFlagsKHR, +} + +impl H265ProfileKey { + /// Build the key from one picture's stream facts (the SPS's + /// `general_profile_idc`, `chroma_format_idc`, `separate_colour_plane_flag` + /// and the two `bit_depth_*_minus8`). + /// + /// The envelope gate is the SAME one [`crate::params_h265`] applies to the SPS + /// — 4:2:0 or 4:4:4, no separate colour planes, 8 or 10 bits, luma depth == + /// chroma depth — restated here because the profile must be built BEFORE any + /// parameter-set conversion runs (the caps query needs it), and a profile that + /// disagreed with the converted SPS would be a half-truth handed to the + /// driver. Restated in FULL, deliberately: this is a `pub` entry point, and a + /// gate that is "the same one, minus a clause" is how the two drift apart. + pub fn from_stream( + general_profile_idc: u8, + chroma_format_idc: u8, + separate_colour_plane_flag: bool, + bit_depth_luma_minus8: u8, + bit_depth_chroma_minus8: u8, + ) -> Result { + let std_profile_idc = profile_to_std(general_profile_idc)?; + let chroma_subsampling = match chroma_format_idc { + 1 => vk::VideoChromaSubsamplingFlagsKHR::TYPE_420, + 3 => vk::VideoChromaSubsamplingFlagsKHR::TYPE_444, + 0 | 2 => { + return Err(H265ParamsError::UnsupportedChromaFormat(chroma_format_idc)); + } + other => return Err(H265ParamsError::InvalidChromaFormatIdc(other)), + }; + // 4:4:4 with separate colour planes is ChromaArrayType 0 in disguise — + // three monochrome-coded planes — and `TYPE_444` in the profile would be a + // straight lie to the driver about what the bitstream contains. + if chroma_format_idc == 3 && separate_colour_plane_flag { + return Err(H265ParamsError::SeparateColourPlanes); + } + if bit_depth_luma_minus8 != bit_depth_chroma_minus8 + || !matches!(bit_depth_luma_minus8, 0 | 2) + { + return Err(H265ParamsError::UnsupportedBitDepth { + luma_minus8: bit_depth_luma_minus8, + chroma_minus8: bit_depth_chroma_minus8, + }); + } + let depth = if bit_depth_luma_minus8 == 0 { + vk::VideoComponentBitDepthFlagsKHR::TYPE_8 + } else { + vk::VideoComponentBitDepthFlagsKHR::TYPE_10 + }; + Ok(Self { + std_profile_idc, + chroma_subsampling, + luma_bit_depth: depth, + chroma_bit_depth: depth, + }) + } + + /// The key for a stream whose (chroma format, bit depth) the SESSION already + /// negotiated but whose SPS has not arrived yet — the construction-time probe's + /// entry point ([`crate::VkH265Decoder::probe_stream_support`]). + /// + /// The profile idc is the one thing the negotiation does not carry, so it is + /// derived from the pair: 4:2:0 8-bit → Main, 4:2:0 10-bit → Main 10, 4:4:4 → + /// Format Range Extensions (4:4:4 is only expressible in RExt, so that leg is + /// exact — and it is the leg the probe exists for). A stream that turns out to + /// carry a DIFFERENT profile idc for the same pair (RExt 4:2:0, say) simply + /// re-queries under its real key at the first AU: [`Self::from_stream`] stays + /// the authority once the SPS is in hand, and this one never widens what that + /// gate admits — every combination it cannot express is refused here too. + pub fn from_negotiated( + chroma_format_idc: u8, + bit_depth_luma_minus8: u8, + ) -> Result { + let general_profile_idc = match (chroma_format_idc, bit_depth_luma_minus8) { + (1, 0) => 1, + (1, 2) => 2, + (3, _) => 4, + // Everything else is outside the envelope; hand it to `from_stream` + // with a profile that cannot rescue it so ONE gate produces the error. + _ => 4, + }; + Self::from_stream( + general_profile_idc, + chroma_format_idc, + false, + bit_depth_luma_minus8, + bit_depth_luma_minus8, + ) + } + + /// The picture format a session on this profile decodes to, or `None` for a + /// combination outside the envelope (unreachable off [`Self::from_stream`], + /// which already gated it). + pub fn output_format(&self) -> Option { + let ten_bit = self.luma_bit_depth == vk::VideoComponentBitDepthFlagsKHR::TYPE_10; + if self.chroma_subsampling == vk::VideoChromaSubsamplingFlagsKHR::TYPE_420 { + Some(if ten_bit { P010 } else { NV12 }) + } else if self.chroma_subsampling == vk::VideoChromaSubsamplingFlagsKHR::TYPE_444 { + Some(if ten_bit { YUV444_10 } else { YUV444_8 }) + } else { + None + } + } +} + +/// The picture format for one stream's (chroma format, bit depth) pair — the same +/// mapping [`H265ProfileKey::output_format`] applies, reachable without a key. +pub fn output_format_for(chroma_format_idc: u8, bit_depth_luma_minus8: u8) -> Option { + match (chroma_format_idc, bit_depth_luma_minus8) { + (1, 0) => Some(NV12), + (1, 2) => Some(P010), + (3, 0) => Some(YUV444_8), + (3, 2) => Some(YUV444_10), + _ => None, + } +} + +/// A complete H.265 decode profile chain in one movable value — +/// [`crate::caps::H264ProfileChain`]'s twin, with the stream's chroma format and +/// bit depths carried through instead of hard-coded. +/// +/// [`Self::wire`] links `profile.p_next` to this struct's OWN `h265` field; the +/// value must not move between `wire()` and the last use of the returned reference. +pub(crate) struct H265ProfileChain { + h265: vk::VideoDecodeH265ProfileInfoKHR<'static>, + profile: vk::VideoProfileInfoKHR<'static>, +} + +impl H265ProfileChain { + /// Build the (unwired) chain for one stream profile. + pub(crate) fn new(key: H265ProfileKey) -> Self { + Self { + h265: vk::VideoDecodeH265ProfileInfoKHR::default().std_profile_idc(key.std_profile_idc), + profile: vk::VideoProfileInfoKHR::default() + .video_codec_operation(vk::VideoCodecOperationFlagsKHR::DECODE_H265) + .chroma_subsampling(key.chroma_subsampling) + .luma_bit_depth(key.luma_bit_depth) + .chroma_bit_depth(key.chroma_bit_depth), + } + } + + /// Wire the internal `p_next` chain and hand out the profile root. Do not move + /// `self` while the returned reference (or any pointer taken from it) lives. + pub(crate) fn wire(&mut self) -> &vk::VideoProfileInfoKHR<'static> { + self.profile.p_next = (&self.h265 as *const vk::VideoDecodeH265ProfileInfoKHR<'_>).cast(); + &self.profile + } +} + +/// Everything the thin H.265 query copies out of the driver, hand-buildable for +/// tests. Field-for-field [`crate::caps::RawH264Caps`], except `max_level_idc` +/// carries an H.265 Std level code point (a separate type so the two can never be +/// mixed up despite both being `c_uint` underneath). +#[derive(Debug, Clone, Default)] +pub struct RawH265Caps { + /// `VkVideoCapabilitiesKHR::flags`. + pub capability_flags: vk::VideoCapabilityFlagsKHR, + /// `VkVideoDecodeCapabilitiesKHR::flags` (the coincide/distinct advertisement). + pub decode_flags: vk::VideoDecodeCapabilityFlagsKHR, + pub min_bitstream_buffer_offset_alignment: u64, + pub min_bitstream_buffer_size_alignment: u64, + pub picture_access_granularity: vk::Extent2D, + pub min_coded_extent: vk::Extent2D, + pub max_coded_extent: vk::Extent2D, + pub max_dpb_slots: u32, + pub max_active_reference_pictures: u32, + /// `VkVideoDecodeH265CapabilitiesKHR::maxLevelIdc` (index-coded Std level). + pub max_level_idc: hh::StdVideoH265LevelIdc, + /// `VkVideoCapabilitiesKHR::stdHeaderVersion` — session creation echoes it back. + pub std_header_version: vk::ExtensionProperties, + /// Formats usable for DISTINCT-mode DPB images (queried with [`DPB_USAGE`]). + pub dpb_formats: Vec, + /// Formats usable for DISTINCT-mode outputs (queried with [`OUTPUT_USAGE`]). + pub output_formats: Vec, + /// Formats usable when DPB and output COINCIDE ([`COINCIDE_USAGE`]). + pub coincide_formats: Vec, +} + +/// Derive the session-shaping facts from one raw H.265 query, for a stream whose +/// SPS asks for `wanted` ([`H265ProfileKey::output_format`]). +/// +/// The refusal semantics are the point: a device whose driver advertises H.265 +/// decode but lists no [`P010`] entry under a Main 10 profile — or no 4:4:4 entry +/// under a RExt profile — yields [`CapsError::NoFormat`] here, with the mode and +/// the format named, and NOTHING is created. That is a clean pre-session demote to +/// the next ladder rung, not a mid-stream failure and never a silent fallback to a +/// format that would lose bits. +pub fn derive_caps_h265(raw: &RawH265Caps, wanted: vk::Format) -> Result { + let arrangement = derive_arrangement( + raw.capability_flags, + raw.decode_flags, + wanted, + &raw.dpb_formats, + &raw.output_formats, + &raw.coincide_formats, + )?; + Ok(arrangement.into_caps( + raw.min_bitstream_buffer_offset_alignment, + raw.min_bitstream_buffer_size_alignment, + raw.picture_access_granularity, + raw.min_coded_extent, + raw.max_coded_extent, + raw.max_dpb_slots, + raw.max_active_reference_pictures, + MaxLevelIdc::H265(raw.max_level_idc), + raw.std_header_version, + )) +} + +/// The one function that asks the driver about an H.265 profile: video +/// capabilities (with the decode + H.265 capability structs chained) plus the +/// three format-property enumerations. Copies facts out and returns; derivation +/// happens in [`derive_caps_h265`]. +/// +/// # Safety +/// +/// `dev` wraps live handles per the [`crate::DeviceHandles`] contract (this calls +/// instance-level functions against its physical device). +pub(crate) unsafe fn query_h265_caps( + dev: &DecodeDevice, + key: H265ProfileKey, +) -> Result { + let mut chain = H265ProfileChain::new(key); + let profile = chain.wire(); + + let mut h265_caps = vk::VideoDecodeH265CapabilitiesKHR::default(); + let mut decode_caps = vk::VideoDecodeCapabilitiesKHR::default(); + // ⚠ ORDER IS LOAD-BEARING on at least one shipping driver. `push_next` PREPENDS, so + // the chain is the reverse of the call order: pushing the codec struct last puts + // VkVideoDecodeCapabilitiesKHR FIRST after the base struct, which is the order every + // Vulkan sample writes it in. + // + // Measured on Intel Arc (Windows 101.8724) with the previous order — codec struct + // first — the driver filled the two by POSITION rather than by sType and returned + // them SWAPPED: `decode_caps.flags` came back 12 (= STD_VIDEO_H265_LEVEL_IDC_6_2) + // and `h265_caps.maxLevelIdc` came back 1 (= DPB_AND_OUTPUT_COINCIDE). Reading a + // level as a flag bitmask means neither COINCIDE nor DISTINCT appeared set, so the + // rung refused a device that in fact supports it, and every Arc fell back to D3D11VA. + // NVIDIA and RADV dispatch by sType and are indifferent to the order, which is why + // the fleet was green and this survived to the field. + let mut caps = vk::VideoCapabilitiesKHR::default() + .push_next(&mut h265_caps) + .push_next(&mut decode_caps); + // SAFETY: physical device is live (DeviceHandles contract); `profile` roots a + // fully wired, immovable chain; `caps` chains driver-fillable structs that all + // outlive the call. + let r = unsafe { + (dev.video_queue_instance() + .fp() + .get_physical_device_video_capabilities_khr)( + dev.physical_device(), profile, &mut caps + ) + }; + if r != vk::Result::SUCCESS { + return Err(r); + } + // Copy everything out before the chained &mut borrows end (encoder precedent). + let capability_flags = caps.flags; + let min_bitstream_buffer_offset_alignment = caps.min_bitstream_buffer_offset_alignment; + let min_bitstream_buffer_size_alignment = caps.min_bitstream_buffer_size_alignment; + let picture_access_granularity = caps.picture_access_granularity; + let min_coded_extent = caps.min_coded_extent; + let max_coded_extent = caps.max_coded_extent; + let max_dpb_slots = caps.max_dpb_slots; + let max_active_reference_pictures = caps.max_active_reference_pictures; + let std_header_version = caps.std_header_version; + let decode_flags = decode_caps.flags; + let max_level_idc = h265_caps.max_level_idc; + + // What the driver ACTUALLY said, before any of our interpretation. Nothing in this + // module logged, so a refusal downstream ("advertises neither COINCIDE nor DISTINCT") + // was indistinguishable from our own chain never reaching the struct: both present as + // a zero. Printing the BASE capabilities beside the decode ones is the discriminator — + // a populated `max_dpb_slots` next to `decode_flags: 0` means the driver filled the + // chain and genuinely declared no DPB mode; zeros across both mean the query never + // landed. Debug rather than info: one line per profile per session, wanted only when + // someone is asking this exact question. + tracing::debug!( + codec = "H.265", + ?capability_flags, + ?decode_flags, + decode_flags_raw = decode_flags.as_raw(), + max_level_idc, + max_dpb_slots, + max_active_reference_pictures, + ?min_coded_extent, + ?max_coded_extent, + ?picture_access_granularity, + "driver video capabilities, verbatim" + ); + + // The three queries carry the REAL creation usages (SAMPLED included for the + // presenter-facing roles) so the answers validate the images the pools build. + let decode_profile = DecodeProfile::H265(key); + // SAFETY: same liveness as above; the helper wires its own chain (this and + // the two calls below). + let dpb_formats = unsafe { crate::caps::query_formats(dev, decode_profile, DPB_USAGE)? }; + // SAFETY: as above. + let output_formats = unsafe { crate::caps::query_formats(dev, decode_profile, OUTPUT_USAGE)? }; + // SAFETY: as above. + let coincide_formats = + unsafe { crate::caps::query_formats(dev, decode_profile, COINCIDE_USAGE)? }; + + Ok(RawH265Caps { + capability_flags, + decode_flags, + min_bitstream_buffer_offset_alignment, + min_bitstream_buffer_size_alignment, + picture_access_granularity, + min_coded_extent, + max_coded_extent, + max_dpb_slots, + max_active_reference_pictures, + max_level_idc, + std_header_version, + dpb_formats, + output_formats, + coincide_formats, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A format entry advertising `usage` plus the mutable-format allowance. + fn entry(format: vk::Format, usage: vk::ImageUsageFlags) -> VideoFormat { + VideoFormat { + format, + image_usage: usage, + image_create_flags: vk::ImageCreateFlags::MUTABLE_FORMAT, + ..Default::default() + } + } + + /// RADV's shape (coincide, separate reference images) advertising exactly the + /// formats in `coincide`. + fn coincide_device(coincide: Vec) -> RawH265Caps { + RawH265Caps { + capability_flags: vk::VideoCapabilityFlagsKHR::SEPARATE_REFERENCE_IMAGES, + decode_flags: vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_COINCIDE, + min_bitstream_buffer_offset_alignment: 256, + min_bitstream_buffer_size_alignment: 256, + picture_access_granularity: vk::Extent2D { + width: 1, + height: 1, + }, + min_coded_extent: vk::Extent2D { + width: 16, + height: 16, + }, + max_coded_extent: vk::Extent2D { + width: 8192, + height: 8192, + }, + max_dpb_slots: 17, + max_active_reference_pictures: 16, + max_level_idc: hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_6_2, + coincide_formats: coincide, + ..Default::default() + } + } + + #[test] + fn the_profile_is_built_from_the_streams_chroma_format_and_bit_depth() { + // Main: 4:2:0 8-bit → NV12. + let main = H265ProfileKey::from_stream(1, 1, false, 0, 0).unwrap(); + assert_eq!( + main.std_profile_idc, + hh::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_MAIN + ); + assert_eq!( + main.chroma_subsampling, + vk::VideoChromaSubsamplingFlagsKHR::TYPE_420 + ); + assert_eq!( + main.luma_bit_depth, + vk::VideoComponentBitDepthFlagsKHR::TYPE_8 + ); + assert_eq!(main.output_format(), Some(NV12)); + + // Main 10: 4:2:0 10-bit → P010, and the profile SAYS 10-bit (a profile + // claiming 8 would have the driver hand back an 8-bit surface). + let main10 = H265ProfileKey::from_stream(2, 1, false, 2, 2).unwrap(); + assert_eq!( + main10.std_profile_idc, + hh::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_MAIN_10 + ); + assert_eq!( + main10.luma_bit_depth, + vk::VideoComponentBitDepthFlagsKHR::TYPE_10 + ); + assert_eq!(main10.chroma_bit_depth, main10.luma_bit_depth); + assert_eq!(main10.output_format(), Some(P010)); + + // RExt 4:4:4, both depths. + let rext8 = H265ProfileKey::from_stream(4, 3, false, 0, 0).unwrap(); + assert_eq!( + rext8.chroma_subsampling, + vk::VideoChromaSubsamplingFlagsKHR::TYPE_444 + ); + assert_eq!(rext8.output_format(), Some(YUV444_8)); + let rext10 = H265ProfileKey::from_stream(4, 3, false, 2, 2).unwrap(); + assert_eq!(rext10.output_format(), Some(YUV444_10)); + + // The free function agrees with the key's own mapping, combination for + // combination. + for (chroma, depth, format) in [ + (1u8, 0u8, NV12), + (1, 2, P010), + (3, 0, YUV444_8), + (3, 2, YUV444_10), + ] { + assert_eq!(output_format_for(chroma, depth), Some(format)); + } + assert_eq!(output_format_for(2, 0), None, "4:2:2 has no output format"); + } + + /// The negotiated-facts constructor: the session knows the chroma format and + /// bit depth from the host's Welcome long before the first SPS, and that is + /// enough to pick the profile a punktfunk host encodes the pair with — which + /// is what lets the client PROBE the device before it commits to the native + /// decoder rung. + #[test] + fn the_negotiated_pair_picks_the_profile_a_host_encodes_it_with() { + let main = H265ProfileKey::from_negotiated(1, 0).unwrap(); + assert_eq!( + main, + H265ProfileKey::from_stream(1, 1, false, 0, 0).unwrap() + ); + assert_eq!(main.output_format(), Some(NV12)); + + let main10 = H265ProfileKey::from_negotiated(1, 2).unwrap(); + assert_eq!( + main10, + H265ProfileKey::from_stream(2, 1, false, 2, 2).unwrap() + ); + assert_eq!(main10.output_format(), Some(P010)); + + // 4:4:4 is only expressible in RExt, so this leg is exact — and it is the + // one the probe exists for (a 4:4:4 session on a device with no 4:4:4 + // decode format used to burn the ladder mid-stream). + let rext8 = H265ProfileKey::from_negotiated(3, 0).unwrap(); + assert_eq!( + rext8, + H265ProfileKey::from_stream(4, 3, false, 0, 0).unwrap() + ); + assert_eq!(rext8.output_format(), Some(YUV444_8)); + let rext10 = H265ProfileKey::from_negotiated(3, 2).unwrap(); + assert_eq!(rext10.output_format(), Some(YUV444_10)); + + // It never admits what `from_stream` refuses: outside-envelope pairs come + // back typed, so the probe REFUSES rather than guessing a profile. + assert_eq!( + H265ProfileKey::from_negotiated(2, 0).unwrap_err(), + H265ParamsError::UnsupportedChromaFormat(2) + ); + assert_eq!( + H265ProfileKey::from_negotiated(0, 0).unwrap_err(), + H265ParamsError::UnsupportedChromaFormat(0) + ); + assert_eq!( + H265ProfileKey::from_negotiated(1, 4).unwrap_err(), + H265ParamsError::UnsupportedBitDepth { + luma_minus8: 4, + chroma_minus8: 4 + } + ); + } + + #[test] + fn stream_facts_outside_the_envelope_are_refused_by_the_profile_builder() { + assert_eq!( + H265ProfileKey::from_stream(9, 1, false, 0, 0).unwrap_err(), + H265ParamsError::UnmappableProfileIdc(9), + "High Throughput/SCC profiles have no Vulkan code point" + ); + assert_eq!( + H265ProfileKey::from_stream(1, 2, false, 0, 0).unwrap_err(), + H265ParamsError::UnsupportedChromaFormat(2), + "4:2:2 is legal H.265 with no punktfunk output plumbing" + ); + assert_eq!( + H265ProfileKey::from_stream(1, 0, false, 0, 0).unwrap_err(), + H265ParamsError::UnsupportedChromaFormat(0) + ); + assert_eq!( + H265ProfileKey::from_stream(1, 4, false, 0, 0).unwrap_err(), + H265ParamsError::InvalidChromaFormatIdc(4) + ); + // 4:4:4 with separate colour planes is ChromaArrayType 0 in disguise: + // params_h265's check_envelope refuses it, and so must this — building the + // key WOULD otherwise put TYPE_444 in the profile and tell the driver the + // bitstream carries interleaved 4:4:4 chroma it does not have. (Unreachable + // through `decode`, which never gets past the planner; reachable through + // this `pub` constructor, which is the point.) + assert_eq!( + H265ProfileKey::from_stream(4, 3, true, 0, 0).unwrap_err(), + H265ParamsError::SeparateColourPlanes + ); + // The flag is only meaningful at 4:4:4 (7.4.3.2.1) — it does not disturb + // the 4:2:0 path. + assert!(H265ProfileKey::from_stream(1, 1, true, 0, 0).is_ok()); + assert_eq!( + H265ProfileKey::from_stream(4, 1, false, 4, 4).unwrap_err(), + H265ParamsError::UnsupportedBitDepth { + luma_minus8: 4, + chroma_minus8: 4 + }, + "12-bit has no output format" + ); + assert_eq!( + H265ProfileKey::from_stream(4, 1, false, 0, 2).unwrap_err(), + H265ParamsError::UnsupportedBitDepth { + luma_minus8: 0, + chroma_minus8: 2 + }, + "disagreeing luma/chroma depths have no output format" + ); + } + + #[test] + fn the_h265_profile_chain_wires_the_codec_struct_behind_the_root_profile() { + let key = H265ProfileKey::from_stream(2, 1, false, 2, 2).unwrap(); + let mut chain = H265ProfileChain::new(key); + let profile = chain.wire(); + assert_eq!( + profile.video_codec_operation, + vk::VideoCodecOperationFlagsKHR::DECODE_H265 + ); + assert_eq!( + profile.chroma_subsampling, + vk::VideoChromaSubsamplingFlagsKHR::TYPE_420 + ); + assert_eq!( + profile.luma_bit_depth, + vk::VideoComponentBitDepthFlagsKHR::TYPE_10 + ); + assert!(!profile.p_next.is_null()); + // SAFETY: wire() pointed p_next at chain's own h265 field, which lives for + // this whole scope and is a valid VideoDecodeH265ProfileInfoKHR. + let h265 = unsafe { + &*profile + .p_next + .cast::>() + }; + assert_eq!( + h265.std_profile_idc, + hh::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_MAIN_10 + ); + + // The type-erased dispatch builds the SAME chain (the H.264/H.265 idc + // confusion this enum exists to prevent would show up right here). + let mut erased = DecodeProfile::H265(key).chain(); + let profile = erased.wire(); + assert_eq!( + profile.video_codec_operation, + vk::VideoCodecOperationFlagsKHR::DECODE_H265 + ); + } + + #[test] + fn a_main_stream_derives_nv12_on_a_coincide_device() { + let raw = coincide_device(vec![entry(NV12, COINCIDE_USAGE)]); + let caps = derive_caps_h265(&raw, NV12).unwrap(); + assert!(caps.coincide); + assert!(!caps.layered_dpb); + assert_eq!(caps.output_format, NV12); + assert_eq!(caps.dpb_format, NV12); + assert_eq!( + caps.plane_view_formats, + [vk::Format::R8_UNORM, vk::Format::R8G8_UNORM] + ); + assert_eq!(caps.max_dpb_slots, 17); + assert_eq!(caps.min_bitstream_offset_alignment, 256); + } + + #[test] + fn the_level_ceiling_derived_here_is_tagged_h265_not_h264() { + // `StdVideoH264LevelIdc` and `StdVideoH265LevelIdc` are both `c_uint`, so + // an H.265 ceiling landing in an H.264-typed field used to compile in + // silence — and the two code spaces do NOT agree (H.265 6.2 is 15, H.264 + // 6.2 is 19). The tag is what makes the decoders' numeric gate honest. + let raw = coincide_device(vec![entry(NV12, COINCIDE_USAGE)]); + let caps = derive_caps_h265(&raw, NV12).unwrap(); + assert_eq!( + caps.max_level_idc, + MaxLevelIdc::H265(hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_6_2) + ); + assert_eq!( + caps.max_level_idc.code_point(), + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_6_2, + "the gate still compares the raw code point" + ); + assert_ne!( + caps.max_level_idc, + MaxLevelIdc::H264(hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_6_2), + "same number, different codec — not the same ceiling" + ); + } + + #[test] + fn a_main10_stream_on_an_eight_bit_only_device_is_refused_before_any_session() { + // The device decodes H.265 and advertises NV12 — but the stream is 10-bit + // and there is no P010 entry. Refuse by name; do NOT fall back to NV12 + // (that would decode 10-bit content into an 8-bit surface). + let raw = coincide_device(vec![entry(NV12, COINCIDE_USAGE)]); + assert_eq!( + derive_caps_h265(&raw, P010).unwrap_err(), + CapsError::NoFormat { + mode: "coincide (DPB|DST|SAMPLED)", + wanted: P010 + } + ); + + // With the P010 entry present it derives, plane views and all. + let raw = coincide_device(vec![ + entry(NV12, COINCIDE_USAGE), + entry(P010, COINCIDE_USAGE), + ]); + let caps = derive_caps_h265(&raw, P010).unwrap(); + assert_eq!(caps.output_format, P010); + assert_eq!( + caps.plane_view_formats, + [ + vk::Format::R10X6_UNORM_PACK16, + vk::Format::R10X6G10X6_UNORM_2PACK16 + ] + ); + } + + #[test] + fn a_444_stream_is_refused_where_caps_stop_at_420_and_derives_where_they_do_not() { + let raw = coincide_device(vec![ + entry(NV12, COINCIDE_USAGE), + entry(P010, COINCIDE_USAGE), + ]); + assert_eq!( + derive_caps_h265(&raw, YUV444_8).unwrap_err(), + CapsError::NoFormat { + mode: "coincide (DPB|DST|SAMPLED)", + wanted: YUV444_8 + } + ); + + let raw = coincide_device(vec![ + entry(NV12, COINCIDE_USAGE), + entry(YUV444_10, COINCIDE_USAGE), + ]); + let caps = derive_caps_h265(&raw, YUV444_10).unwrap(); + assert_eq!(caps.output_format, YUV444_10); + assert_eq!( + caps.plane_view_formats, + [ + vk::Format::R10X6_UNORM_PACK16, + vk::Format::R10X6G10X6_UNORM_2PACK16 + ] + ); + } + + #[test] + fn a_distinct_device_missing_the_format_on_one_half_names_that_half() { + // NVIDIA's shape: distinct only, layered DPB. The DPB half advertises + // P010, the OUTPUT half does not — the refusal must say which. + let raw = RawH265Caps { + capability_flags: vk::VideoCapabilityFlagsKHR::empty(), + decode_flags: vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_DISTINCT, + dpb_formats: vec![VideoFormat { + format: P010, + image_usage: DPB_USAGE, + image_create_flags: vk::ImageCreateFlags::empty(), + ..Default::default() + }], + output_formats: vec![entry(NV12, OUTPUT_USAGE)], + ..coincide_device(vec![]) + }; + assert_eq!( + derive_caps_h265(&raw, P010).unwrap_err(), + CapsError::NoFormat { + mode: "output (DST|SAMPLED)", + wanted: P010 + } + ); + + // With both halves carrying it, distinct derives (the DPB entry needs + // neither SAMPLED nor MUTABLE_FORMAT — reference images are never sampled). + let raw = RawH265Caps { + output_formats: vec![entry(P010, OUTPUT_USAGE)], + ..raw + }; + let caps = derive_caps_h265(&raw, P010).unwrap(); + assert!(!caps.coincide); + assert!(caps.layered_dpb); + assert_eq!(caps.output_format, P010); + } + + #[test] + fn an_h265_entry_missing_a_creation_usage_bit_is_refused_naming_the_gap() { + // The Intel-refusal shape, one codec over: the format is listed but not + // for SAMPLED, so the presenter could never read it. + let raw = coincide_device(vec![entry( + P010, + vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR | vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR, + )]); + assert_eq!( + derive_caps_h265(&raw, P010).unwrap_err(), + CapsError::UsageUnsupported { + mode: "coincide (DPB|DST|SAMPLED)", + format: P010, + missing: vk::ImageUsageFlags::SAMPLED + } + ); + + // And a presenter-facing entry without MUTABLE_FORMAT has no plane views. + let raw = coincide_device(vec![VideoFormat { + format: P010, + image_usage: COINCIDE_USAGE, + image_create_flags: vk::ImageCreateFlags::empty(), + ..Default::default() + }]); + assert_eq!( + derive_caps_h265(&raw, P010).unwrap_err(), + CapsError::NoMutableFormat { + mode: "coincide (DPB|DST|SAMPLED)", + format: P010, + } + ); + } + + #[test] + fn an_h265_device_with_no_decode_mode_at_all_is_a_hard_error() { + let mut raw = coincide_device(vec![entry(NV12, COINCIDE_USAGE)]); + raw.decode_flags = vk::VideoDecodeCapabilityFlagsKHR::empty(); + assert_eq!( + derive_caps_h265(&raw, NV12).unwrap_err(), + CapsError::NoDecodeMode + ); + + // Coincide with a layered DPB stays unsupported here too (the picture-pool + // model needs per-slot images, whatever the codec). + let mut raw = coincide_device(vec![entry(NV12, COINCIDE_USAGE)]); + raw.capability_flags = vk::VideoCapabilityFlagsKHR::empty(); + assert_eq!( + derive_caps_h265(&raw, NV12).unwrap_err(), + CapsError::CoincideLayeredDpb + ); + } +} diff --git a/crates/pf-vkdecode/src/decoder.rs b/crates/pf-vkdecode/src/decoder.rs new file mode 100644 index 00000000..ac417aa5 --- /dev/null +++ b/crates/pf-vkdecode/src/decoder.rs @@ -0,0 +1,2076 @@ +//! [`VkH264Decoder`]: the assembled native decoder — pf-bitstream's planner and +//! WP-A's conversions driving a Vulkan Video session end to end. +//! +//! Per AU: `plan_au` → `plan_to_vk` → AU upload into the bitstream ring → record +//! (barriers, `vkCmdBeginVideoCodingKHR` with every bound DPB slot, the one-time +//! session RESET control, a `RESULT_STATUS_ONLY` query bracketing +//! `vkCmdDecodeVideoKHR`) → submit on the decode queue under the caller's +//! [`QueueLock`] with a per-image timeline signal. +//! +//! **Image model (zero-copy, the FFmpeg pool contract):** decode targets come from +//! a picture pool DECOUPLED from DPB slots ([`crate::images`] module docs) — a +//! slot binds a fresh free image at activation, so a delivered picture is never a +//! decode target while the consumer reads it. Each image's own timeline semaphore +//! carries the AVVkFrame hand-off: the decoder signals `value+1` at decode-write, +//! the presenter waits it, samples, restores the layout and signals `value+1` +//! again in its own submission; [`VkH264Decoder::release_frame`] reports that +//! write-back and the decoder waits it before the image's next use — presenter +//! layout traffic is ordered against decode reads without any copy. +//! +//! The status query is THE point of this program: FFmpeg's `vulkan_decode.c` runs +//! `nb_queries = 0` and therefore architecturally cannot see driver-reported decode +//! corruption (the Xbox Ally X field case). Here every decode op has a query slot, +//! [`VkH264Decoder::poll_status`] reads it WITHOUT waiting, and a non-COMPLETE +//! result is the concealment signal the integration layer wires to +//! `want_keyframe`. +//! +//! Known residual (WP-D on-glass, same class as the shipping AVVkFrame arm): a +//! delivered frame whose picture is STILL a live reference can be sampled by the +//! presenter while a decode references it — reads on both sides, but the +//! presenter's layout round-trip writes metadata. `VK_KHR_unified_image_layouts` +//! (GENERAL everywhere) removes the round-trip entirely and is the documented +//! fast-path TODO once the fleet's drivers carry it. + +use std::collections::BTreeMap; +use std::collections::VecDeque; + +use ash::vk; +use ash::vk::native as hh; +use pf_bitstream::h264::AuPlan; +use pf_bitstream::h264::ColourDescription; +use pf_bitstream::h264::DisplayCrop; +use pf_bitstream::h264::DpbUpdate; +use pf_bitstream::h264::H264Planner; +use pf_bitstream::h264::PicId; +use pf_bitstream::h264::PlanError; +use pf_bitstream::h264::PlanWarning; +use tracing::debug; +use tracing::trace; + +use crate::caps::derive_caps; +use crate::caps::query_h264_caps; +use crate::caps::CapsError; +use crate::caps::DecodeCaps; +use crate::caps::DecodeProfile; +use crate::device::AllocError; +use crate::device::DecodeDevice; +use crate::device::DeviceError; +use crate::device::DeviceHandles; +use crate::device::QueueLock; +use crate::device::QueueSubmitGuard; +use crate::images::plan_pools; +use crate::images::DpbPool; +use crate::images::PicturePool; +use crate::images::HOLD_HEADROOM; +use crate::params::level_to_std; +use crate::params::ParamsError; +use crate::params_av1::ParamsAv1Error; +use crate::params_h265::H265ParamsError; +use crate::pic::plan_to_vk; +use crate::pic::DecodePlanVk; +use crate::pic::PlanToVkError; +use crate::pic_av1::PlanToVkAv1Error; +use crate::pic_h265::PlanToVkH265Error; +use crate::ring::pack_slices; +use crate::ring::BitstreamRing; +use crate::ring::RingLayout; +use crate::ring::UploadedAu; +use crate::ring::INITIAL_SLOT_SIZE; +use crate::ring::RING_SLOTS; +use crate::session::ParamsAction; +use crate::session::SessionConfig; +use crate::session::SessionError; +use crate::session::VideoSession; +use crate::slots::SlotMap; + +/// Ceiling on any blocking GPU wait on the decode thread (5 s) — generous against +/// a real decode, finite against a wedged driver, matching the encoder's fence +/// budget so the session layer's recovery path is never parked forever. +const DECODE_TIMEOUT_NS: u64 = 5_000_000_000; + +/// Result of one decode op's `RESULT_STATUS_ONLY` query. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecodeStatus { + /// The op has not completed (or its status is not yet readable). + Pending, + /// The driver reports the op COMPLETE. + Ok, + /// The driver reports an error status, the query slot was recycled before it + /// was read, or the device is lost — in every case the frame's content is + /// unproven and the caller should treat it as concealed (want_keyframe). + Failed, +} + +/// One decoded, display-ready picture over a pool image the decoder will not +/// touch again until [`VkH264Decoder::release_frame`] returns it. +/// +/// Sync contract (the AVVkFrame shape): pixels are ready when `semaphore` +/// reaches [`Self::value`]. A consumer that SAMPLES the image must, in the same +/// submission that waits `value`, signal `value + 1` after its reads (and layout +/// restore) — and report that via `release_frame(frame, true)`; a consumer that +/// drops the frame unsampled releases with `false`. Handles survive session +/// rebuilds via the graveyard: release every frame exactly once, even +/// stale-generation ones. +#[derive(Debug, Clone)] +pub struct DecodedVkFrame { + pub image: vk::Image, + /// The picture format the image was created with, and the format + /// [`Self::view`] aliases — the caps-resolved `output_format` of the session + /// that decoded it. + /// + /// Read it; do NOT assume it. H.264 in this program is 8-bit 4:2:0 by + /// envelope, so the format is [`crate::NV12`] on every H.264 frame — but an + /// H.265 session's picture format is the STREAM's: Main → [`crate::NV12`], + /// Main 10 → [`crate::P010`], RExt 4:4:4 → [`crate::YUV444_8`] / + /// [`crate::YUV444_10`], and it can change MID-STREAM when the host + /// renegotiates (a new generation, a new pool). A consumer that hard-codes + /// 8-bit 4:2:0 decodes a Main 10 picture correctly and then renders it with + /// 8-bit transfer/range math, and gives a 4:4:4 picture 4:2:0 UV scaling — + /// both plausible-looking and wrong, the class this crate exists to refuse. + /// [`crate::plane_formats`] maps this to the per-plane view formats + /// [`Self::plane_views`] carry. + pub format: vk::Format, + /// Full-picture view of [`Self::format`] (`COLOR` aspect, all planes). + pub view: vk::ImageView, + /// Per-plane views for the presenter's sampler path, in the formats + /// [`crate::plane_formats`] resolves for [`Self::format`]: `R8`/`R8G8` for + /// the 8-bit families, `R10X6`/`R10X6G10X6` for the 10-bit ones. + pub plane_views: [vk::ImageView; 2], + /// Always 0 — pool images are single-layer (kept for the consumer ABI). + pub layer: u32, + /// The layout the picture is in when the semaphore signals — and the layout + /// the consumer must RESTORE after sampling: `VIDEO_DECODE_DPB_KHR` + /// (coincide) or `VIDEO_DECODE_DST_KHR` (distinct). + pub layout: vk::ImageLayout, + /// The ALLOCATED picture extent (`pictureAccessGranularity`-aligned) — what + /// UV-scale math must divide by (the 1088-row class); the DISPLAY region is + /// [`Self::crop`]. + pub coded_width: u32, + pub coded_height: u32, + /// Conformance-window crop: the region to display. + pub crop: DisplayCrop, + /// Colour signalling from the picture's ACTIVE SPS (pf-bitstream applies + /// E.2.1's "unspecified" inference where the VUI is silent). Per frame, like + /// [`Self::crop`]: the host switches HDR in-band with a new SPS mid-stream. + pub colour: ColourDescription, + /// Timeline pair: pixels ready at `semaphore >= value`; the sampling + /// consumer signals `value + 1` (see the type docs). + pub semaphore: vk::Semaphore, + pub value: u64, + pub poc: i32, + pub is_idr: bool, + /// What the recovery point SEI of this picture's AU (and any outstanding one + /// before it) is worth — see [`crate::recovery`]. `RecoveryMark::NONE` on every + /// picture of a stream that carries no recovery point SEI, which is every + /// punktfunk host today that is not running an NVENC intra-refresh wave. + /// + /// It exists because [`Self::is_idr`] cannot answer for an intra-refresh + /// session: the wave never emits an IDR, so a consumer freezing on loss has no + /// decoder-visible clean point and holds the last good picture until its + /// backstop forces the very IDR the wave exists to avoid. The mark is the + /// stream saying, in-band, where it healed. + pub recovery: crate::recovery::RecoveryMark, + /// This picture's position in DECODE order: a strictly increasing per-decoder + /// ordinal stamped when the AU was planned (1 for the first picture of the + /// decoder's life; it survives session rebuilds, because it describes the + /// STREAM, not the Vulkan objects). + /// + /// It exists because delivery order is not decode order, and a consumer + /// pairing [`Self::recovery`] against its own loss needs to know which of the + /// two a frame belongs to. A post-failure DPB flush hands back every picture + /// still buffered — pictures decoded BEFORE the loss — and each carries the + /// recovery marks of the wave it was decoded in. Delivered after the + /// consumer armed its freeze, those marks read as a heal that happened after + /// the loss, and lift a freeze on a wave that completed before it. Comparing + /// this ordinal against the one current at the arm is what tells them apart. + pub decode_order: u64, + /// The decode op's slot in the status query pool. + pub query_slot: u32, + /// The decode op's submission ordinal (validates the query slot has not been + /// re-armed since). + pub submission: u64, + /// The pool index of the image (release bookkeeping). + pub picture: u32, + /// The session generation this frame belongs to (graveyard routing). + pub generation: u64, +} + +/// Everything that can go wrong. Never panics; device loss is first-class so the +/// session layer can tear down and rebuild. +#[derive(Debug)] +pub enum VkDecodeError { + /// pf-bitstream could not plan the AU at all. + Plan(PlanError), + /// [`VkDecodeError::Plan`]'s H.265 counterpart. Note what is NOT here: + /// `h265::PlanError::RaslSkipped` never becomes an error — the H.265 decoder + /// answers `Ok(None)` for a RASL picture after an open-GOP join, because it is + /// undecodable by definition and the next AU plans normally. + PlanH265(pf_bitstream::h265::PlanError), + /// A parameter set has no Std representation (stream-integrity failure). + Params(ParamsError), + /// An H.265 parameter set has no Std representation, or the stream sits + /// outside the H.265 decode envelope (chroma format / bit depth / profile) — + /// a stream-integrity failure, refused rather than half-converted. + ParamsH265(H265ParamsError), + /// [`VkDecodeError::Plan`]'s AV1 counterpart. + PlanAv1(pf_bitstream::av1::PlanError), + /// [`VkDecodeError::Convert`]'s AV1 counterpart. + ConvertAv1(PlanToVkAv1Error), + /// An AV1 sequence header has no Std representation, or the stream sits + /// outside the AV1 decode envelope (sampling / bit depth / profile). + ParamsAv1(ParamsAv1Error), + /// The AV1 access unit's tile groups could not be split into the per-tile + /// byte ranges `VkVideoDecodeAV1PictureInfoKHR::pTileOffsets` wants — a + /// malformed or unexpected OBU. Refused rather than submitted with the whole + /// OBU standing in for its tiles ([`crate::decoder_av1`]). + TilesAv1(crate::decoder_av1::Av1TileError), + /// An AV1 frame named a reference slot the planner's store no longer holds. + /// + /// Fatal rather than degraded, and for a sharper reason than "the picture + /// would be wrong": the planner COMPACTS the surviving references into + /// `AuPlan::refs`, so the seven AV1 reference NAMES stop lining up with that + /// list the moment one is lost — every later name would resolve to the wrong + /// picture, which is the plausible-looking corruption this crate refuses to + /// produce. `ref_index` is the AV1 reference name (`LAST_FRAME` = 0 through + /// `ALTREF_FRAME` = 6), `slot` the reference slot it pointed at. + MissingReferenceAv1 { slot: u8, ref_index: u8 }, + /// Every frame of this AV1 temporal unit was SKIPPED because the decoder is + /// waiting for the next key frame after a failure — nothing decoded, nothing + /// displayed. + /// + /// AV1's answer to [`pf_bitstream::h264::PlanError::AwaitingIdr`], and + /// deliberately the same KIND of answer: an error, once per access unit, for + /// as long as the wait lasts. The AV1 planner has no `flush`, so the wait is + /// held in [`crate::VkAv1Decoder`] rather than in the planner — but a consumer + /// must not be able to tell the two codecs apart here, because the consumer's + /// demotion streak is what turns "this rung produces no picture" into "fall + /// through to the next rung". Answering the wait with a clean `Ok(None)` + /// instead RESETS that streak once per frame, and a rung whose every key frame + /// fails (a film-grain sequence on a device without the grain profile, a level + /// above `maxLevelIdc`, a sequence header disagreeing with the negotiation) + /// then never demotes at all: one error per key frame, cleared by the inter + /// frames between them, and a frozen screen for the whole session. + /// + /// A key frame ANYWHERE in the unit clears the wait and decodes, so this is + /// returned only when the unit produced nothing at all. + AwaitingKeyAv1, + /// Plan-to-Vulkan conversion failed (caller/session bugs; `CapacityMismatch` + /// is consumed internally by the rebuild path and only surfaces if the rebuilt + /// session STILL mismatches). + Convert(PlanToVkError), + /// [`VkDecodeError::Convert`]'s H.265 counterpart. + ConvertH265(PlanToVkH265Error), + /// The device's capabilities cannot host any session (demote to the next rung). + Caps(CapsError), + /// The handle bundle was rejected. + Device(DeviceError), + /// The stream asks for more than this device's caps allow. + Unsupported(String), + /// A Vulkan call failed (anything but device loss). + Vk(vk::Result), + /// `VK_ERROR_DEVICE_LOST` — every later call fails fast with this until the + /// owner rebuilds on fresh handles. + DeviceLost, + /// A bounded GPU wait expired: the driver is wedged; treat as fatal for this + /// decoder instance. + Timeout(&'static str), + /// The picture pool is exhausted: the consumer holds more than + /// [`HOLD_HEADROOM`] unreleased frames while the stream's whole DPB depth is + /// live — a real backpressure fault worth surfacing (the pool is sized so a + /// correct consumer can never hit this). The AU was planned but NOT decoded; + /// release frames and request a keyframe. + NoFreeSlot, + /// A DPB slot this AU references holds no bound image. H.265 only, and fatal + /// rather than skippable: `StdVideoDecodeH265PictureInfo`'s RPS arrays are + /// INDICES into `pReferenceSlots`, so dropping one entry would silently + /// re-point every later index at the wrong picture — the exact class of + /// plausible-looking corruption this crate refuses to produce. (H.264 carries + /// no such index arrays and only traces the case.) + UnboundReferenceSlot { slot: u8 }, + /// The frame belongs to a generation whose retired pool is already gone + /// (double release, or a frame outliving its graveyard entry). + StaleFrame { + frame_generation: u64, + current_generation: u64, + }, + /// No device memory type satisfies an allocation's requirements. + NoMemoryType { + type_bits: u32, + flags: vk::MemoryPropertyFlags, + }, +} + +impl std::fmt::Display for VkDecodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VkDecodeError::Plan(e) => write!(f, "AU planning failed: {e}"), + VkDecodeError::PlanH265(e) => write!(f, "H.265 AU planning failed: {e}"), + VkDecodeError::Params(e) => write!(f, "parameter-set conversion failed: {e}"), + VkDecodeError::ParamsH265(e) => { + write!(f, "H.265 parameter-set conversion failed: {e}") + } + VkDecodeError::PlanAv1(e) => write!(f, "AV1 AU planning failed: {e}"), + VkDecodeError::ConvertAv1(e) => write!(f, "AV1 plan conversion failed: {e}"), + VkDecodeError::ParamsAv1(e) => { + write!(f, "AV1 sequence-header conversion failed: {e}") + } + VkDecodeError::TilesAv1(e) => write!(f, "AV1 tile split failed: {e}"), + VkDecodeError::MissingReferenceAv1 { slot, ref_index } => { + write!( + f, + "AV1 reference name {ref_index} points at slot {slot}, which holds \ + no picture — the surviving references would renumber" + ) + } + VkDecodeError::AwaitingKeyAv1 => write!( + f, + "every frame of this AV1 temporal unit was skipped — the decoder is \ + waiting for the next key frame after a failure" + ), + VkDecodeError::Convert(e) => write!(f, "plan conversion failed: {e}"), + VkDecodeError::ConvertH265(e) => write!(f, "H.265 plan conversion failed: {e}"), + VkDecodeError::Caps(e) => write!(f, "decode capabilities unusable: {e}"), + VkDecodeError::Device(e) => write!(f, "device handles rejected: {e}"), + VkDecodeError::Unsupported(what) => write!(f, "outside device caps: {what}"), + VkDecodeError::Vk(r) => write!(f, "Vulkan call failed: {r:?}"), + VkDecodeError::DeviceLost => write!(f, "VK_ERROR_DEVICE_LOST"), + VkDecodeError::Timeout(what) => { + write!(f, "GPU wait expired after {DECODE_TIMEOUT_NS} ns: {what}") + } + VkDecodeError::NoFreeSlot => { + write!( + f, + "picture pool exhausted — more than {HOLD_HEADROOM} delivered frames \ + are unreleased (release_frame owed)" + ) + } + VkDecodeError::UnboundReferenceSlot { slot } => { + write!( + f, + "DPB slot {slot} is referenced by this AU but binds no image — \ + the H.265 RPS index arrays would point at the wrong pictures" + ) + } + VkDecodeError::StaleFrame { + frame_generation, + current_generation, + } => { + write!( + f, + "frame from session generation {frame_generation} (current \ + {current_generation}) has no retired pool — double release?" + ) + } + VkDecodeError::NoMemoryType { type_bits, flags } => { + write!( + f, + "no memory type satisfies bits {type_bits:#x} with {flags:?}" + ) + } + } + } +} + +impl std::error::Error for VkDecodeError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + VkDecodeError::Plan(e) => Some(e), + VkDecodeError::PlanH265(e) => Some(e), + VkDecodeError::Params(e) => Some(e), + VkDecodeError::ParamsH265(e) => Some(e), + VkDecodeError::Convert(e) => Some(e), + VkDecodeError::ConvertH265(e) => Some(e), + VkDecodeError::PlanAv1(e) => Some(e), + VkDecodeError::ConvertAv1(e) => Some(e), + VkDecodeError::ParamsAv1(e) => Some(e), + VkDecodeError::TilesAv1(e) => Some(e), + VkDecodeError::Caps(e) => Some(e), + VkDecodeError::Device(e) => Some(e), + _ => None, + } + } +} + +impl From for VkDecodeError { + fn from(r: vk::Result) -> Self { + if r == vk::Result::ERROR_DEVICE_LOST { + VkDecodeError::DeviceLost + } else { + VkDecodeError::Vk(r) + } + } +} + +impl From for VkDecodeError { + fn from(e: PlanError) -> Self { + VkDecodeError::Plan(e) + } +} + +impl From for VkDecodeError { + fn from(e: ParamsError) -> Self { + VkDecodeError::Params(e) + } +} + +impl From for VkDecodeError { + fn from(e: H265ParamsError) -> Self { + VkDecodeError::ParamsH265(e) + } +} + +impl From for VkDecodeError { + fn from(e: ParamsAv1Error) -> Self { + VkDecodeError::ParamsAv1(e) + } +} + +impl From for VkDecodeError { + fn from(e: PlanToVkAv1Error) -> Self { + VkDecodeError::ConvertAv1(e) + } +} + +impl From for VkDecodeError { + fn from(e: CapsError) -> Self { + VkDecodeError::Caps(e) + } +} + +impl From for VkDecodeError { + fn from(e: DeviceError) -> Self { + VkDecodeError::Device(e) + } +} + +impl From for VkDecodeError { + fn from(e: SessionError) -> Self { + match e { + SessionError::Vk(r) => VkDecodeError::from(r), + SessionError::Params(p) => VkDecodeError::Params(p), + SessionError::ParamsH265(p) => VkDecodeError::ParamsH265(p), + SessionError::ParamsAv1(p) => VkDecodeError::ParamsAv1(p), + SessionError::NoMemoryType { type_bits, flags } => { + VkDecodeError::NoMemoryType { type_bits, flags } + } + } + } +} + +impl From for VkDecodeError { + fn from(e: AllocError) -> Self { + match e { + AllocError::Vk(r) => VkDecodeError::from(r), + AllocError::NoMemoryType { type_bits, flags } => { + VkDecodeError::NoMemoryType { type_bits, flags } + } + } + } +} + +/// Query pool + command pool/buffers. Query slots cycle per SUBMISSION (validated +/// against [`DecodedVkFrame::submission`]); command buffers cycle within the +/// bitstream ring's in-flight bound. Owns and destroys its Vulkan objects. +/// +/// `query_pool` is `None` when the decode family lacks `queryResultStatusSupport` +/// (RADV): recording a RESULT_STATUS query there is invalid — on the .25 box it +/// HANGS the VCN ring — so no query objects exist at all and status verdicts fall +/// back to timeline completion. +pub(crate) struct OpRing { + device: ash::Device, + pub(crate) query_pool: Option, + pub(crate) query_count: u32, + cmd_pool: vk::CommandPool, + pub(crate) cmds: Vec, +} + +impl OpRing { + /// # Safety + /// + /// `dev` wraps live handles ([`DeviceHandles`] contract). + pub(crate) unsafe fn create( + dev: &DecodeDevice, + decode_profile: DecodeProfile, + query_count: u32, + cmd_count: u32, + ) -> Result { + let query_pool = if dev.result_status_queries() { + let mut chain = decode_profile.chain(); + // SAFETY: fn contract. `chain` outlives the call, and the helper's + // SIGNATURE — not a comment — is what keeps it immobile across it. + Some(unsafe { Self::create_status_query_pool(dev, chain.wire(), query_count)? }) + } else { + debug!( + "decode family lacks queryResultStatusSupport — no per-op status \ + queries on this driver (verdicts fall back to timeline completion)" + ); + None + }; + + let destroy_query = |pool: Option| { + if let Some(pool) = pool { + // SAFETY: destroying the just-created query pool (unwind path). + unsafe { dev.ash().destroy_query_pool(pool, None) }; + } + }; + let pool_ci = vk::CommandPoolCreateInfo::default() + .queue_family_index(dev.decode_qf()) + .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER); + // SAFETY: live device; unwind destroys the query pool on failure. + let cmd_pool = match unsafe { dev.ash().create_command_pool(&pool_ci, None) } { + Ok(p) => p, + Err(e) => { + destroy_query(query_pool); + return Err(e); + } + }; + let alloc = vk::CommandBufferAllocateInfo::default() + .command_pool(cmd_pool) + .command_buffer_count(cmd_count); + // SAFETY: live device + the pool created above; unwind destroys both pools + // (destroying the command pool frees any allocated buffers). + let cmds = match unsafe { dev.ash().allocate_command_buffers(&alloc) } { + Ok(c) => c, + Err(e) => { + // SAFETY: destroying the command pool created above. + unsafe { dev.ash().destroy_command_pool(cmd_pool, None) }; + destroy_query(query_pool); + return Err(e); + } + }; + Ok(Self { + device: dev.ash().clone(), + query_pool, + query_count, + cmd_pool, + cmds, + }) + } + + /// The RESULT_STATUS query pool, created against `profile`. + /// + /// Split out for the BORROW rather than for tidiness. `VkQueryPoolCreateInfo` + /// has no codec-aware builder here — `push_next` would clobber the profile's + /// own `p_next` (its codec half), so the chain is written as a raw `*const`, and + /// a raw pointer ends the borrow the moment it is taken. Inline, only inspection + /// stopped a later edit from moving or dropping the chain between that write and + /// `vkCreateQueryPool`; taking `&vk::VideoProfileInfoKHR<'_>` as a PARAMETER + /// makes the compiler hold the borrow across the whole call instead + /// ([`crate::caps::H264ProfileChain`]'s contract, which used to claim the + /// borrow checker covered this site and did not). + /// + /// # Safety + /// + /// `dev` wraps live handles ([`DeviceHandles`] contract). + unsafe fn create_status_query_pool( + dev: &DecodeDevice, + profile: &vk::VideoProfileInfoKHR<'_>, + query_count: u32, + ) -> Result { + let mut query_ci = vk::QueryPoolCreateInfo::default() + .query_type(vk::QueryType::RESULT_STATUS_ONLY_KHR) + .query_count(query_count); + // Chained manually: `push_next` would clobber the profile's own `p_next` + // — the encoder's exact precedent. + query_ci.p_next = std::ptr::from_ref(profile).cast(); + // SAFETY: fn contract; `query_ci` roots the wired chain for the call, and + // `profile` is borrowed for the whole of this body so the chain cannot move + // out from under that pointer. The video profile chained in satisfies the + // "same profile as the session" rule for queries used inside a coding scope. + unsafe { dev.ash().create_query_pool(&query_ci, None) } + } +} + +impl Drop for OpRing { + fn drop(&mut self) { + // SAFETY: own handles on the contract-live device; the owning decoder + // drains GPU work before dropping state. Destroying the command pool frees + // its buffers; both destroys ignore NULL. + unsafe { + self.device.destroy_command_pool(self.cmd_pool, None); + if let Some(pool) = self.query_pool { + self.device.destroy_query_pool(pool, None); + } + } + } +} + +/// A decoded picture awaiting its output verdict: which pool image holds it and +/// everything its eventual [`DecodedVkFrame`] needs. Codec-agnostic — the H.265 +/// decoder keeps the same map. +pub(crate) struct PendingPic { + pub(crate) image: usize, + pub(crate) submission: u64, + pub(crate) query_slot: u32, + /// The image's timeline value the decode signalled (frame readiness). + pub(crate) timeline_value: u64, + pub(crate) crop: DisplayCrop, + pub(crate) colour: ColourDescription, + pub(crate) poc: i32, + pub(crate) is_idr: bool, + /// Folded at PLAN time (the only place the codec's counting unit is known) and + /// carried here, because a picture's display order is not its decode order — + /// see [`DecodedVkFrame::recovery`]. + pub(crate) recovery: crate::recovery::RecoveryMark, + /// See [`DecodedVkFrame::decode_order`]. + pub(crate) decode_order: u64, +} + +/// A retired generation's picture pool: images the presenter still holds live +/// here until their release tokens return, then the pool dies. +pub(crate) struct RetiredPool { + pub(crate) generation: u64, + pub(crate) pool: PicturePool, +} + +/// Everything tied to ONE session generation. A stream renegotiation (extent, +/// DPB depth, profile) retires it and builds fresh. +struct SessionState { + session: VideoSession, + slots: SlotMap, + /// Distinct mode's reference-only DPB backing; `None` in coincide mode (the + /// picture pool backs the DPB there). + dpb: Option, + pool: PicturePool, + ring: BitstreamRing, + ops: OpRing, + /// Last-known Std reference info per DPB slot — `vkCmdBeginVideoCodingKHR` + /// wants codec reference info for EVERY bound slot, including ones this AU's + /// slices do not reference; refreshed from each plan's setup/ref entries so + /// marking transitions (e.g. MMCO long-term promotion) propagate. + slot_refs: Vec>, + /// Coincide mode: which pool image each DPB slot currently binds (rebound at + /// every activation — the decoupling that keeps delivered images safe). + slot_image: Vec>, + /// Per command-buffer completion tokens (reuse gate). + cmd_marks: Vec>, + /// Per query-slot submission ordinals (staleness validation). + query_marks: Vec, + /// Submissions recorded on this session (cmd/query indexing). + submitted: u64, + /// The newest submission's completion token (session drain). + last_submit: Option<(vk::Semaphore, u64)>, + /// The STREAM's coded extent (renegotiation comparison). + coded_extent: vk::Extent2D, + /// The granularity-aligned allocation extent (picture resources + frames). + image_extent: vk::Extent2D, +} + +/// The native Vulkan Video H.264 decoder. +pub struct VkH264Decoder { + dev: DecodeDevice, + lock: Box, + planner: H264Planner, + /// Caps per Std profile idc, queried once per profile. + caps: Option<(hh::StdVideoH264ProfileIdc, DecodeCaps)>, + state: Option, + /// Decoded pictures awaiting their planner output verdict, keyed by [`PicId`]. + pending: BTreeMap, + /// Display-ready frames not yet handed out (under the zero-reorder punktfunk + /// envelope at most one per AU; deeper only around discontinuities/flushes). + ready: VecDeque, + /// Retired generations' pools with consumer-held images (die on their last + /// release token). + graveyard: Vec, + /// The most recent plan's warnings ([`Self::take_warnings`]). + last_warnings: Vec, + /// The outstanding recovery point SEI, if any — see [`crate::recovery`]. + /// Survives session rebuilds on purpose: it is a fact about the STREAM's + /// prediction structure, not about this decoder's Vulkan objects. + recovery_watch: crate::recovery::RecoveryWatch, + /// Pictures planned so far — stamped onto each one as + /// [`DecodedVkFrame::decode_order`]. Survives session rebuilds for the same + /// reason the watch does. + decoded: u64, + /// Session generation: bumped on every rebuild, stamped into frames. + generation: u64, + device_lost: bool, +} + +impl VkH264Decoder { + /// Wrap the borrowed device. Sessions/pools are built lazily from the first + /// AU's SPS (their shape is the stream's, not the device's). + /// + /// # Safety + /// + /// The full [`DeviceHandles`] caller contract (liveness, enabled extensions + /// and features, truthful queue families) — held for this decoder's whole + /// lifetime, not just this call. The device must additionally have been + /// created with `VK_KHR_video_decode_h264` enabled; that one part of the + /// contract is CHECKED below rather than trusted, because getting it wrong is + /// undefined behaviour at session creation rather than an error. + pub unsafe fn new( + handles: &DeviceHandles, + lock: Box, + ) -> Result { + // SAFETY: forwarded caller contract. + let dev = unsafe { DecodeDevice::wrap(handles)? }; + // Before anything is queried or created: does this queue family actually + // run H.264 decode ops? (device.rs — the caps query would answer for the + // hardware even where the extension was never enabled.) + dev.require_codec_op(vk::VideoCodecOperationFlagsKHR::DECODE_H264, "H.264 decode")?; + Ok(Self { + dev, + lock, + planner: H264Planner::new(), + caps: None, + state: None, + pending: BTreeMap::new(), + ready: VecDeque::new(), + graveyard: Vec::new(), + last_warnings: Vec::new(), + recovery_watch: crate::recovery::RecoveryWatch::new(), + decoded: 0, + generation: 0, + device_lost: false, + }) + } + + /// Decode one access unit. Returns the next display-ready frame, if the + /// planner declared one (zero-reorder streams: the AU's own picture). + /// + /// Never panics. `VkDecodeError::DeviceLost` latches: every later call fails + /// fast until the owner rebuilds the decoder on fresh handles. + pub fn decode(&mut self, au: &[u8]) -> Result, VkDecodeError> { + if self.device_lost { + return Err(VkDecodeError::DeviceLost); + } + let result = self.decode_inner(au); + if matches!(result, Err(VkDecodeError::DeviceLost)) { + self.device_lost = true; + } + result + } + + fn decode_inner(&mut self, au: &[u8]) -> Result, VkDecodeError> { + // `take_warnings` promises "cleared by the next decode", and this IS a + // decode: clear BEFORE planning, so an AU that fails to plan at all cannot + // leave the previous AU's warnings behind to be re-read as damage on the + // next one. That the ledger is drained after every successful decode + // (`take_warnings` is a `mem::take`) makes the failed-plan case the only + // one where it could carry over — it is a hole closed by construction, not + // a fix for anything observed in the field. + self.last_warnings.clear(); + let plan = self.planner.plan_au(au)?; + for warning in &plan.warnings { + // The recovery verdict is the integration layer's + // ([`Self::take_warnings`]); never silent here though. + trace!(?warning, "plan warning"); + } + self.last_warnings = plan.warnings.clone(); + // One picture per AU under this envelope: stamp its DECODE-order ordinal + // before anything can reorder it (see `DecodedVkFrame::decode_order`). + self.decoded = self.decoded.saturating_add(1); + let decode_order = self.decoded; + // The recovery-point watch, folded ONCE per successfully planned AU and in + // DECODE order — the order the SEI counts in. The mark rides the pending + // picture to display order, which may differ (crate::recovery). + let recovery = self.recovery_watch.note_h264( + plan.picture.frame_num, + plan.picture.is_idr, + plan.picture.recovery_point, + ); + if recovery != crate::recovery::RecoveryMark::NONE { + trace!( + sei = recovery.sei_here, + recovery_point = recovery.is_recovery_point, + frame_num = plan.picture.frame_num, + "recovery point SEI" + ); + } + + self.ensure_state(&plan)?; + let sps_id = plan.sps.seq_parameter_set_id; + + // Convert, with ONE rebuild retry on CapacityMismatch — the designed + // trigger for a DPB-depth renegotiation (pic.rs docs). + let mut vk_plan: Option = None; + for attempt in 0..2 { + // A parameters RECREATE destroys the old object, which an in-flight + // decode may still be executing against: drain first. Recreate is a + // parameter-set content change under a stable id — rare enough (an + // encoder reconfiguration) that the stall is the right trade. + if self + .state + .as_ref() + .expect("ensure_state built it") + .session + .parameters_action(&plan.sps, &plan.pps) + == ParamsAction::Recreate + { + self.drain_gpu()?; + } + let state = self.state.as_mut().expect("ensure_state built it"); + // SAFETY: live device (constructor contract); the drain above + // satisfies ensure_parameters' Recreate contract, and Current/Add + // touch nothing a submitted decode reads. + unsafe { state.session.ensure_parameters(&plan.sps, &plan.pps)? }; + match plan_to_vk(&plan, &mut state.slots, sps_id) { + Ok(converted) => { + vk_plan = Some(converted); + break; + } + Err(PlanToVkError::CapacityMismatch { required, capacity }) if attempt == 0 => { + debug!( + required, + capacity, "DPB depth renegotiated — rebuilding session" + ); + self.rebuild_state(&plan)?; + } + Err(e) => return Err(VkDecodeError::Convert(e)), + } + } + let vk_plan = vk_plan.expect("the rebuilt session matches its own plan"); + + let state = self.state.as_mut().expect("ensured above"); + // The per-AU active-reference gate: the session was created with + // maxActiveReferencePictures; binding more in one decode op would be a + // silent VUID violation on the drivers that matter most. + let max_active = state.session.config.max_active_references as usize; + if vk_plan.refs.len() > max_active { + return Err(VkDecodeError::Unsupported(format!( + "AU references {} pictures, session allows {max_active} active references", + vk_plan.refs.len() + ))); + } + + // Coincide binding sync: slots the planner released no longer bind their + // images (the pictures may still be pending/held — untouched), and the + // setup slot's PREVIOUS binding is cleared before it binds fresh. + let setup = usize::from(vk_plan.setup_slot); + if state.dpb.is_none() { + let mut held = vec![false; state.slot_image.len()]; + for (slot, _id) in state.slots.held() { + held[usize::from(slot)] = true; + } + for (slot, binding) in state.slot_image.iter_mut().enumerate() { + if let Some(picture) = *binding { + if !held[slot] || slot == setup { + state.pool.pictures[picture].bound = false; + *binding = None; + } + } + } + } + + // The decode target: a FREE pool image (never one a consumer holds — the + // whole point of the pool model). Exhaustion means the consumer owes + // more than HOLD_HEADROOM releases; no wait can free an image here. + let Some(dst) = state.pool.free_index() else { + debug!( + held = state.pool.held_total(), + "picture pool exhausted — release_frame owed" + ); + return Err(VkDecodeError::NoFreeSlot); + }; + + // Cross-queue waits (the AVVkFrame contract): the dst image's last known + // timeline value (covers a presenter write-back after release), plus — + // coincide mode — every referenced image's value, so reference reads + // order after any presenter layout restore already reported back. + let mut waits: Vec<(vk::Semaphore, u64)> = Vec::new(); + { + let dst_pic = &state.pool.pictures[dst]; + if dst_pic.value > 0 { + waits.push((dst_pic.semaphore, dst_pic.value)); + } + } + if state.dpb.is_none() { + for r in &vk_plan.refs { + if let Some(picture) = state.slot_image[usize::from(r.slot)] { + let pic = &state.pool.pictures[picture]; + if pic.value > 0 && !waits.iter().any(|(sem, _)| *sem == pic.semaphore) { + waits.push((pic.semaphore, pic.value)); + } + } + } + } + let signal_value = state.pool.pictures[dst].value + 1; + + // Command buffer + query slot for this submission. + let submission = state.submitted; + let cmd_index = (submission % state.ops.cmds.len() as u64) as usize; + if let Some((sem, value)) = state.cmd_marks[cmd_index] { + // SAFETY: live device; the token is a pool image's semaphore. + unsafe { wait_timeline(self.dev.ash(), sem, value, "command buffer reuse")? }; + } + let query_index = (submission % u64::from(state.ops.query_count)) as u32; + + // Upload the AU (recycles/grows against submission-completion tokens). + let device = self.dev.ash().clone(); + let mut poll = |token: &(vk::Semaphore, u64)| -> Result { + // SAFETY: live device; the token's semaphore is a pool semaphore. + let current = unsafe { device.get_semaphore_counter_value(token.0) } + .map_err(VkDecodeError::from)?; + Ok(current >= token.1) + }; + let device2 = self.dev.ash().clone(); + let mut wait = |token: &(vk::Semaphore, u64)| -> Result<(), VkDecodeError> { + // SAFETY: as above. + unsafe { wait_timeline(&device2, token.0, token.1, "bitstream slot drain") } + }; + // The bitstream buffer carries the SLICE NALUs only, concatenated — a + // real AU opens with AUD/SEI (and, at IDRs, SPS/PPS) NALUs, and feeding + // those to the VCN firmware inside the decode range HANGS it (the .25 + // `vcn_unified_0 ring timeout`; FFmpeg feeds slices-only for the same + // reason). `pack_slices` rebases the offsets into the packed buffer AND + // normalises each slice's Annex-B prefix to three bytes — the two go + // together by construction, see `crate::ring::three_byte_prefix`. + let plan_segments: Vec> = + plan.slices.iter().map(|s| s.data.clone()).collect(); + let Some(packed) = pack_slices(au, &plan_segments) else { + return Err(VkDecodeError::Unsupported( + "packed slice data exceeds the u32 offsets Vulkan submits".into(), + )); + }; + let slice_offsets = packed.offsets; + // SAFETY: live device; the segments are the plan's own in-bounds slice + // ranges (narrowed by the prefix normalisation, so still in bounds); every + // pending token is the completion signal of the submission that consumed + // the slot. + let upload = unsafe { + state + .ring + .upload(&self.dev, au, &packed.segments, &mut poll, &mut wait)? + }; + + // Record + submit, signalling the dst image's next timeline value. + // SAFETY: live device; every handle recorded below belongs to this + // session generation, and the packed slices sit uploaded in the ring slot. + unsafe { + record_and_submit( + &self.dev, + &*self.lock, + state, + &vk_plan, + &slice_offsets, + &upload, + dst, + cmd_index, + query_index, + &waits, + signal_value, + )?; + } + + // Post-submit bookkeeping. + let dst_sem = state.pool.pictures[dst].semaphore; + state.pool.pictures[dst].value = signal_value; + state.pool.pictures[dst].pending = true; + if state.dpb.is_none() { + state.pool.pictures[dst].bound = true; + state.slot_image[setup] = Some(dst); + } + state.cmd_marks[cmd_index] = Some((dst_sem, signal_value)); + state.query_marks[query_index as usize] = submission; + state.submitted += 1; + state.last_submit = Some((dst_sem, signal_value)); + state + .ring + .pending + .set_pending(upload.slot, (dst_sem, signal_value)); + + // Refresh the per-slot reference cache from this AU's facts. + state.slot_refs[setup] = Some(vk_plan.setup_ref); + for r in &vk_plan.refs { + state.slot_refs[usize::from(r.slot)] = Some(r.std); + } + + self.pending.insert( + vk_plan.setup_id, + PendingPic { + image: dst, + submission, + query_slot: query_index, + timeline_value: signal_value, + crop: plan.picture.display_crop, + colour: plan.picture.colour, + poc: plan.picture.pic_order_cnt, + is_idr: plan.picture.is_idr, + recovery, + decode_order, + }, + ); + + // The plan's DPB verdicts over the pending map: outputs become ready + // frames (their images move pending → held until released); + // removed-but-never-output pictures free their images. + let (ready, dropped) = settle_dpb(&mut self.pending, &plan.dpb); + let state = self.state.as_mut().expect("ensured above"); + for entry in ready { + let frame = build_frame( + &mut state.pool, + state.dpb.is_none(), + state.image_extent, + &entry, + self.generation, + ); + self.ready.push_back(frame); + } + for entry in dropped { + debug!( + poc = entry.poc, + "picture removed without output — freeing its image" + ); + state.pool.pictures[entry.image].pending = false; + } + Ok(self.ready.pop_front()) + } + + /// Hand a delivered frame back. `presenter_signaled` reports whether the + /// consumer SAMPLED the image (and therefore enqueued the `value + 1` + /// timeline signal per the [`DecodedVkFrame`] contract) — the decoder then + /// waits that write-back before the image's next use. Every frame + /// `decode`/`take_ready` returns must come back exactly once, including + /// stale-generation frames (their retired pool dies on its last token). + pub fn release_frame( + &mut self, + frame: &DecodedVkFrame, + presenter_signaled: bool, + ) -> Result<(), VkDecodeError> { + let pool = if frame.generation == self.generation { + match &mut self.state { + Some(state) => &mut state.pool, + None => { + return Err(VkDecodeError::StaleFrame { + frame_generation: frame.generation, + current_generation: self.generation, + }) + } + } + } else { + match self + .graveyard + .iter_mut() + .find(|r| r.generation == frame.generation) + { + Some(retired) => &mut retired.pool, + None => { + return Err(VkDecodeError::StaleFrame { + frame_generation: frame.generation, + current_generation: self.generation, + }) + } + } + }; + let index = frame.picture as usize; + if index >= pool.pictures.len() { + return Err(VkDecodeError::StaleFrame { + frame_generation: frame.generation, + current_generation: self.generation, + }); + } + let picture = &mut pool.pictures[index]; + match picture.held.checked_sub(1) { + Some(remaining) => picture.held = remaining, + None => { + debug!(index, "frame released more often than delivered"); + return Ok(()); + } + } + if presenter_signaled { + picture.value = picture.value.max(frame.value + 1); + } + // A retired pool dies on its last token (presenter fence-waited before + // the token per the release contract; decode work drained at retirement). + if frame.generation != self.generation { + self.graveyard + .retain(|r| r.generation != frame.generation || r.pool.held_total() > 0); + } + Ok(()) + } + + /// A display-ready frame beyond the one `decode` returned, if any (only + /// non-empty around discontinuities/flushes — the punktfunk envelope is + /// zero-reorder). Drain after every decode; frames left here still occupy + /// pool images. + pub fn take_ready(&mut self) -> Option { + self.ready.pop_front() + } + + /// The warnings of the most recent successfully planned AU (concealment + /// signals — the integration layer's want_keyframe hook). Cleared by the + /// next `decode`. + pub fn take_warnings(&mut self) -> Vec { + std::mem::take(&mut self.last_warnings) + } + + /// The current session generation ([`DecodedVkFrame::generation`] of newly + /// delivered frames). + pub fn generation(&self) -> u64 { + self.generation + } + + /// The DECODE-order ordinal of the most recently planned picture — the + /// watermark a consumer compares [`DecodedVkFrame::decode_order`] against to + /// tell a frame decoded before a loss from one decoded after it. 0 before the + /// first AU plans. + pub fn decode_order(&self) -> u64 { + self.decoded + } + + /// One-line state snapshot for failure paths and field logs (not a stable + /// format). + pub fn debug_snapshot(&self) -> String { + match &self.state { + None => format!("gen={} ", self.generation), + Some(state) => { + let occupancy: Vec = state + .pool + .pictures + .iter() + .enumerate() + .map(|(i, p)| { + format!( + "{i}:{}{}h{}", + if p.bound { "B" } else { "-" }, + if p.pending { "P" } else { "-" }, + p.held + ) + }) + .collect(); + format!( + "gen={} mode={} slots_held={}/{} pool=[{}] pending={} ready={} graveyard={}", + self.generation, + if state.dpb.is_none() { + "coincide" + } else { + "distinct" + }, + state.slots.active(), + state.slots.capacity(), + occupancy.join(" "), + self.pending.len(), + self.ready.len(), + self.graveyard.len(), + ) + } + } + } + + /// Read `frame`'s decode status WITHOUT waiting. + /// + /// [`DecodeStatus::Failed`] covers driver-reported errors AND a query slot + /// re-armed before it was read (the status is then unprovable — same + /// conservative verdict). + /// + /// On drivers whose decode family lacks `queryResultStatusSupport` (RADV) + /// there is no per-op verdict to read: `Ok` then means "the decode op + /// COMPLETED on the timeline" — the same information FFmpeg has on every + /// driver, no worse; the Ally-X-class detection exists exactly where the + /// driver can give it (NVIDIA, AMD's Windows driver). + pub fn poll_status(&mut self, frame: &DecodedVkFrame) -> DecodeStatus { + self.read_status(frame, false) + } + + /// Does this decode queue family answer per-op `RESULT_STATUS` queries at all + /// (`queryResultStatusSupport`)? + /// + /// The single most important thing a support engineer can know about a + /// session's integrity reporting, and the reason this is exposed rather than + /// left internal. Where it is TRUE, [`DecodeStatus::Failed`] is the driver's + /// own verdict on a decode operation — the signal the Xbox Ally X corruption + /// needed and FFmpeg's query-less Vulkan decoder (`nb_queries = 0`) can never + /// produce. Where it is FALSE — RADV, whose VCN ring HANGS if a query is + /// recorded anyway — `Ok` degrades to "the op completed on the timeline", which + /// is exactly as much as FFmpeg knows on every driver: no worse, but a clean + /// integrity report from such a session means "nothing was detectable", not + /// "nothing was wrong". A telemetry surface that cannot say which of those it + /// is repeats the failure this program exists to end. + pub fn status_queries(&self) -> bool { + self.dev.result_status_queries() + } + + /// [`Self::poll_status`], but WAITs for the op to complete first — the only + /// place a status read blocks (the GPU smoke test's assertion path; the + /// integration layer's steady state polls). + pub fn wait_status(&mut self, frame: &DecodedVkFrame) -> DecodeStatus { + self.read_status(frame, true) + } + + fn read_status(&mut self, frame: &DecodedVkFrame, block: bool) -> DecodeStatus { + if frame.generation != self.generation { + trace!( + frame_generation = frame.generation, + current = self.generation, + "status asked for a stale-generation frame — Failed, without \ + touching the new pools" + ); + return DecodeStatus::Failed; + } + let Some(state) = &self.state else { + return DecodeStatus::Failed; + }; + let Some(query_pool) = state.ops.query_pool else { + // No queries on this driver: the verdict degrades to timeline + // completion (poll_status docs). + if block { + // SAFETY: live device; pool-owned semaphore. + return match unsafe { + wait_timeline(self.dev.ash(), frame.semaphore, frame.value, "status wait") + } { + Ok(()) => DecodeStatus::Ok, + Err(VkDecodeError::DeviceLost) => { + self.device_lost = true; + DecodeStatus::Failed + } + Err(_) => DecodeStatus::Failed, + }; + } + // SAFETY: live device; pool-owned semaphore. + return match unsafe { self.dev.ash().get_semaphore_counter_value(frame.semaphore) } { + Ok(current) if current >= frame.value => DecodeStatus::Ok, + Ok(_) => DecodeStatus::Pending, + Err(vk::Result::ERROR_DEVICE_LOST) => { + self.device_lost = true; + DecodeStatus::Failed + } + Err(_) => DecodeStatus::Failed, + }; + }; + let slot = frame.query_slot as usize; + if slot >= state.query_marks.len() || state.query_marks[slot] != frame.submission { + trace!( + slot, + "status query slot re-armed before it was read — unprovable, reported Failed" + ); + return DecodeStatus::Failed; + } + let flags = if block { + vk::QueryResultFlags::WAIT | vk::QueryResultFlags::WITH_STATUS_KHR + } else { + vk::QueryResultFlags::WITH_STATUS_KHR + }; + let mut status = [0i32; 1]; + // SAFETY: live device; the query pool is this session generation's own and + // `frame.query_slot` indexes within its count (checked above against the + // marks array it is sized to). + let result = unsafe { + self.dev + .ash() + .get_query_pool_results(query_pool, frame.query_slot, &mut status, flags) + }; + match result { + // VkQueryResultStatusKHR: >0 complete, 0 not ready, <0 error. + Ok(()) if status[0] > 0 => DecodeStatus::Ok, + Ok(()) if status[0] == 0 => DecodeStatus::Pending, + Ok(()) => DecodeStatus::Failed, + Err(vk::Result::NOT_READY) => DecodeStatus::Pending, + Err(vk::Result::ERROR_DEVICE_LOST) => { + self.device_lost = true; + DecodeStatus::Failed + } + Err(r) => { + debug!(?r, "status query read failed"); + DecodeStatus::Failed + } + } + } + + /// Wait — bounded by `timeout_ns` — for a delivered frame's decode-complete + /// signal ([`DecodedVkFrame::semaphore`] reaching [`DecodedVkFrame::value`]). + /// Pure measurement (the integration layer's sampled decode-latency stat): + /// touches no decoder state, so a timeout or error only degrades the stat — + /// the consumer's own GPU wait is what gates sampling, never this. `frame` + /// must be unreleased (`release_frame` still owed), which pins its pool — and + /// with it the semaphore — alive, graveyarded generations included; a + /// stale-generation frame declines rather than block on a verdict the + /// rebuild's drain already implied. + pub fn wait_decoded(&self, frame: &DecodedVkFrame, timeout_ns: u64) -> bool { + if frame.generation != self.generation { + return false; + } + let semaphores = [frame.semaphore]; + let values = [frame.value]; + let info = vk::SemaphoreWaitInfo::default() + .semaphores(&semaphores) + .values(&values); + // SAFETY: live device (constructor contract); the semaphore is a pool + // semaphore the unreleased frame keeps alive (fn docs); the info arrays + // are locals outliving the call. + unsafe { self.dev.ash().wait_semaphores(&info, timeout_ns) }.is_ok() + } + + /// Drain the planner (teardown / stream discontinuity): every buffered + /// picture becomes display-ready via [`Self::take_ready`] (zero-copy — the + /// images already hold the content), all DPB slots free, and any picture + /// removed without ever reaching output frees its image. + pub fn flush(&mut self) { + let update = self.planner.flush(); + let (ready, dropped) = settle_dpb(&mut self.pending, &update); + if let Some(state) = &mut self.state { + state.slots.apply(&update); + for entry in ready { + let frame = build_frame( + &mut state.pool, + state.dpb.is_none(), + state.image_extent, + &entry, + self.generation, + ); + self.ready.push_back(frame); + } + for entry in dropped { + state.pool.pictures[entry.image].pending = false; + } + // Defensive: a pending picture neither output nor removed should not + // exist after a flush; free any leftover. + for (_, entry) in std::mem::take(&mut self.pending) { + debug!(poc = entry.poc, "pending picture survived a flush — freed"); + state.pool.pictures[entry.image].pending = false; + } + } else { + self.pending.clear(); + } + } + + /// Session/caps for THIS plan exist and match its extent + profile, and the + /// stream sits inside the device's level ceiling. DPB-depth mismatches + /// surface later as `plan_to_vk`'s `CapacityMismatch` (the designed trigger) + /// and take the same rebuild path. + fn ensure_state(&mut self, plan: &AuPlan) -> Result<(), VkDecodeError> { + let std_profile = std_profile_for(plan)?; + if self.caps.as_ref().map(|(p, _)| *p) != Some(std_profile) { + // SAFETY: live device (constructor contract). + let raw = + unsafe { query_h264_caps(&self.dev, std_profile) }.map_err(VkDecodeError::from)?; + self.caps = Some((std_profile, derive_caps(&raw)?)); + } + // The level gate: a stream above the device's maxLevelIdc is refused up + // front (within one codec the Std code points ascend with the level, so + // the comparison is numeric), never submitted on a hope. The ceiling came + // from an H.264 caps query, so it is compared against an H.264 code point + // — the pairing MaxLevelIdc's tag exists to keep honest. + let caps_max_level = self.caps.as_ref().expect("queried above").1.max_level_idc; + let stream_level = level_to_std(plan.picture.level_idc); + if stream_level > caps_max_level.code_point() { + return Err(VkDecodeError::Unsupported(format!( + "stream level (Std code point {stream_level}) above the device's \ + maxLevelIdc ({caps_max_level})" + ))); + } + let coded = vk::Extent2D { + width: plan.picture.coded_width, + height: plan.picture.coded_height, + }; + match &self.state { + Some(state) + if state.coded_extent == coded + && state.session.config.std_profile_idc == std_profile => + { + Ok(()) + } + _ => self.rebuild_state(plan), + } + } + + /// Tear down the current session generation (draining its decode work, + /// retiring its picture pool to the graveyard when the consumer still holds + /// images) and build a fresh one shaped by `plan`, bumping + /// [`Self::generation`] so frames of the old one route to the graveyard. + /// + /// Why a mid-stream rebuild is safe against presenter-held frames (the + /// renegotiation-teardown question, settled): + /// - **Images**: a pool with consumer holds retires to the graveyard INTACT — + /// images, views and semaphores stay live until `release_frame` takes its + /// last token, and a token is sent only after the presenter's sampling + /// submission's fence was waited (its `value+1` write-back included), so no + /// pool image is ever destroyed under in-flight GPU reads. + /// - **Tokens**: every frame and its token carry the generation they were + /// born under, and `release_frame` routes strictly by it (current pool vs + /// graveyard entry), so releases cannot alias across generations. + /// - **Session objects**: the session/ring/ops (query pool included) DO die + /// right here — but only after [`Self::drain_gpu`], and no consumer-facing + /// handle points at them: [`DecodedVkFrame`] borrows pool resources only, + /// and `poll_status` generation-gates before it would touch the NEW + /// generation's query pool with an old frame's slot. + fn rebuild_state(&mut self, plan: &AuPlan) -> Result<(), VkDecodeError> { + self.drain_gpu()?; + if let Some(state) = self.state.take() { + debug!("rebuilding decode session (stream renegotiation)"); + // Move the fields out (SessionState has no Drop of its own): the + // session/dpb/ring/ops die here — decode work was just drained and + // the presenter never references them. The PICTURE POOL may outlive: + // undelivered frames drop (their holds cleared), pending pictures + // free, and if the consumer still holds delivered images the pool + // retires to the graveyard until its last release token. + let SessionState { mut pool, .. } = state; + for frame in self.ready.drain(..) { + let picture = &mut pool.pictures[frame.picture as usize]; + picture.held = picture.held.saturating_sub(1); + } + for (_, entry) in std::mem::take(&mut self.pending) { + pool.pictures[entry.image].pending = false; + } + for picture in &mut pool.pictures { + picture.bound = false; + } + let held = pool.held_total(); + if held > 0 { + debug!( + held, + generation = self.generation, + "consumer still holds images of the retired generation — graveyarding" + ); + self.graveyard.push(RetiredPool { + generation: self.generation, + pool, + }); + } + } + self.generation += 1; + + let (std_profile, caps) = self.caps.as_ref().expect("ensure_state queried caps"); + let std_profile = *std_profile; + let required_slots = plan.picture.max_dpb_frames as u32 + 1; + if required_slots > caps.max_dpb_slots { + return Err(VkDecodeError::Unsupported(format!( + "stream needs {required_slots} DPB slots, device caps at {}", + caps.max_dpb_slots + ))); + } + let coded = vk::Extent2D { + width: plan.picture.coded_width, + height: plan.picture.coded_height, + }; + // Bounds-checked at the ALLOCATION extent (granularity-rounded): that is + // what the images are created at and what maxCodedExtent must cover. + let image_extent = caps.aligned_extent(coded); + if coded.width < caps.min_coded_extent.width + || coded.height < caps.min_coded_extent.height + || image_extent.width > caps.max_coded_extent.width + || image_extent.height > caps.max_coded_extent.height + { + return Err(VkDecodeError::Unsupported(format!( + "coded extent {}x{} (allocated {}x{}) outside device range {}x{}..{}x{}", + coded.width, + coded.height, + image_extent.width, + image_extent.height, + caps.min_coded_extent.width, + caps.min_coded_extent.height, + caps.max_coded_extent.width, + caps.max_coded_extent.height + ))); + } + + let config = SessionConfig { + max_coded_extent: image_extent, + max_dpb_slots: required_slots, + max_active_references: (required_slots - 1).min(caps.max_active_references), + std_profile_idc: std_profile, + }; + let mut pool_plan = plan_pools(caps, required_slots); + // TEST-ONLY readback hook: the GPU parity test (tests/gpu_parity.rs) + // copies decoded pictures back to the host to hash them against + // libavcodec's output, and `vkCmdCopyImageToBuffer` requires + // TRANSFER_SRC on the source image — a bit the zero-copy production + // pools deliberately do not carry. Opt-in via env so no production path + // ever grows it. (The fleet's drivers — RADV, NVIDIA, AMD Windows — + // advertise TRANSFER_SRC on their decode-output formats; it is the same + // bit FFmpeg's hwdownload path relies on.) + if std::env::var("PF_VKD_TEST_READBACK").is_ok_and(|v| v == "1") { + pool_plan.picture_usage |= vk::ImageUsageFlags::TRANSFER_SRC; + } + let decode_profile = DecodeProfile::H264(std_profile); + // SAFETY: live device per the constructor contract, for every create in + // this block; each created half is owned by a Drop type the moment it + // exists, so a mid-build failure unwinds cleanly. + let state = unsafe { + let session = VideoSession::create(&self.dev, caps, config)?; + let dpb = if caps.coincide { + None + } else { + Some( + DpbPool::create(&self.dev, caps, &pool_plan, image_extent, decode_profile) + .map_err(VkDecodeError::from)?, + ) + }; + let pool = + PicturePool::create(&self.dev, caps, &pool_plan, image_extent, decode_profile) + .map_err(VkDecodeError::from)?; + let ring = BitstreamRing::create( + &self.dev, + RingLayout::new( + INITIAL_SLOT_SIZE, + RING_SLOTS, + caps.min_bitstream_offset_alignment, + caps.min_bitstream_size_alignment, + ), + decode_profile, + ) + .map_err(VkDecodeError::from)?; + let ops = OpRing::create( + &self.dev, + decode_profile, + pool_plan.picture_count, + RING_SLOTS, + ) + .map_err(VkDecodeError::from)?; + SessionState { + session, + slots: SlotMap::new(plan.picture.max_dpb_frames), + slot_refs: vec![None; required_slots as usize], + slot_image: vec![None; required_slots as usize], + cmd_marks: vec![None; RING_SLOTS as usize], + query_marks: vec![u64::MAX; pool_plan.picture_count as usize], + submitted: 0, + last_submit: None, + coded_extent: coded, + image_extent, + dpb, + pool, + ring, + ops, + } + }; + self.state = Some(state); + Ok(()) + } + + /// Wait out every in-flight decode submission of the current session. + fn drain_gpu(&mut self) -> Result<(), VkDecodeError> { + let Some(state) = &self.state else { + return Ok(()); + }; + if let Some((sem, value)) = state.last_submit { + // SAFETY: live device; the token is a pool image's semaphore. + unsafe { wait_timeline(self.dev.ash(), sem, value, "session drain")? }; + } + Ok(()) + } +} + +impl Drop for VkH264Decoder { + fn drop(&mut self) { + // Best-effort decode drain so the pools' Drop impls never destroy + // in-flight decode work; a wedged driver falls through after the bounded + // timeout. Presenter-side sampling of graveyarded/held images is the + // CALLER's teardown contract: the integration layer waits (bounded) for + // every release token BEFORE dropping this decoder, so remaining + // graveyard pools here are either token-drained or a warned forfeit. + if let Err(e) = self.drain_gpu() { + debug!(error = %e, "drain on drop failed; tearing down anyway"); + } + if !self.graveyard.is_empty() { + debug!( + pools = self.graveyard.len(), + "graveyard not fully token-drained at decoder drop — destroying anyway \ + (upstream teardown forfeited its bounded wait)" + ); + } + } +} + +/// Map the plan's `profile_idc` to the Std code point (identity for the four +/// Vulkan-representable profiles, reject otherwise — WP-A's exact rule). +fn std_profile_for(plan: &AuPlan) -> Result { + match u32::from(plan.picture.profile_idc) { + p @ (66 | 77 | 100 | 244) => Ok(p), + _ => Err(VkDecodeError::Params(ParamsError::UnmappableProfileIdc( + plan.picture.profile_idc, + ))), + } +} + +/// Build the delivered frame for one settled pending picture, moving its image +/// pending → held. +/// +/// Takes the pool and the two mode facts rather than a `SessionState` so both +/// codecs' decoders share it (their session states differ only in codec-specific +/// fields, and this function reads none of them). The frame's +/// [`DecodedVkFrame::format`] comes off the POOL rather than a caller argument, +/// which is what makes it truthful for both codecs by construction: the pool +/// stamped it from the very `caps.output_format` its images were created with. +pub(crate) fn build_frame( + pool: &mut PicturePool, + coincide: bool, + image_extent: vk::Extent2D, + entry: &PendingPic, + generation: u64, +) -> DecodedVkFrame { + let format = pool.format; + let picture = &mut pool.pictures[entry.image]; + picture.pending = false; + picture.held += 1; + DecodedVkFrame { + image: picture.image, + format, + view: picture.view, + plane_views: picture.plane_views, + layer: 0, + layout: if coincide { + vk::ImageLayout::VIDEO_DECODE_DPB_KHR + } else { + vk::ImageLayout::VIDEO_DECODE_DST_KHR + }, + coded_width: image_extent.width, + coded_height: image_extent.height, + crop: entry.crop, + colour: entry.colour, + semaphore: picture.semaphore, + value: entry.timeline_value, + poc: entry.poc, + is_idr: entry.is_idr, + recovery: entry.recovery, + decode_order: entry.decode_order, + query_slot: entry.query_slot, + submission: entry.submission, + picture: entry.image as u32, + generation, + } +} + +/// Split one [`DpbUpdate`]'s verdicts over the pending map: `outputs` (in bump +/// order) become deliverable; `removed` ids that never reached output — an IDR's +/// `no_output_of_prior_pics_flag` discard, or a flush racing a drop — are +/// returned separately so their images are freed instead of leaking. Pure and +/// generic for testability — and codec-agnostic (H.265 plans carry the very same +/// [`DpbUpdate`] type), so both decoders settle through this one function. +pub(crate) fn settle_dpb(pending: &mut BTreeMap, dpb: &DpbUpdate) -> (Vec, Vec) { + settle_dpb_ids(pending, &dpb.outputs, &dpb.removed) +} + +/// [`settle_dpb`] over the two id lists directly. +/// +/// It exists because AV1's planner declares its OWN `DpbUpdate` +/// ([`pf_bitstream::av1::DpbUpdate`]) rather than re-using the H.264 one the way +/// H.265 does — structurally identical, a distinct type. Splitting the settle at +/// the id lists is what lets all three codecs share ONE implementation of the +/// output/free bookkeeping instead of the AV1 rung growing a copy that could drift. +pub(crate) fn settle_dpb_ids( + pending: &mut BTreeMap, + outputs: &[PicId], + removed: &[PicId], +) -> (Vec, Vec) { + let mut ready = Vec::new(); + for id in outputs { + match pending.remove(id) { + Some(entry) => ready.push(entry), + // Ids planned before this decoder existed (post-recovery), or + // dropped across a rebuild: display-order gaps, not errors. + None => trace!(id, "output id without a pending picture"), + } + } + let dropped = removed.iter().filter_map(|id| pending.remove(id)).collect(); + (ready, dropped) +} + +/// Bounded timeline wait (no-op for the never-signalled value 0). +/// +/// # Safety +/// +/// `device` is live and `semaphore` is a live timeline semaphore on it. +pub(crate) unsafe fn wait_timeline( + device: &ash::Device, + semaphore: vk::Semaphore, + value: u64, + what: &'static str, +) -> Result<(), VkDecodeError> { + if value == 0 { + return Ok(()); + } + let semaphores = [semaphore]; + let values = [value]; + let info = vk::SemaphoreWaitInfo::default() + .semaphores(&semaphores) + .values(&values); + // SAFETY: fn contract; the info arrays are locals outliving the call. + match unsafe { device.wait_semaphores(&info, DECODE_TIMEOUT_NS) } { + Ok(()) => Ok(()), + Err(vk::Result::TIMEOUT) => Err(VkDecodeError::Timeout(what)), + Err(e) => Err(VkDecodeError::from(e)), + } +} + +/// The picture resource view bound for DPB `slot`: the bound pool image +/// (coincide) or the DPB array layer (distinct). `None` when a coincide slot has +/// no binding (unreachable in practice — every held slot was activated). +fn slot_view(state: &SessionState, slot: u8) -> Option { + match &state.dpb { + Some(dpb) => Some(dpb.dpb_view(slot)), + None => state.slot_image[usize::from(slot)].map(|p| state.pool.pictures[p].view), + } +} + +/// Record one decode op into the chosen command buffer and submit it under the +/// queue lock: image waits per the pool contract, the dst image's timeline +/// signal at `signal_value`. +/// +/// # Safety +/// +/// Live device; `state` is the current session generation with `vk_plan` derived +/// against its `SlotMap`, `dst` a free pool image, the AU resident in `upload`'s +/// ring slot, and the command buffer's previous submission completed (caller +/// waited its mark). +#[allow(clippy::too_many_arguments)] +unsafe fn record_and_submit( + dev: &DecodeDevice, + lock: &dyn QueueLock, + state: &mut SessionState, + vk_plan: &DecodePlanVk, + slice_offsets: &[u32], + upload: &UploadedAu, + dst: usize, + cmd_index: usize, + query_index: u32, + waits: &[(vk::Semaphore, u64)], + signal_value: u64, +) -> Result<(), VkDecodeError> { + let device = dev.ash(); + let cmd = state.ops.cmds[cmd_index]; + let coded_extent = state.coded_extent; + let coincide = state.dpb.is_none(); + + let begin_info = + vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); + // SAFETY: the buffer's previous submission completed (fn contract) and its + // pool allows per-buffer reset, so begin implicitly resets it. + unsafe { + device + .begin_command_buffer(cmd, &begin_info) + .map_err(VkDecodeError::from)? + }; + + // ---- barriers (outside the video coding scope) ---- + // Prior reconstructions must be visible to this op's reference reads. + let memory_barriers = [vk::MemoryBarrier2::default() + .src_stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + .src_access_mask(vk::AccessFlags2::VIDEO_DECODE_WRITE_KHR) + .dst_stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + .dst_access_mask( + vk::AccessFlags2::VIDEO_DECODE_READ_KHR | vk::AccessFlags2::VIDEO_DECODE_WRITE_KHR, + )]; + // Decode targets are fully overwritten: discard via UNDEFINED with an + // execution+memory dependency on earlier ops that touched them. + let decode_layer_barrier = |image: vk::Image, layer: u32, new_layout: vk::ImageLayout| { + vk::ImageMemoryBarrier2::default() + .src_stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + .src_access_mask( + vk::AccessFlags2::VIDEO_DECODE_READ_KHR | vk::AccessFlags2::VIDEO_DECODE_WRITE_KHR, + ) + .dst_stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + .dst_access_mask( + vk::AccessFlags2::VIDEO_DECODE_READ_KHR | vk::AccessFlags2::VIDEO_DECODE_WRITE_KHR, + ) + .old_layout(vk::ImageLayout::UNDEFINED) + .new_layout(new_layout) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(image) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: layer, + layer_count: 1, + }) + }; + let dst_image = state.pool.pictures[dst].image; + let mut image_barriers = Vec::new(); + if coincide { + // The dst pool image IS the setup DPB picture. + image_barriers.push(decode_layer_barrier( + dst_image, + 0, + vk::ImageLayout::VIDEO_DECODE_DPB_KHR, + )); + } else { + let dpb = state.dpb.as_ref().expect("distinct mode"); + let (setup_image, setup_layer) = dpb.dpb_target(vk_plan.setup_slot); + image_barriers.push(decode_layer_barrier( + setup_image, + setup_layer, + vk::ImageLayout::VIDEO_DECODE_DPB_KHR, + )); + image_barriers.push(decode_layer_barrier( + dst_image, + 0, + vk::ImageLayout::VIDEO_DECODE_DST_KHR, + )); + } + let dependency = vk::DependencyInfo::default() + .memory_barriers(&memory_barriers) + .image_memory_barriers(&image_barriers); + // SAFETY: recording into the begun buffer; synchronization2 is enabled per + // the DeviceHandles feature contract. + unsafe { device.cmd_pipeline_barrier2(cmd, &dependency) }; + + // This op's status query slot, reset before the coding scope (encoder idiom). + // None on drivers without queryResultStatusSupport (RADV — recording a query + // there hangs the VCN; OpRing docs). + if let Some(query_pool) = state.ops.query_pool { + // SAFETY: recording; `query_index` is within the pool's count (fn contract). + unsafe { device.cmd_reset_query_pool(cmd, query_pool, query_index, 1) }; + } + + // ---- bound-slot staging ---- + // Scope list: this AU's references first, then every other still-held slot + // (their resources must stay bound for their associations to persist), then + // the setup slot as the ACTIVATION entry (slot index -1 binds its resource + // without a current association; the decode op's setup slot then claims it). + let mut scope: Vec<(i32, vk::ImageView, hh::StdVideoDecodeH264ReferenceInfo)> = Vec::new(); + for r in &vk_plan.refs { + match slot_view(state, r.slot) { + Some(view) => scope.push((i32::from(r.slot), view, r.std)), + None => trace!(slot = r.slot, "referenced slot without a bound image"), + } + } + for (slot, _id) in state.slots.held() { + if slot == vk_plan.setup_slot + || scope + .iter() + .any(|&(index, _, _)| index >= 0 && index as u8 == slot) + { + continue; + } + match (state.slot_refs[usize::from(slot)], slot_view(state, slot)) { + (Some(std), Some(view)) => scope.push((i32::from(slot), view, std)), + // Unreachable in practice: every held slot was a setup slot once. + _ => trace!( + slot, + "held slot without reference info/binding — left unbound" + ), + } + } + let reference_count = vk_plan.refs.len().min(scope.len()); + // The setup/dst resource: the fresh pool image (coincide) or the DPB layer + // (distinct — the pool image is the separate decode output). + let setup_view = if coincide { + state.pool.pictures[dst].view + } else { + state + .dpb + .as_ref() + .expect("distinct mode") + .dpb_view(vk_plan.setup_slot) + }; + scope.push((-1, setup_view, vk_plan.setup_ref)); + + // Staged arrays: resources → std infos → codec slot infos → slot infos. Each + // vector is fully built before the next borrows it, so nothing reallocates + // under a stored pointer. + let resources: Vec> = scope + .iter() + .map(|&(_, view, _)| { + vk::VideoPictureResourceInfoKHR::default() + .coded_extent(coded_extent) + .base_array_layer(0) + .image_view_binding(view) + }) + .collect(); + let std_refs: Vec = + scope.iter().map(|&(_, _, std)| std).collect(); + let mut dpb_infos: Vec> = std_refs + .iter() + .map(|std| vk::VideoDecodeH264DpbSlotInfoKHR::default().std_reference_info(std)) + .collect(); + let mut begin_slots: Vec> = Vec::with_capacity(scope.len()); + for (index, &(slot_index, _, _)) in scope.iter().enumerate() { + begin_slots.push( + vk::VideoReferenceSlotInfoKHR::default() + .slot_index(slot_index) + .picture_resource(&resources[index]), + ); + } + for (slot_info, dpb_info) in begin_slots.iter_mut().zip(dpb_infos.iter_mut()) { + *slot_info = (*slot_info).push_next(dpb_info); + } + // The decode op's reference list: exactly this AU's references (the first + // `reference_count` scope entries, which carry their real slot indices). + let decode_refs: Vec> = + begin_slots[..reference_count].to_vec(); + + // The setup slot as the decode op sees it: its REAL index (the begin list's + // twin entry carries -1), same resource, its own codec info chain. + let setup_std = vk_plan.setup_ref; + let mut setup_dpb = vk::VideoDecodeH264DpbSlotInfoKHR::default().std_reference_info(&setup_std); + let setup_resource = resources[scope.len() - 1]; + let setup_slot_info = vk::VideoReferenceSlotInfoKHR::default() + .slot_index(i32::from(vk_plan.setup_slot)) + .picture_resource(&setup_resource) + .push_next(&mut setup_dpb); + + // Decode destination: the setup picture itself (coincide) or the pool image + // (distinct). + let dst_resource = if coincide { + setup_resource + } else { + vk::VideoPictureResourceInfoKHR::default() + .coded_extent(coded_extent) + .base_array_layer(0) + .image_view_binding(state.pool.pictures[dst].view) + }; + + let std_pic = vk_plan.std_pic; + // Offsets rebased into the packed slices-only buffer (NOT the plan's + // AU-absolute offsets — the AU's non-slice NALUs were never uploaded). + let mut h264_pic = vk::VideoDecodeH264PictureInfoKHR::default() + .std_picture_info(&std_pic) + .slice_offsets(slice_offsets); + let mut decode_info = vk::VideoDecodeInfoKHR::default() + .src_buffer(state.ring.buffer()) + .src_buffer_offset(upload.offset) + .src_buffer_range(upload.range) + .dst_picture_resource(dst_resource) + .setup_reference_slot(&setup_slot_info) + .push_next(&mut h264_pic); + if reference_count > 0 { + decode_info = decode_info.reference_slots(&decode_refs); + } + + let begin_coding = vk::VideoBeginCodingInfoKHR::default() + .video_session(state.session.session()) + .video_session_parameters(state.session.parameters()) + .reference_slots(&begin_slots); + // The one-shot session RESET, consumed HERE but re-armed on every error path + // below — a RESET recorded into a command buffer that never reaches the + // queue initialized nothing, and the next successful recording must carry it + // or the session runs its whole life uninitialized. + let did_reset = state.session.take_needs_reset(); + // SAFETY: recording into the begun buffer, through end_command_buffer; every + // pointed-to struct above is a local (or session-state field) that outlives + // the calls; the session/parameters handles are this generation's own. + let recorded: Result<(), vk::Result> = unsafe { + (dev.video_queue().fp().cmd_begin_video_coding_khr)(cmd, &begin_coding); + if did_reset { + // Session first-use initialization — ONCE, before its first decode. + let control = vk::VideoCodingControlInfoKHR::default() + .flags(vk::VideoCodingControlFlagsKHR::RESET); + (dev.video_queue().fp().cmd_control_video_coding_khr)(cmd, &control); + } + if let Some(query_pool) = state.ops.query_pool { + device.cmd_begin_query(cmd, query_pool, query_index, vk::QueryControlFlags::empty()); + } + (dev.video_decode_queue().fp().cmd_decode_video_khr)(cmd, &decode_info); + if let Some(query_pool) = state.ops.query_pool { + device.cmd_end_query(cmd, query_pool, query_index); + } + (dev.video_queue().fp().cmd_end_video_coding_khr)( + cmd, + &vk::VideoEndCodingInfoKHR::default(), + ); + device.end_command_buffer(cmd) + }; + if let Err(e) = recorded { + if did_reset { + state.session.re_arm_reset(); + } + return Err(VkDecodeError::from(e)); + } + + // ---- submit, under the caller's queue lock ---- + let cmd_infos = [vk::CommandBufferSubmitInfo::default().command_buffer(cmd)]; + let wait_infos: Vec> = waits + .iter() + .map(|&(semaphore, value)| { + vk::SemaphoreSubmitInfo::default() + .semaphore(semaphore) + .value(value) + .stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + }) + .collect(); + let signals = [vk::SemaphoreSubmitInfo::default() + .semaphore(state.pool.pictures[dst].semaphore) + .value(signal_value) + .stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS)]; + let submits = [vk::SubmitInfo2::default() + .command_buffer_infos(&cmd_infos) + .wait_semaphore_infos(&wait_infos) + .signal_semaphore_infos(&signals)]; + let guard = QueueSubmitGuard::acquire(lock); + // SAFETY: the decode queue is the device's own (DeviceHandles contract) and + // externally synchronized by the guard; the submit arrays are locals. + let result = unsafe { device.queue_submit2(dev.decode_queue(), &submits, vk::Fence::null()) }; + drop(guard); + if let Err(e) = result { + // The recorded RESET never executed: the next recording must redo it. + if did_reset { + state.session.re_arm_reset(); + } + return Err(VkDecodeError::from(e)); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn settle_dpb_readies_outputs_in_order_and_returns_never_output_removals() { + let mut pending: BTreeMap = BTreeMap::new(); + pending.insert(1, 100); + pending.insert(2, 200); + pending.insert(3, 300); + + // Picture 1 outputs (and is also removed — the normal bump); picture 2 + // is removed WITHOUT ever reaching output (no_output_of_prior_pics): + // its image must be freed, not leak in the map. + let update = DpbUpdate { + stored: Some(3), + outputs: vec![1], + removed: vec![1, 2], + }; + let (ready, dropped) = settle_dpb(&mut pending, &update); + assert_eq!(ready, vec![100]); + assert_eq!(dropped, vec![200]); + assert_eq!( + pending.keys().copied().collect::>(), + vec![3], + "the still-buffered picture stays pending" + ); + + // Output order is bump order, and unknown ids are tolerated. + let mut pending: BTreeMap = BTreeMap::new(); + pending.insert(5, 500); + pending.insert(4, 400); + let update = DpbUpdate { + stored: None, + outputs: vec![5, 99, 4], + removed: vec![], + }; + let (ready, dropped) = settle_dpb(&mut pending, &update); + assert_eq!(ready, vec![500, 400], "bump order, not id order"); + assert!(dropped.is_empty()); + } + + #[test] + fn std_level_code_points_ascend_so_the_max_level_gate_compares_numerically() { + use pf_bitstream::h264::Level; + // The gate is `level_to_std(stream) > caps.max_level_idc.code_point()`; + // that is only sound if the Std code points ascend with the level WITHIN + // one codec (which is why the ceiling carries its codec — MaxLevelIdc). + // Pin the ordering across the range (and the 1b fold onto 1.1). + let ascending = [ + Level::L1, + Level::L1_1, + Level::L2_0, + Level::L3_1, + Level::L4, + Level::L4_2, + Level::L5_2, + Level::L6_2, + ]; + for pair in ascending.windows(2) { + assert!( + level_to_std(pair[0]) < level_to_std(pair[1]), + "{:?} vs {:?}", + pair[0], + pair[1] + ); + } + assert_eq!(level_to_std(Level::L1B), level_to_std(Level::L1_1)); + + // The gate itself, on both sides of a ceiling. + let max = level_to_std(Level::L4_1); + assert!( + level_to_std(Level::L4) <= max, + "within the ceiling: allowed" + ); + assert!( + level_to_std(Level::L4_2) > max, + "above the ceiling: Unsupported" + ); + } +} diff --git a/crates/pf-vkdecode/src/decoder_av1.rs b/crates/pf-vkdecode/src/decoder_av1.rs new file mode 100644 index 00000000..bb7c7212 --- /dev/null +++ b/crates/pf-vkdecode/src/decoder_av1.rs @@ -0,0 +1,3302 @@ +//! [`VkAv1Decoder`]: the assembled native AV1 decoder — [`crate::decoder_h265`] +//! one codec over, over pf-bitstream's AV1 planner and M7's CPU half. +//! +//! Per access unit: `plan_au` → (per frame) `plan_to_vk_av1` → tile OBUs into the +//! bitstream ring → record (barriers, `vkCmdBeginVideoCodingKHR` with every bound +//! DPB slot, the one-time session RESET control, a caps-gated +//! `RESULT_STATUS_ONLY` query bracketing `vkCmdDecodeVideoKHR`) → submit on the +//! decode queue under the caller's [`QueueLock`] with a per-image timeline signal. +//! +//! Everything codec-agnostic is SHARED with the other two decoders rather than +//! re-implemented: the picture pool and its zero-copy hand-off contract +//! ([`crate::images`]), the bitstream ring, the op ring (command buffers + status +//! queries), the pending/ready/graveyard bookkeeping, `build_frame` and the DPB +//! settle (`settle_dpb_ids`, split off `settle_dpb` precisely so AV1's own +//! `DpbUpdate` type can share it). What is genuinely AV1's own lives here: +//! +//! - **One access unit is a TEMPORAL UNIT, and may carry several frames.** +//! `Av1Planner::plan_au` returns a VECTOR — the vendored 250-packet vector holds +//! 274 frames, the extras being hidden ALTREFs. Every plan is decoded, in order; +//! the frames they make ready queue up and `decode` hands back the first. +//! - **A `show_existing_frame` plan decodes nothing.** It has `dpb.stored == None` +//! and displays `dpb.outputs` — a picture an earlier, hidden frame decoded. It +//! is settled like any other DPB verdict and never reaches a submission. +//! - **`referenceNameSlotIndices` holds DPB SLOT indices, not positions in +//! `pReferenceSlots`.** The two coincide for as long as references happen to land +//! in slots `0..refs.len()` in `refs` order, which on a freshly keyed stream they +//! do — and that is exactly how the HEVC RPS defect shipped. The plan computes +//! slots ([`DecodePlanVkAv1::reference_name_slot_indices`]); this module lays +//! `pReferenceSlots` out in [`DecodePlanVkAv1::refs`] order INDEPENDENTLY, and +//! [`build_scope_av1`] fails closed when the two disagree about a slot the op +//! binds. +//! - **A DPB slot this frame READS may not be recycled until its decode op is +//! recorded.** `refresh_frame_flags` applies AFTER the frame decodes (7.20), so +//! almost every inter frame of a low-delay stream overwrites a slot it is +//! reading — 268 of the vendored vector's 274 frames. Releasing that slot inside +//! the conversion, which is what H.264 and H.265 do with their whole `removed` +//! list, gives it to this frame's own decode target: the reference then names +//! the slot being written. [`DecodePlanVkAv1::release_after_decode`] carries +//! those ids and this module releases them after the submission. +//! - **A lost reference is fatal, not degraded.** `AuPlan::refs` is indexed by +//! reference NAME and a lost reference leaves a HOLE there, so nothing is +//! renumbered and the conversion could in principle write +//! [`REFERENCE_NAME_UNUSED`] for it and carry on. It does not: `-1` for a name +//! the frame DOES reference is a spec violation, and what a driver's firmware +//! then predicts from is undefined. The AU is refused +//! ([`VkDecodeError::MissingReferenceAv1`], predicate [`lost_reference`]), +//! recovery is latched and the stream re-anchors on the next key frame. Since +//! the plan became name-indexed this is defence in depth rather than the only +//! guard. +//! - **Tiles, not slices.** `VkVideoDecodeAV1PictureInfoKHR` wants a per-TILE +//! offset and size into the uploaded buffer, and the plan carries whole +//! tile-group (or frame) OBUs. [`plan_bitstream`] walks each OBU's tile-group +//! header and per-tile size fields to recover the tile payloads, and it is those +//! payloads — nothing else — that go into the ring slot +//! ([`crate::ring::pack_av1_tiles`]). +//! +//! Codec dispatch (which decoder a stream gets) is the client wiring's job, not +//! this crate's: the public surface here mirrors [`crate::VkH265Decoder`] +//! method-for-method so the dispatch is a three-arm enum. +//! +//! # What the bitstream buffer contains +//! +//! Exactly the raw tile payloads, concatenated, with `frameHeaderOffset` at 0 — +//! libavcodec's `vulkan_av1.c` layout, byte for byte. Nothing else goes in: no OBU +//! headers, no frame header, none of the `tile_size_minus_1` fields between tiles. +//! +//! That is a deliberate choice over the spec-literal alternative (upload the whole +//! tile-group/frame OBUs, point `frameHeaderOffset` at the real frame header). The +//! spec-literal layout is not WRONG — the per-tile offsets and sizes are the part a +//! driver indexes by and they are identical either way, AV1 has no start-code +//! scanning to be confused by the extra bytes, and `frameHeaderOffset` is read by +//! no driver in this fleet (every one of them takes the whole frame header out of +//! `pStdPictureInfo`). But libavcodec is the implementation every driver was +//! validated against, so matching it removes the residual risk on the drivers +//! nobody here has tested, uploads fewer bytes per frame, and deletes the rebase +//! arithmetic that mapping in-OBU tile offsets to packed-buffer offsets needed. +//! +//! # `pTileOffsets` / `pTileSizes` are sized to the driver's read, not to tileCount +//! +//! ⚠ RADV reads `AV1_MAX_NUM_TILES` (256) entries out of both arrays +//! unconditionally — `radv_video.c`'s `for (i = 0; i < AV1_MAX_NUM_TILES; ++i)` — +//! and never looks at `tileCount`. libavcodec gets away with it because its +//! `tile_sizes` is a static `uint32_t[256]`. A `Vec` sized to the real tile count +//! (one, for every frame of the vendored vector) is a four-byte allocation the +//! driver reads a kilobyte deep. So both arrays are always 256 entries with the +//! tail zeroed, and `tileCount` is set separately — see [`SubmittedTiles`]. + +use std::collections::BTreeMap; +use std::collections::VecDeque; +use std::ops::Range; + +use ash::vk; +use ash::vk::native as hh; +use cros_codecs::codec::av1::parser::FrameHeaderObu; +use pf_bitstream::av1::AuPlan; +use pf_bitstream::av1::Av1Planner; +use pf_bitstream::av1::PicId; +use pf_bitstream::av1::PlanWarning; +use pf_bitstream::av1::NUM_REF_SLOTS; +use pf_bitstream::h264::DisplayCrop; +use tracing::debug; +use tracing::trace; + +use crate::caps::DecodeCaps; +use crate::caps::DecodeProfile; +use crate::caps_av1::derive_caps_av1; +use crate::caps_av1::query_av1_caps; +use crate::caps_av1::Av1ProfileKey; +use crate::decoder::build_frame; +use crate::decoder::settle_dpb_ids; +use crate::decoder::wait_timeline; +use crate::decoder::DecodeStatus; +use crate::decoder::DecodedVkFrame; +use crate::decoder::OpRing; +use crate::decoder::PendingPic; +use crate::decoder::RetiredPool; +use crate::decoder::VkDecodeError; +use crate::decoder_h265::RecoveryLatch; +use crate::device::DecodeDevice; +use crate::device::DeviceHandles; +use crate::device::QueueLock; +use crate::device::QueueSubmitGuard; +use crate::images::plan_pools; +use crate::images::DpbPool; +use crate::images::PicturePool; +use crate::pic_av1::plan_to_vk_av1; +use crate::pic_av1::DecodePlanVkAv1; +use crate::pic_av1::VkRefAv1; +use crate::pic_av1::REFERENCE_NAME_UNUSED; +use crate::ring::pack_av1_tiles; +use crate::ring::BitstreamRing; +use crate::ring::PackedAv1Tiles; +use crate::ring::RingLayout; +use crate::ring::UploadedAu; +use crate::ring::INITIAL_SLOT_SIZE; +use crate::ring::RING_SLOTS; +use crate::session_av1::ParamsActionAv1; +use crate::session_av1::SessionConfigAv1; +use crate::session_av1::VideoSessionAv1; +use crate::slots::SlotMap; + +/// AV1's DPB depth: eight reference slots (`NUM_REF_FRAMES`) plus the picture +/// being decoded. Unlike H.264/H.265 this is a CONSTANT of the codec, not an SPS +/// field — so an AV1 session never renegotiates its DPB depth and +/// `plan_to_vk_av1` has no `CapacityMismatch` to answer. +const REQUIRED_SLOTS: u32 = NUM_REF_SLOTS as u32 + 1; + +/// `OBU_TILE_GROUP` — the OBU type carrying tile data on its own. +const OBU_TILE_GROUP: u8 = 4; +/// `OBU_FRAME` — a frame header and its tile group in one OBU. +const OBU_FRAME: u8 = 6; + +/// Why an access unit's tile OBUs cannot be turned into the per-tile byte ranges +/// `VkVideoDecodeAV1PictureInfoKHR` wants. +/// +/// Every variant is a MALFORMED-INPUT verdict, and every one of them refuses the +/// AU. Submitting the whole OBU as if it were tile payload would hand the hardware +/// the OBU header, the tile-group header and the `tile_size_minus_1` fields as +/// entropy-coded data — plausible-looking garbage, which is the outcome this crate +/// exists to refuse. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Av1TileError { + /// The OBU (or a field inside it) runs past the access unit. + Truncated { obu: usize }, + /// `obu_forbidden_bit` was set: this is not an OBU header. + NotAnObu { obu: usize }, + /// An OBU type the plan's tile list should never contain — only + /// `OBU_TILE_GROUP` and `OBU_FRAME` carry tiles. + UnexpectedObu { obu: usize, obu_type: u8 }, + /// The frame's tile info claims no tiles at all, so nothing can be located. + NoTiles, + /// The OBU's own `obu_size` field disagrees with the byte range the plan + /// carries for it. + /// + /// Worth its own variant because it is the one cross-check the tile walk gets + /// for free: the AV1 spec makes the LAST tile's size implicit (whatever is + /// left), so a walk always ends flush with the payload no matter how wrong the + /// preceding sizes were. `obu_size` is the only independent statement of where + /// the payload ends, and a range that disagrees with it means every offset + /// derived from that range is suspect. + SizeMismatch { + obu: usize, + declared_end: usize, + ranged_end: usize, + }, + /// A tile offset or size beyond the `u32` fields Vulkan submits. + Overflow, + /// More tiles than [`AV1_MAX_NUM_TILES`], which is as many as the submission + /// arrays hold and as many as libavcodec accepts. + TooManyTiles { tiles: usize }, +} + +impl std::fmt::Display for Av1TileError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Av1TileError::Truncated { obu } => { + write!(f, "tile OBU {obu} runs past the access unit") + } + Av1TileError::NotAnObu { obu } => { + write!(f, "tile OBU {obu} has obu_forbidden_bit set") + } + Av1TileError::UnexpectedObu { obu, obu_type } => { + write!( + f, + "tile OBU {obu} has type {obu_type}, which carries no tiles" + ) + } + Av1TileError::NoTiles => write!(f, "the frame header codes no tiles"), + Av1TileError::SizeMismatch { + obu, + declared_end, + ranged_end, + } => write!( + f, + "tile OBU {obu} declares its payload ending at {declared_end}, the \ + plan's range ends at {ranged_end}" + ), + Av1TileError::Overflow => { + write!(f, "a tile offset or size exceeds the u32 Vulkan submits") + } + Av1TileError::TooManyTiles { tiles } => write!( + f, + "{tiles} tiles exceed the {AV1_MAX_NUM_TILES} a submission carries" + ), + } + } +} + +impl std::error::Error for Av1TileError {} + +/// The bitstream facts one AV1 frame's submission needs, in ACCESS-UNIT +/// coordinates: every tile's raw payload range, in decode order. +/// +/// These ranges ARE what gets uploaded — the module docs' layout — so the packed +/// offsets fall straight out of the concatenation and there is nothing to rebase. +/// +/// # Why [`Self::groups`] exists when this rung never reads it +/// +/// The DXVA rung (`pf_dxvadec::pack_av1`, which depends on this crate — the link +/// only goes one way, so it cannot be a doc link) uploads a DIFFERENT layout: whole +/// `tile_data` regions, `tile_size_minus_1` fields and all, because that is +/// byte-for-byte what libavcodec's `dxva2_av1.c` hands a Windows driver and this +/// program's method there is to reproduce libavcodec rather than to reason from a +/// specification. The two layouts differ only in bytes NEITHER API's per-tile +/// offsets address, so the walk that finds the tiles is the same walk — and the +/// region each tile group contributes is a byte offset this function already +/// computes and used to throw away. +/// +/// Publishing it here rather than duplicating the walk in pf-dxvadec is the same +/// call [`SlotMap`] records: a second copy of 150 lines of spec-literal byte +/// arithmetic buys one fewer crate edge and costs a divergence. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Av1Bitstream { + /// Every tile's raw payload, in decode order across all of the frame's tile + /// groups. Access-unit coordinates. + pub tiles: Vec>, + /// One region per tile-group (or frame) OBU, in plan order: the OBU's + /// `tile_data` — from the first tile's `tile_size_minus_1` field through the + /// end of the OBU payload. Access-unit coordinates, and every range in + /// [`Self::tiles`] lies inside exactly one of these. + pub groups: Vec>, +} + +/// Read one LEB128 value at `at`, returning it and its byte length. +fn leb128(au: &[u8], at: usize) -> Option<(u64, usize)> { + let mut value = 0u64; + // The AV1 spec caps leb128() at 8 bytes; a ninth continuation byte is + // malformed, not a bigger number. + for i in 0..8 { + let byte = *au.get(at + i)?; + value |= u64::from(byte & 0x7f) << (i * 7); + if byte & 0x80 == 0 { + return Some((value, i + 1)); + } + } + None +} + +/// Walk one access unit's tile OBUs into per-tile payload ranges. +/// +/// # Why this is here rather than in the planner +/// +/// `TilePlan::data` is a whole tile-group (or frame) OBU: the OBU header, then — +/// for `OBU_FRAME` — the frame header, then the tile-group header, then for every +/// tile but the last a `tile_size_minus_1` field followed by that tile's payload. +/// Vulkan wants the PAYLOADS, one offset and one size each, which is also what +/// libavcodec's Vulkan AV1 hwaccel submits. So the walk has to happen somewhere, +/// and it happens here because everything it needs is already in the plan: +/// +/// - `FrameHeaderObu::header_bytes` is the frame header's length inside the OBU +/// payload — the vendored parser's own figure, the same one it uses to hand the +/// tile group its slice of an `OBU_FRAME` — so the tile-group header's start is +/// not guessed; +/// - `TileInfo` gives `TileCols`/`TileRows` (hence `NumTiles`), the two `log2` +/// fields the `tg_start`/`tg_end` bit width comes from, and `TileSizeBytes`. +/// +/// The walk is the spec's `tile_group_obu()` byte layout (5.11.1) and nothing more; +/// it decodes no tile data. It takes the plan's PIECES rather than the plan so a +/// hand-built tile group can be walked in a unit test — the vendored vector is one +/// tile per frame, so the multi-tile arithmetic below has no other way to be +/// exercised. +/// +/// ⚠ What this can and cannot catch: the AV1 spec makes the LAST tile's size +/// IMPLICIT — whatever is left of the payload — so a walk always ends flush with +/// the OBU no matter how wrong the preceding sizes were, and "the sizes add up" +/// is not a check that exists. What does exist is [`Av1TileError::SizeMismatch`]: +/// the OBU's own `obu_size` field against the byte range the plan carries. A coded +/// size that OVERSHOOTS the payload is caught too ([`Av1TileError::Truncated`]); +/// one that undershoots simply shortens the last tile, and nothing in the +/// bitstream contradicts it. +pub fn plan_bitstream( + au: &[u8], + plan_tiles: &[pf_bitstream::av1::TilePlan], + header: &FrameHeaderObu, +) -> Result { + let tile_info = &header.tile_info; + let num_tiles = tile_info + .tile_cols + .checked_mul(tile_info.tile_rows) + .unwrap_or(0); + if num_tiles == 0 { + return Err(Av1TileError::NoTiles); + } + + let mut tiles: Vec> = Vec::with_capacity(num_tiles as usize); + let mut groups: Vec> = Vec::with_capacity(plan_tiles.len()); + + for (index, tile_group) in plan_tiles.iter().enumerate() { + let obu = &tile_group.data; + if obu.end > au.len() || obu.start >= obu.end { + return Err(Av1TileError::Truncated { obu: index }); + } + // --- obu_header() + the leb128 obu_size --- + let first = au[obu.start]; + if first & 0x80 != 0 { + return Err(Av1TileError::NotAnObu { obu: index }); + } + let obu_type = (first >> 3) & 0x0f; + let extension_flag = (first >> 2) & 1 == 1; + let has_size_field = (first >> 1) & 1 == 1; + let mut cursor = obu + .start + .checked_add(1 + usize::from(extension_flag)) + .ok_or(Av1TileError::Truncated { obu: index })?; + // The payload ends where the plan's range does: pf-bitstream builds that + // range from the parser's `bytes_used`, which is header + obu_size. When + // the OBU carries its own size field, the two are cross-checked — the only + // independent statement of the payload's end there is (see the fn docs). + // An Annex-B stream omits the field, and the range stands alone. + let payload_end = obu.end; + if has_size_field { + let (size, len) = leb128(au, cursor).ok_or(Av1TileError::Truncated { obu: index })?; + cursor += len; + let declared_end = cursor + .checked_add(usize::try_from(size).map_err(|_| Av1TileError::Overflow)?) + .ok_or(Av1TileError::Overflow)?; + if declared_end != payload_end { + return Err(Av1TileError::SizeMismatch { + obu: index, + declared_end, + ranged_end: payload_end, + }); + } + } + if cursor >= payload_end { + return Err(Av1TileError::Truncated { obu: index }); + } + + // --- past the frame header, for an OBU_FRAME --- + // Stepped OVER, never uploaded: the driver reads the frame header out of + // `pStdPictureInfo` and the bitstream buffer holds tile payloads only + // (module docs). + match obu_type { + OBU_FRAME => { + cursor = cursor + .checked_add(header.header_bytes) + .ok_or(Av1TileError::Truncated { obu: index })?; + } + OBU_TILE_GROUP => {} + other => { + return Err(Av1TileError::UnexpectedObu { + obu: index, + obu_type: other, + }) + } + } + if cursor >= payload_end { + return Err(Av1TileError::Truncated { obu: index }); + } + + // --- tile_group_obu()'s own header --- + // `tile_start_and_end_present_flag` is only coded when the frame has more + // than one tile; when it IS coded and set, `tg_start`/`tg_end` follow at + // `tile_cols_log2 + tile_rows_log2` bits each. Then byte_alignment(). + // (The flag has to be READ rather than inferred from the plan's tg_start / + // tg_end: a single-tile-group frame codes 0/NumTiles-1 either way, and the + // two spellings have different header lengths.) + let mut header_bits = 0usize; + if num_tiles > 1 { + let present = au[cursor] & 0x80 != 0; + header_bits += 1; + if present { + header_bits += 2 * (tile_info.tile_cols_log2 + tile_info.tile_rows_log2) as usize; + } + } + cursor += header_bits.div_ceil(8); + if cursor >= payload_end { + return Err(Av1TileError::Truncated { obu: index }); + } + // `tile_data` begins here — libavcodec's `AV1RawTileGroup::tile_data.data`, + // which is exactly the pointer its DXVA hwaccel `memcpy`s (struct docs). + groups.push(cursor..payload_end); + + // --- the tiles --- + // `tg_start`/`tg_end` index tiles 0..NumTiles-1, so a group claiming more + // than the frame has is malformed — and bounding the count here is also + // what keeps a hostile header from steering the walk below by its own + // arithmetic rather than by the payload. + let count = tile_group + .tg_end + .checked_sub(tile_group.tg_start) + .and_then(|span| span.checked_add(1)) + .filter(|count| *count <= num_tiles) + .ok_or(Av1TileError::Truncated { obu: index })? as usize; + // `TileSizeBytes` is `tile_size_bytes_minus_1 + 1` off two coded bits, so + // it is 1..=4 — but ONLY when the frame has more than one tile. The field + // is not coded at all for a single-tile frame (5.9.15), where the parser + // leaves whatever it last saw (0 on a fresh one), and the vendored vector + // is single-tile throughout: a width check applied unconditionally refuses + // every frame of it. So it is checked exactly where it is USED, and an + // out-of-range width is refused rather than shifted with (a debug panic, + // and a silent wrap in release). + let size_bytes = tile_info.tile_size_bytes as usize; + if count > 1 && !(1..=4).contains(&size_bytes) { + return Err(Av1TileError::Overflow); + } + for tile in 0..count { + let last = tile + 1 == count; + let size = if last { + payload_end + .checked_sub(cursor) + .ok_or(Av1TileError::Truncated { obu: index })? + } else { + // le(TileSizeBytes): little-endian, TileSizeBytes wide — and read + // from INSIDE the OBU, not merely inside the access unit. + if cursor + size_bytes > payload_end { + return Err(Av1TileError::Truncated { obu: index }); + } + let mut value = 0usize; + for byte in 0..size_bytes { + value |= usize::from(au[cursor + byte]) << (8 * byte); + } + cursor += size_bytes; + value + 1 + }; + let end = cursor + .checked_add(size) + .ok_or(Av1TileError::Truncated { obu: index })?; + if end > payload_end { + return Err(Av1TileError::Truncated { obu: index }); + } + tiles.push(cursor..end); + cursor = end; + } + debug_assert_eq!( + cursor, payload_end, + "the last tile's size is the payload remainder by construction" + ); + } + + if tiles.is_empty() { + return Err(Av1TileError::NoTiles); + } + Ok(Av1Bitstream { tiles, groups }) +} + +/// As many tiles as `pTileOffsets` / `pTileSizes` carry. +/// +/// It is 256 because RADV reads 256 entries out of both arrays whatever +/// `tileCount` says (module docs), and because libavcodec refuses a frame with more +/// — "exceeding all defined levels in the AV1 spec". +pub(crate) const AV1_MAX_NUM_TILES: usize = 256; + +/// The submission-final per-tile offsets and sizes. +/// +/// Fixed 256-entry arrays with a zeroed tail and a separate `count`, because the +/// arrays are sized to what a DRIVER reads and `tileCount` states what is +/// meaningful — the two are not the same number (module docs). Handing ash a slice +/// would fuse them, since both `tile_offsets()` and `tile_sizes()` set `tileCount` +/// from the slice length. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct SubmittedTiles { + pub(crate) offsets: [u32; AV1_MAX_NUM_TILES], + pub(crate) sizes: [u32; AV1_MAX_NUM_TILES], + pub(crate) count: u32, +} + +/// Where each tile lands once packed into the ring slot, and how long it is. +/// +/// A plain read-out of the packing: the uploaded segments ARE the tiles, so a +/// tile's offset is its segment's offset. What is left to check is that every +/// range stays inside the `u32` fields Vulkan submits — an offset that does not +/// land on the byte its tile starts at points the hardware into the middle of +/// somebody else's data, which is silent corruption rather than an error. +fn submitted_tiles(packed: &PackedAv1Tiles) -> Result { + if packed.segments.len() > AV1_MAX_NUM_TILES { + return Err(Av1TileError::TooManyTiles { + tiles: packed.segments.len(), + }); + } + let mut tiles = SubmittedTiles { + offsets: [0; AV1_MAX_NUM_TILES], + sizes: [0; AV1_MAX_NUM_TILES], + count: packed.segments.len() as u32, + }; + for (i, (segment, offset)) in packed.segments.iter().zip(&packed.offsets).enumerate() { + let size = u32::try_from(segment.len()).map_err(|_| Av1TileError::Overflow)?; + // The tile must end inside the packed buffer too — a size that overflows + // its own offset would be a range Vulkan reads past the buffer. + offset.checked_add(size).ok_or(Av1TileError::Overflow)?; + tiles.offsets[i] = *offset; + tiles.sizes[i] = size; + } + Ok(tiles) +} + +/// `frameHeaderOffset`, which is always 0 here: the bitstream buffer holds tile +/// payloads only, so there is no frame header in it to point at. libavcodec +/// hardcodes the same 0, and no driver in this fleet reads the field — each takes +/// the whole frame header out of `pStdPictureInfo`. +const FRAME_HEADER_OFFSET: u32 = 0; + +/// The submission-final `VkVideoDecodeAV1PictureInfoKHR`. +/// +/// Split out of the recording so the wiring a driver actually reads — which array +/// each pointer targets, and what `tileCount` says about them — is exercised by a +/// test rather than only by a device. +/// +/// ⚠ `tileCount` is assigned AFTER both setters, not left to them. ash's +/// `tile_offsets()` and `tile_sizes()` each set it from their slice length, and the +/// arrays here are deliberately longer than the tile count (module docs): letting +/// a setter win would tell the driver there are 256 tiles. +fn av1_picture_info<'a>( + std_pic: &'a hh::StdVideoDecodeAV1PictureInfo, + reference_name_slot_indices: [i32; pf_bitstream::av1::REFS_PER_FRAME], + tiles: &'a SubmittedTiles, +) -> vk::VideoDecodeAV1PictureInfoKHR<'a> { + let mut info = vk::VideoDecodeAV1PictureInfoKHR::default() + .std_picture_info(std_pic) + .reference_name_slot_indices(reference_name_slot_indices) + .frame_header_offset(FRAME_HEADER_OFFSET) + .tile_offsets(&tiles.offsets) + .tile_sizes(&tiles.sizes); + info.tile_count = tiles.count; + info +} + +/// The condition [`VkAv1Decoder::decode_planned`] refuses a whole access unit on: +/// the planner reported a reference the DPB no longer holds. +/// +/// A named function rather than a `find_map` inlined at the call site because it +/// is THE guard for the AV1 corruption class — a name the frame references +/// resolving to `-1`, or (before the plan became name-indexed) to the wrong +/// picture entirely — and a test that re-implements the predicate stays green when +/// the real one is deleted. Production and test call this. +/// +/// Note what it does NOT match: [`PlanWarning::TruncatedAu`] is concealment +/// material the planner already accounted for, and refusing on it would turn every +/// clipped access unit into a keyframe request. +pub(crate) fn lost_reference(warnings: &[PlanWarning]) -> Option<(u8, u8)> { + warnings.iter().find_map(|w| match w { + PlanWarning::MissingReference { slot, ref_index } => Some((*slot, *ref_index)), + _ => None, + }) +} + +/// What [`VkAv1Decoder::decode_planned`] did with one frame of a temporal unit. +/// +/// Two outcomes rather than a bare `Ok(())`, because "the plan was honoured" and +/// "the decoder is waiting for a key frame and did nothing" are opposite +/// statements about the rung, and the caller has to count the second: a unit in +/// which EVERY frame was skipped produced no picture at all and comes back as +/// [`VkDecodeError::AwaitingKeyAv1`], while a unit where a key frame cleared the +/// wait partway through decoded normally (see [`VkAv1Decoder::awaiting_key`]). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FrameOutcome { + /// The plan was carried out: submitted, or (a `show_existing_frame`) settled + /// into a display verdict without a submission. Either way the unit produced + /// this frame. + Decoded, + /// Skipped: the decoder is waiting for the next key frame after a failure and + /// this frame is undecodable by construction. + SkippedAwaitingKey, +} + +/// Everything tied to ONE AV1 session generation. A stream renegotiation (extent +/// or profile — including a bit-depth, sampling or film-grain switch) retires it +/// and builds fresh. +struct SessionStateAv1 { + session: VideoSessionAv1, + slots: SlotMap, + /// Distinct mode's reference-only DPB backing; `None` in coincide mode (the + /// picture pool backs the DPB there). + dpb: Option, + pool: PicturePool, + ring: BitstreamRing, + ops: OpRing, + /// Last-known Std reference info per DPB slot — `vkCmdBeginVideoCodingKHR` + /// wants codec reference info for EVERY bound slot, including ones this frame + /// does not reference; refreshed from each plan's setup/ref entries. + slot_refs: Vec>, + /// Coincide mode: which pool image each DPB slot currently binds (rebound at + /// every activation — the decoupling that keeps delivered images safe). + slot_image: Vec>, + /// Per command-buffer completion tokens (reuse gate). + cmd_marks: Vec>, + /// Per query-slot submission ordinals (staleness validation). + query_marks: Vec, + /// Submissions recorded on this session (cmd/query indexing). + submitted: u64, + /// The newest submission's completion token (session drain). + last_submit: Option<(vk::Semaphore, u64)>, + /// The STREAM's coded extent (renegotiation comparison). + coded_extent: vk::Extent2D, + /// The granularity-aligned allocation extent (picture resources + frames). + image_extent: vk::Extent2D, +} + +/// The native Vulkan Video AV1 decoder. Mirrors [`crate::VkH265Decoder`]'s public +/// surface method-for-method. +pub struct VkAv1Decoder { + dev: DecodeDevice, + lock: Box, + planner: Av1Planner, + /// Caps per profile key, queried once per profile (a bit-depth or film-grain + /// switch is a different key and re-queries). + caps: Option<(Av1ProfileKey, DecodeCaps)>, + state: Option, + /// Decoded pictures awaiting their planner output verdict, keyed by [`PicId`]. + /// For AV1 this holds the HIDDEN frames: a `show_frame` picture is settled into + /// `ready` by the very plan that decoded it. + pending: BTreeMap, + /// Display-ready frames not yet handed out. Genuinely deeper than one here: a + /// temporal unit carrying several shown frames makes several ready at once. + ready: VecDeque, + /// Retired generations' pools with consumer-held images (die on their last + /// release token). + graveyard: Vec, + /// The most recent access unit's warnings ([`Self::take_warnings`]) — the whole + /// temporal unit's, concatenated in decode order. + last_warnings: Vec, + /// Pictures decoded so far — stamped onto each one as + /// [`DecodedVkFrame::decode_order`]. Survives session rebuilds because it + /// describes the STREAM, not the Vulkan objects. + decoded: u64, + /// Session generation: bumped on every rebuild, stamped into frames. + generation: u64, + device_lost: bool, + /// Recovery owed after a failed frame whose planning had already advanced + /// ([`RecoveryLatch`] docs for the whole argument). + recovery: RecoveryLatch, + /// Every frame until the next KEY frame is undecodable, and is skipped rather + /// than converted. + /// + /// This exists because AV1's planner has no `flush`: when a failure forces + /// [`Self::recover_dpb`] to empty this decoder's slot ledger and image + /// bindings, the PLANNER's own eight-slot store still believes those pictures + /// are resident and keeps handing out inter frames that reference them. Each + /// would fail in `plan_to_vk_av1` with `UnresolvedReference` — a per-frame + /// failure whose message describes a phantom reference gap rather than the + /// wait that is really in progress, and which would drag every one of those + /// frames through a conversion that cannot succeed. + /// + /// So the frames are skipped. What they are NOT is laundered into a clean + /// answer: a temporal unit in which every frame was skipped comes back as + /// [`VkDecodeError::AwaitingKeyAv1`], once per access unit, exactly as the + /// H.264/H.265 decoders answer the same wait with their planners' + /// `PlanError::AwaitingIdr`. The three codecs must be indistinguishable here, + /// because the consumer's demotion streak is the only thing that turns "this + /// rung produces no picture" into "fall through to the next rung": a clean + /// `Ok(None)` RESETS that streak once per frame, so a rung whose every key + /// frame fails would never reach the threshold and the session would keep a + /// frozen screen with a clean bill of health. During a recovery wait the + /// decoder really has stopped working, and that is what the streak must see. + /// + /// A DECODED key frame (which references nothing and refreshes all eight + /// slots) clears it and decoding resumes — including one that arrives partway + /// through a temporal unit, which is why the skip is per FRAME while the error + /// is per ACCESS UNIT. + awaiting_key: bool, +} + +impl VkAv1Decoder { + /// Wrap the borrowed device. Sessions/pools are built lazily from the first + /// frame's sequence header (their shape is the stream's, not the device's). + /// + /// # Safety + /// + /// The full [`DeviceHandles`] caller contract (liveness, enabled extensions + /// and features, truthful queue families) — held for this decoder's whole + /// lifetime, not just this call. The device must additionally have been + /// created with `VK_KHR_video_decode_av1` enabled; that part of the contract + /// is checked below AS FAR AS IT CAN BE — the check reads the decode queue + /// family's advertised `videoCodecOperations`, which is the device's own claim + /// about the family, not proof that the client enabled the extension at + /// `vkCreateDevice`. Getting it wrong is undefined behaviour at session + /// creation rather than an error, which is why the family check runs before + /// anything is queried or created. + pub unsafe fn new( + handles: &DeviceHandles, + lock: Box, + ) -> Result { + // SAFETY: forwarded caller contract. + let dev = unsafe { DecodeDevice::wrap(handles)? }; + dev.require_codec_op(vk::VideoCodecOperationFlagsKHR::DECODE_AV1, "AV1 decode")?; + Ok(Self { + dev, + lock, + planner: Av1Planner::new(), + caps: None, + state: None, + pending: BTreeMap::new(), + ready: VecDeque::new(), + graveyard: Vec::new(), + last_warnings: Vec::new(), + decoded: 0, + generation: 0, + device_lost: false, + recovery: RecoveryLatch::default(), + awaiting_key: false, + }) + } + + /// Ask the device, BEFORE a single AU is fed, whether it can decode a stream of + /// the negotiated shape — the construction-time half of what the lazy + /// `ensure_state` path would otherwise only discover at the first sequence + /// header. + /// + /// `film_grain` is the load-bearing argument. Grain synthesis is part of the + /// AV1 decode PROFILE, and a device that decodes AV1 need not offer the + /// grain-enabled one; discovering that lazily makes the refusal a mid-stream + /// error streak, which demotes past the FFmpeg rungs, where discovering it here + /// is a construction failure the client's ladder answers by falling through to + /// the next rung with the session's hardware decode intact. + /// + /// The negotiated facts are a HINT (the in-band sequence header is + /// authoritative), so this is deliberately not a promise that decode will + /// succeed: the level ceiling and a sequence header that disagrees with the + /// Welcome still surface at the first AU. + pub fn probe_stream_support( + &self, + chroma_format_idc: u8, + bit_depth: u8, + film_grain: bool, + ) -> Result<(), VkDecodeError> { + let key = Av1ProfileKey::from_negotiated(chroma_format_idc, bit_depth, film_grain)?; + // SAFETY: the constructor's `DeviceHandles` contract holds for this + // decoder's whole lifetime, so the physical device is live — the same + // proof `ensure_state`'s identical call carries. + let raw = + unsafe { query_av1_caps(&self.dev, key) }.map_err(|r| caps_query_error(r, key))?; + let wanted = key + .output_format() + .expect("from_negotiated gated the sampling/depth combination"); + derive_caps_av1(&raw, wanted)?; + Ok(()) + } + + /// Decode one access unit — one TEMPORAL UNIT, which may carry several frames. + /// Returns the next display-ready frame, if the planner declared one; drain the + /// rest with [`Self::take_ready`]. + /// + /// A temporal unit whose every frame was skipped while [`Self::awaiting_key`] + /// is set comes back as [`VkDecodeError::AwaitingKeyAv1`] — the same kind of + /// answer the H.264/H.265 decoders give for the same wait, and for the reason + /// [`Self::awaiting_key`]'s docs carry. A `show_existing_frame` naming an empty + /// slot is NOT that: the planner reports it as a warning and it simply displays + /// nothing. + /// + /// Never panics. `VkDecodeError::DeviceLost` latches: every later call fails + /// fast until the owner rebuilds the decoder on fresh handles. + pub fn decode(&mut self, au: &[u8]) -> Result, VkDecodeError> { + if self.device_lost { + return Err(VkDecodeError::DeviceLost); + } + let result = self.decode_inner(au); + if matches!(result, Err(VkDecodeError::DeviceLost)) { + self.device_lost = true; + } + result + } + + fn decode_inner(&mut self, au: &[u8]) -> Result, VkDecodeError> { + // A previous frame failed after its planning had advanced: clear the stale + // DPB residency BEFORE planning this AU, or every later frame referencing + // the stranded picture fails forever ([`RecoveryLatch`] docs). + if self.recovery.take() { + self.recover_dpb(); + } + // Cleared BEFORE planning so an AU that fails to PLAN cannot leave the + // previous one's warnings to be re-read as fresh damage. + self.last_warnings.clear(); + let plans = match self.planner.plan_au(au) { + Ok(plans) => plans, + Err(e) => return Err(VkDecodeError::PlanAv1(e)), + }; + // The whole temporal unit's warnings, in decode order — `take_warnings` + // answers per ACCESS UNIT, and one unit's frames share a concealment + // verdict as far as the integration layer is concerned. + for plan in &plans { + for warning in &plan.warnings { + trace!(?warning, "plan warning"); + } + self.last_warnings.extend(plan.warnings.iter().cloned()); + } + + let mut skipped = 0usize; + for plan in &plans { + // From here the PLANNER has already advanced past this frame — its + // store holds the picture whatever happens next — so any failure below + // leaves the planner's store and this decoder's ledgers able to + // disagree. Latch the recovery rather than returning into a permanently + // wedged state. + match self.decode_planned(plan, au) { + Ok(FrameOutcome::Decoded) => {} + Ok(FrameOutcome::SkippedAwaitingKey) => skipped += 1, + Err(e) => { + self.recovery.latch(); + return Err(e); + } + } + } + // Nothing in this unit decoded and nothing was displayed, because the + // decoder is still waiting for a key frame. That is an ERROR per access + // unit — [`VkDecodeError::AwaitingKeyAv1`] and [`Self::awaiting_key`] carry + // the argument — and deliberately not a latch: `recover_dpb` has already + // run, the ledgers are consistent, and re-latching would re-flush an empty + // ledger once per frame for the whole wait. + // + // Counted rather than short-circuited inside the loop, because a key frame + // may sit BEHIND a skipped frame in the same temporal unit: returning at + // the first skip would never reach it, and the wait would never end. + if whole_unit_skipped(plans.len(), skipped) { + return Err(VkDecodeError::AwaitingKeyAv1); + } + Ok(self.ready.pop_front()) + } + + /// One planned frame of a temporal unit. + fn decode_planned(&mut self, plan: &AuPlan, au: &[u8]) -> Result { + // A key frame re-anchors everything: it references nothing and refreshes + // all eight slots, so it is decodable no matter what came before. + // + // A DECODED one, specifically. `show_existing_frame` of a key frame also + // resets the planner's store (7.20) but decodes nothing, so it leaves this + // decoder with an empty ledger against a full planner store — resuming + // there would fail on the very next inter frame and re-arm the wait, one + // error per frame, which is the storm this flag exists to avoid. + if self.awaiting_key && clears_awaiting_key(plan) { + debug!("AV1 key frame reached — decoding resumes"); + self.awaiting_key = false; + } + if self.awaiting_key { + trace!( + show_existing = plan.dpb.stored.is_none(), + "frame skipped while awaiting the next AV1 key frame" + ); + return Ok(FrameOutcome::SkippedAwaitingKey); + } + + // `show_existing_frame`: no decode at all. It displays a slot's contents — + // a picture some earlier hidden frame put there — so its DPB verdicts are + // settled and nothing is submitted. + let Some(setup_id) = plan.dpb.stored else { + self.settle(&plan.dpb.outputs, &plan.dpb.removed); + if let Some(state) = &mut self.state { + for &id in &plan.dpb.removed { + state.slots.release(id); + } + } + // Decoded: nothing was submitted, but the plan was HONOURED — it + // declared a picture displayable, which is a frame the unit produced. + return Ok(FrameOutcome::Decoded); + }; + + // A reference the planner could not resolve: refuse before anything is + // converted (module docs, and [`lost_reference`]). + if let Some((slot, ref_index)) = lost_reference(&plan.warnings) { + return Err(VkDecodeError::MissingReferenceAv1 { slot, ref_index }); + } + + // One picture per plan: stamp its DECODE-order ordinal before anything can + // reorder it (see `DecodedVkFrame::decode_order`). + self.decoded = self.decoded.saturating_add(1); + let decode_order = self.decoded; + + self.ensure_state(plan)?; + + // A parameters RECREATE over an EXISTING object destroys it, which an + // in-flight decode may still be executing against: drain first. The FIRST + // one of a session's life destroys nothing (the session is created without + // a parameters object — `session_av1` module docs) and needs no drain. + { + let session = &self.state.as_ref().expect("ensure_state built it").session; + if session.parameters_action(&plan.sequence) == ParamsActionAv1::Recreate + && session.has_parameters() + { + self.drain_gpu()?; + } + } + let state = self.state.as_mut().expect("ensure_state built it"); + // SAFETY: live device (constructor contract); the drain above satisfies + // ensure_parameters' Recreate contract, and Current touches nothing a + // submitted decode reads. + unsafe { state.session.ensure_parameters(&plan.sequence)? }; + + // The bitstream layout, decided BEFORE the DPB ledger is touched: a + // malformed tile group must not leave a half-applied slot map behind. + let bitstream = + plan_bitstream(au, &plan.tiles, &plan.header).map_err(VkDecodeError::TilesAv1)?; + + let vk_plan = plan_to_vk_av1(plan, &mut state.slots).map_err(VkDecodeError::ConvertAv1)?; + + // The per-AU active-reference gate: the session was created with + // maxActiveReferencePictures; binding more in one decode op would be a + // silent VUID violation on the drivers that matter most. + let max_active = state.session.config.max_active_references as usize; + if vk_plan.refs.len() > max_active { + return Err(VkDecodeError::Unsupported(format!( + "frame references {} pictures, session allows {max_active} active references", + vk_plan.refs.len() + ))); + } + + // Coincide binding sync: slots the planner released no longer bind their + // images (the pictures may still be pending/held — untouched), and the + // setup slot's PREVIOUS binding is cleared before it binds fresh. + let setup = usize::from(vk_plan.setup_slot); + if state.dpb.is_none() { + let unbound = + sync_slot_bindings(&state.slots, &mut state.slot_image, vk_plan.setup_slot); + for picture in unbound { + state.pool.pictures[picture].bound = false; + } + } + + // The decode target: a FREE pool image (never one a consumer holds — the + // whole point of the pool model). + let Some(dst) = state.pool.free_index() else { + debug!( + held = state.pool.held_total(), + "picture pool exhausted — release_frame owed" + ); + return Err(VkDecodeError::NoFreeSlot); + }; + + // Cross-queue waits (the AVVkFrame contract): the dst image's last known + // timeline value (covers a presenter write-back after release), plus — + // coincide mode — every referenced image's value, so reference reads order + // after any presenter layout restore already reported back. + let mut waits: Vec<(vk::Semaphore, u64)> = Vec::new(); + { + let dst_pic = &state.pool.pictures[dst]; + if dst_pic.value > 0 { + waits.push((dst_pic.semaphore, dst_pic.value)); + } + } + if state.dpb.is_none() { + for r in &vk_plan.refs { + if let Some(picture) = state.slot_image[usize::from(r.slot)] { + let pic = &state.pool.pictures[picture]; + if pic.value > 0 && !waits.iter().any(|(sem, _)| *sem == pic.semaphore) { + waits.push((pic.semaphore, pic.value)); + } + } + } + } + let signal_value = state.pool.pictures[dst].value + 1; + + // Command buffer + query slot for this submission. + let submission = state.submitted; + let cmd_index = (submission % state.ops.cmds.len() as u64) as usize; + if let Some((sem, value)) = state.cmd_marks[cmd_index] { + // SAFETY: live device; the token is a pool image's semaphore. + unsafe { wait_timeline(self.dev.ash(), sem, value, "command buffer reuse")? }; + } + let query_index = (submission % u64::from(state.ops.query_count)) as u32; + + // Upload the raw TILE PAYLOADS — nothing else goes in the buffer (module + // docs) — recycling/growing the ring against submission-completion tokens. + // AV1 has no start codes and nothing to strip: the tiles go in verbatim. + let Some(packed) = pack_av1_tiles(&bitstream.tiles) else { + return Err(VkDecodeError::Unsupported( + "packed tile data exceeds the u32 offsets Vulkan submits".into(), + )); + }; + let tiles = submitted_tiles(&packed).map_err(VkDecodeError::TilesAv1)?; + + let device = self.dev.ash().clone(); + let mut poll = |token: &(vk::Semaphore, u64)| -> Result { + // SAFETY: live device; the token's semaphore is a pool semaphore. + let current = unsafe { device.get_semaphore_counter_value(token.0) } + .map_err(VkDecodeError::from)?; + Ok(current >= token.1) + }; + let device2 = self.dev.ash().clone(); + let mut wait = |token: &(vk::Semaphore, u64)| -> Result<(), VkDecodeError> { + // SAFETY: as above. + unsafe { wait_timeline(&device2, token.0, token.1, "bitstream slot drain") } + }; + // SAFETY: live device; the segments are the plan's own in-bounds OBU + // ranges; every pending token is the completion signal of the submission + // that consumed the slot. + let upload = unsafe { + state + .ring + .upload(&self.dev, au, &packed.segments, &mut poll, &mut wait)? + }; + + // Record + submit, signalling the dst image's next timeline value. + // SAFETY: live device; every handle recorded below belongs to this session + // generation, and the packed OBUs sit uploaded in the ring slot. + unsafe { + record_and_submit_av1( + &self.dev, + &*self.lock, + state, + &vk_plan, + &tiles, + &upload, + dst, + cmd_index, + query_index, + &waits, + signal_value, + )?; + } + + // Post-submit bookkeeping. + let dst_sem = state.pool.pictures[dst].semaphore; + state.pool.pictures[dst].value = signal_value; + state.pool.pictures[dst].pending = true; + if state.dpb.is_none() { + state.pool.pictures[dst].bound = true; + state.slot_image[setup] = Some(dst); + } + state.cmd_marks[cmd_index] = Some((dst_sem, signal_value)); + state.query_marks[query_index as usize] = submission; + state.submitted += 1; + state.last_submit = Some((dst_sem, signal_value)); + state + .ring + .pending + .set_pending(upload.slot, (dst_sem, signal_value)); + + // Refresh the per-slot reference cache from this frame's facts. + state.slot_refs[setup] = Some(vk_plan.setup_ref); + for r in &vk_plan.refs { + state.slot_refs[usize::from(r.slot)] = Some(r.std); + } + + // The slots this frame's own refresh displaced while it was still READING + // them. Held through the conversion and the submission above so neither the + // setup assignment nor the binding sync could take them + // (`DecodePlanVkAv1::release_after_decode`); free now that the decode op is + // recorded, so the next frame may have them. Their pool images stay pinned + // by `bound` until that frame's sync, which is the same one-frame grace + // every other released slot's image gets. + for &id in &vk_plan.release_after_decode { + if !state.slots.release(id) { + trace!(id, "deferred release of an id the slot map no longer holds"); + } + } + + self.pending.insert( + vk_plan.setup_id, + PendingPic { + image: dst, + submission, + query_slot: query_index, + timeline_value: signal_value, + crop: DisplayCrop { + x: 0, + y: 0, + // AV1's display region is `render_width`/`render_height`, its + // answer to a conformance window — the decoded picture is the + // (post-superres) `upscaled_width` x `frame_height`. + // + // ⚠ CLAMPED, because AV1's render size is a display HINT and + // not a window: 5.9.6 puts no upper bound on + // `render_width_minus_1`, so a stream may legally ask to be + // shown at more than it coded (that is how a decoder is told to + // upscale on output). Used as a crop unclamped it addresses + // rows and columns the decoded image does not have. + width: plan.picture.render_width.min(plan.picture.upscaled_width), + height: plan.picture.render_height.min(plan.picture.frame_height), + }, + colour: plan.picture.colour, + // AV1 has no POC. `OrderHint` is the closest thing the stream + // states and is what a consumer ordering frames would compare; + // it is a small wrapping counter, not a monotone one. + poc: plan.picture.order_hint as i32, + // AV1's re-anchor point is the KEY frame — there is no IDR and no + // recovery point SEI, so this is the only clean point a consumer + // freezing on loss ever sees. + is_idr: plan.picture.is_key, + recovery: crate::recovery::RecoveryMark::NONE, + decode_order, + }, + ); + + // The plan's DPB verdicts over the pending map. + self.settle(&plan.dpb.outputs, &plan.dpb.removed); + + // A frame that refreshes NO slot enters the planner's store nowhere, so the + // planner can never report it removed — while `plan_to_vk_av1` did assign + // it a slot in this decoder's ledger. Left alone that slot is held for the + // session's whole life, and nine such frames exhaust the ledger with + // `SlotError::Full`. It is legal AV1 (a frame shown once and never + // referenced), it does not occur in the vendored vector, and it costs one + // release to close. + if plan.header.refresh_frame_flags == 0 { + let state = self.state.as_mut().expect("ensured above"); + state.slots.release(setup_id); + // If it was not shown either, nothing can ever display or reference it: + // free its image instead of leaving the picture pending forever. + if let Some(entry) = self.pending.remove(&setup_id) { + trace!( + id = setup_id, + "frame refreshes no slot and is not shown — freeing its image" + ); + state.pool.pictures[entry.image].pending = false; + } + } + Ok(FrameOutcome::Decoded) + } + + /// Apply one plan's DPB verdicts: outputs become ready frames (their images + /// move pending → held), removed-but-never-shown pictures free their images. + fn settle(&mut self, outputs: &[PicId], removed: &[PicId]) { + let (ready, dropped) = settle_dpb_ids(&mut self.pending, outputs, removed); + let Some(state) = self.state.as_mut() else { + return; + }; + for entry in ready { + let frame = build_frame( + &mut state.pool, + state.dpb.is_none(), + state.image_extent, + &entry, + self.generation, + ); + self.ready.push_back(frame); + } + for entry in dropped { + debug!( + order_hint = entry.poc, + "picture displaced from every slot without being shown — freeing its image" + ); + state.pool.pictures[entry.image].pending = false; + } + } + + /// Hand a delivered frame back. `presenter_signaled` reports whether the + /// consumer SAMPLED the image (and therefore enqueued the `value + 1` timeline + /// signal per the [`DecodedVkFrame`] contract) — the decoder then waits that + /// write-back before the image's next use. Every frame `decode`/`take_ready` + /// returns must come back exactly once, including stale-generation frames + /// (their retired pool dies on its last release token). + pub fn release_frame( + &mut self, + frame: &DecodedVkFrame, + presenter_signaled: bool, + ) -> Result<(), VkDecodeError> { + let pool = if frame.generation == self.generation { + match &mut self.state { + Some(state) => &mut state.pool, + None => { + return Err(VkDecodeError::StaleFrame { + frame_generation: frame.generation, + current_generation: self.generation, + }) + } + } + } else { + match self + .graveyard + .iter_mut() + .find(|r| r.generation == frame.generation) + { + Some(retired) => &mut retired.pool, + None => { + return Err(VkDecodeError::StaleFrame { + frame_generation: frame.generation, + current_generation: self.generation, + }) + } + } + }; + let index = frame.picture as usize; + if index >= pool.pictures.len() { + return Err(VkDecodeError::StaleFrame { + frame_generation: frame.generation, + current_generation: self.generation, + }); + } + let picture = &mut pool.pictures[index]; + match picture.held.checked_sub(1) { + Some(remaining) => picture.held = remaining, + None => { + debug!(index, "frame released more often than delivered"); + return Ok(()); + } + } + if presenter_signaled { + picture.value = picture.value.max(frame.value + 1); + } + // A retired pool dies on its last token (presenter fence-waited before the + // token per the release contract; decode work drained at retirement). + if frame.generation != self.generation { + self.graveyard + .retain(|r| r.generation != frame.generation || r.pool.held_total() > 0); + } + Ok(()) + } + + /// A display-ready frame beyond the one `decode` returned, if any. Drain after + /// every decode; frames left here still occupy pool images. Genuinely needed on + /// AV1: one temporal unit can make several frames ready. + pub fn take_ready(&mut self) -> Option { + self.ready.pop_front() + } + + /// The warnings of the most recent successfully planned access unit — every + /// frame's, concatenated in decode order (concealment signals: the integration + /// layer's want_keyframe hook). Cleared by the next `decode`. + pub fn take_warnings(&mut self) -> Vec { + std::mem::take(&mut self.last_warnings) + } + + /// The current session generation ([`DecodedVkFrame::generation`] of newly + /// delivered frames). + pub fn generation(&self) -> u64 { + self.generation + } + + /// The DECODE-order ordinal of the most recently decoded picture — the + /// watermark a consumer compares [`DecodedVkFrame::decode_order`] against to + /// tell a frame decoded before a loss from one decoded after it. 0 before the + /// first frame decodes; `show_existing_frame` plans do not advance it, because + /// they decode nothing. + pub fn decode_order(&self) -> u64 { + self.decoded + } + + /// One-line state snapshot for failure paths and field logs (not a stable + /// format). + pub fn debug_snapshot(&self) -> String { + let recovery = if self.recovery.is_latched() { + " recovery=owed" + } else { + "" + }; + let awaiting = if self.awaiting_key { + " awaiting=key" + } else { + "" + }; + match &self.state { + None => format!("gen={}{recovery}{awaiting} ", self.generation), + Some(state) => { + let occupancy: Vec = state + .pool + .pictures + .iter() + .enumerate() + .map(|(i, p)| { + format!( + "{i}:{}{}h{}", + if p.bound { "B" } else { "-" }, + if p.pending { "P" } else { "-" }, + p.held + ) + }) + .collect(); + format!( + "av1 gen={}{recovery}{awaiting} mode={} slots_held={}/{} pool=[{}] \ + pending={} ready={} graveyard={}", + self.generation, + if state.dpb.is_none() { + "coincide" + } else { + "distinct" + }, + state.slots.active(), + state.slots.capacity(), + occupancy.join(" "), + self.pending.len(), + self.ready.len(), + self.graveyard.len(), + ) + } + } + } + + /// Read `frame`'s decode status WITHOUT waiting. + /// + /// [`DecodeStatus::Failed`] covers driver-reported errors AND a query slot + /// re-armed before it was read (the status is then unprovable — same + /// conservative verdict). + /// + /// On drivers whose decode family lacks `queryResultStatusSupport` (RADV) + /// there is no per-op verdict to read: `Ok` then means "the decode op + /// COMPLETED on the timeline" — the same information FFmpeg has on every + /// driver, no worse. + pub fn poll_status(&mut self, frame: &DecodedVkFrame) -> DecodeStatus { + self.read_status(frame, false) + } + + /// Does this decode queue family answer per-op `RESULT_STATUS` queries at all? + /// The fact is the DEVICE's, identical for every codec, and it is what tells a + /// clean integrity report apart from an undetectable one. + pub fn status_queries(&self) -> bool { + self.dev.result_status_queries() + } + + /// [`Self::poll_status`], but WAITs for the op to complete first. + pub fn wait_status(&mut self, frame: &DecodedVkFrame) -> DecodeStatus { + self.read_status(frame, true) + } + + fn read_status(&mut self, frame: &DecodedVkFrame, block: bool) -> DecodeStatus { + if frame.generation != self.generation { + trace!( + frame_generation = frame.generation, + current = self.generation, + "status asked for a stale-generation frame — Failed, without \ + touching the new pools" + ); + return DecodeStatus::Failed; + } + let Some(state) = &self.state else { + return DecodeStatus::Failed; + }; + let Some(query_pool) = state.ops.query_pool else { + // No queries on this driver: the verdict degrades to timeline + // completion (poll_status docs). + if block { + // SAFETY: live device; pool-owned semaphore. + return match unsafe { + wait_timeline(self.dev.ash(), frame.semaphore, frame.value, "status wait") + } { + Ok(()) => DecodeStatus::Ok, + Err(VkDecodeError::DeviceLost) => { + self.device_lost = true; + DecodeStatus::Failed + } + Err(_) => DecodeStatus::Failed, + }; + } + // SAFETY: live device; pool-owned semaphore. + return match unsafe { self.dev.ash().get_semaphore_counter_value(frame.semaphore) } { + Ok(current) if current >= frame.value => DecodeStatus::Ok, + Ok(_) => DecodeStatus::Pending, + Err(vk::Result::ERROR_DEVICE_LOST) => { + self.device_lost = true; + DecodeStatus::Failed + } + Err(_) => DecodeStatus::Failed, + }; + }; + let slot = frame.query_slot as usize; + if slot >= state.query_marks.len() || state.query_marks[slot] != frame.submission { + trace!( + slot, + "status query slot re-armed before it was read — unprovable, reported Failed" + ); + return DecodeStatus::Failed; + } + let flags = if block { + vk::QueryResultFlags::WAIT | vk::QueryResultFlags::WITH_STATUS_KHR + } else { + vk::QueryResultFlags::WITH_STATUS_KHR + }; + let mut status = [0i32; 1]; + // SAFETY: live device; the query pool is this session generation's own and + // `frame.query_slot` indexes within its count (checked above against the + // marks array it is sized to). + let result = unsafe { + self.dev + .ash() + .get_query_pool_results(query_pool, frame.query_slot, &mut status, flags) + }; + match result { + // VkQueryResultStatusKHR: >0 complete, 0 not ready, <0 error. + Ok(()) if status[0] > 0 => DecodeStatus::Ok, + Ok(()) if status[0] == 0 => DecodeStatus::Pending, + Ok(()) => DecodeStatus::Failed, + Err(vk::Result::NOT_READY) => DecodeStatus::Pending, + Err(vk::Result::ERROR_DEVICE_LOST) => { + self.device_lost = true; + DecodeStatus::Failed + } + Err(r) => { + debug!(?r, "status query read failed"); + DecodeStatus::Failed + } + } + } + + /// Wait — bounded by `timeout_ns` — for a delivered frame's decode-complete + /// signal. Pure measurement (the integration layer's sampled decode-latency + /// stat): touches no decoder state. `frame` must be unreleased, which pins its + /// pool — and with it the semaphore — alive. + pub fn wait_decoded(&self, frame: &DecodedVkFrame, timeout_ns: u64) -> bool { + if frame.generation != self.generation { + return false; + } + let semaphores = [frame.semaphore]; + let values = [frame.value]; + let info = vk::SemaphoreWaitInfo::default() + .semaphores(&semaphores) + .values(&values); + // SAFETY: live device (constructor contract); the semaphore is a pool + // semaphore the unreleased frame keeps alive (fn docs); the info arrays + // are locals outliving the call. + unsafe { self.dev.ash().wait_semaphores(&info, timeout_ns) }.is_ok() + } + + /// Drain this decoder (teardown / stream discontinuity). + /// + /// AV1's flush is a DISCARD, not a bump, and that is the codec's doing rather + /// than a shortcut: there is no reorder buffer and no bumping process, so a + /// picture still `pending` here is a HIDDEN frame — one the stream decoded with + /// `show_frame = 0` and would only ever have displayed through a later + /// `show_existing_frame`. Handing those to the consumer would show frames the + /// stream deliberately hid, out of order. Their images are freed instead. + /// + /// The decoder is left [`Self::awaiting_key`], because the PLANNER's own + /// eight-slot store is untouched by this (it has no `flush`) and now disagrees + /// with an emptied ledger — see that field's docs. + pub fn flush(&mut self) { + if let Some(state) = &mut self.state { + for (_, entry) in std::mem::take(&mut self.pending) { + state.pool.pictures[entry.image].pending = false; + } + let unbound = reset_slot_bindings( + &mut state.slots, + &mut state.slot_image, + &mut state.slot_refs, + ); + for picture in unbound { + state.pool.pictures[picture].bound = false; + } + } else { + self.pending.clear(); + } + self.awaiting_key = true; + } + + /// Clear the DPB state a failed frame left behind, so decoding resumes at the + /// next key frame instead of erroring on residency nothing can honour. + /// + /// Three ledgers have to agree and, after a post-planning failure, do not: the + /// PLANNER's eight-slot store, this decoder's [`SlotMap`], and the slot→image + /// bindings. [`Self::flush`] empties the last two (and arms + /// [`Self::awaiting_key`], which covers the first — the planner keeps its store + /// and is simply not asked to decode anything until the key frame refreshes it). + /// + /// Deliberately not a session rebuild: the session, pools and ring are all + /// still valid — only the DPB bookkeeping is stale — and a rebuild would churn + /// every image allocation for a condition a key frame fixes anyway. + fn recover_dpb(&mut self) { + debug!( + snapshot = %self.debug_snapshot(), + "recovering from a failed AV1 frame — skipping to the next key frame" + ); + self.flush(); + } + + /// Session/caps for THIS plan exist and match its extent + profile, and the + /// stream sits inside the device's level ceiling. + fn ensure_state(&mut self, plan: &AuPlan) -> Result<(), VkDecodeError> { + let key = profile_key_for(plan)?; + if self.caps.as_ref().map(|(k, _)| *k) != Some(key) { + let wanted = key + .output_format() + .expect("from_stream gated the sampling/depth combination"); + // SAFETY: live device (constructor contract). + let raw = + unsafe { query_av1_caps(&self.dev, key) }.map_err(|r| caps_query_error(r, key))?; + self.caps = Some((key, derive_caps_av1(&raw, wanted)?)); + } + // The level gate. AV1's `StdVideoAV1Level` is index-coded exactly like the + // bitstream's `seq_level_idx` (2.0 = 0 … 7.3 = 23) and ascends with the + // level, so this is a plain comparison — of AV1 code points against an AV1 + // ceiling, the pairing `MaxLevelIdc`'s tag exists to keep honest. + let caps_max_level = self.caps.as_ref().expect("queried above").1.max_level_idc; + let stream_level = u32::from(stream_level_idx(plan)); + if stream_level > caps_max_level.code_point() { + return Err(VkDecodeError::Unsupported(format!( + "stream level (seq_level_idx {stream_level}) above the device's \ + maxLevel ({caps_max_level})" + ))); + } + let coded = coded_extent(plan); + match &self.state { + Some(state) if state.coded_extent == coded && state.session.config.profile == key => { + Ok(()) + } + _ => self.rebuild_state(plan), + } + } + + /// Tear down the current session generation (draining its decode work, retiring + /// its picture pool to the graveyard when the consumer still holds images) and + /// build a fresh one shaped by `plan`, bumping [`Self::generation`] so frames + /// of the old one route to the graveyard. + fn rebuild_state(&mut self, plan: &AuPlan) -> Result<(), VkDecodeError> { + self.drain_gpu()?; + if let Some(state) = self.state.take() { + debug!("rebuilding AV1 decode session (stream renegotiation)"); + let SessionStateAv1 { mut pool, .. } = state; + for frame in self.ready.drain(..) { + let picture = &mut pool.pictures[frame.picture as usize]; + picture.held = picture.held.saturating_sub(1); + } + for (_, entry) in std::mem::take(&mut self.pending) { + pool.pictures[entry.image].pending = false; + } + for picture in &mut pool.pictures { + picture.bound = false; + } + let held = pool.held_total(); + if held > 0 { + debug!( + held, + generation = self.generation, + "consumer still holds images of the retired generation — graveyarding" + ); + self.graveyard.push(RetiredPool { + generation: self.generation, + pool, + }); + } + } + self.generation += 1; + + let (key, caps) = self.caps.as_ref().expect("ensure_state queried caps"); + let key = *key; + if REQUIRED_SLOTS > caps.max_dpb_slots { + return Err(VkDecodeError::Unsupported(format!( + "AV1 needs {REQUIRED_SLOTS} DPB slots, device caps at {}", + caps.max_dpb_slots + ))); + } + let coded = coded_extent(plan); + // Bounds-checked at the ALLOCATION extent (granularity-rounded): that is + // what the images are created at and what maxCodedExtent must cover. + let image_extent = caps.aligned_extent(coded); + if coded.width < caps.min_coded_extent.width + || coded.height < caps.min_coded_extent.height + || image_extent.width > caps.max_coded_extent.width + || image_extent.height > caps.max_coded_extent.height + { + return Err(VkDecodeError::Unsupported(format!( + "coded extent {}x{} (allocated {}x{}) outside device range {}x{}..{}x{}", + coded.width, + coded.height, + image_extent.width, + image_extent.height, + caps.min_coded_extent.width, + caps.min_coded_extent.height, + caps.max_coded_extent.width, + caps.max_coded_extent.height + ))); + } + + let config = SessionConfigAv1 { + max_coded_extent: image_extent, + max_dpb_slots: REQUIRED_SLOTS, + max_active_references: (REQUIRED_SLOTS - 1).min(caps.max_active_references), + profile: key, + }; + let mut pool_plan = plan_pools(caps, REQUIRED_SLOTS); + // TEST-ONLY readback hook, exactly as the other two decoders': a parity + // test copies decoded pictures back to hash them, and + // `vkCmdCopyImageToBuffer` needs TRANSFER_SRC on the source — a bit the + // zero-copy production pools deliberately do not carry. + if std::env::var("PF_VKD_TEST_READBACK").is_ok_and(|v| v == "1") { + pool_plan.picture_usage |= vk::ImageUsageFlags::TRANSFER_SRC; + } + let decode_profile = DecodeProfile::Av1(key); + // SAFETY: live device per the constructor contract, for every create in + // this block; each created half is owned by a Drop type the moment it + // exists, so a mid-build failure unwinds cleanly. + let state = unsafe { + let session = VideoSessionAv1::create(&self.dev, caps, config)?; + let dpb = if caps.coincide { + None + } else { + Some( + DpbPool::create(&self.dev, caps, &pool_plan, image_extent, decode_profile) + .map_err(VkDecodeError::from)?, + ) + }; + let pool = + PicturePool::create(&self.dev, caps, &pool_plan, image_extent, decode_profile) + .map_err(VkDecodeError::from)?; + let ring = BitstreamRing::create( + &self.dev, + RingLayout::new( + INITIAL_SLOT_SIZE, + RING_SLOTS, + caps.min_bitstream_offset_alignment, + caps.min_bitstream_size_alignment, + ), + decode_profile, + ) + .map_err(VkDecodeError::from)?; + let ops = OpRing::create( + &self.dev, + decode_profile, + pool_plan.picture_count, + RING_SLOTS, + ) + .map_err(VkDecodeError::from)?; + SessionStateAv1 { + session, + slots: SlotMap::new(NUM_REF_SLOTS), + slot_refs: vec![None; REQUIRED_SLOTS as usize], + slot_image: vec![None; REQUIRED_SLOTS as usize], + cmd_marks: vec![None; RING_SLOTS as usize], + query_marks: vec![u64::MAX; pool_plan.picture_count as usize], + submitted: 0, + last_submit: None, + coded_extent: coded, + image_extent, + dpb, + pool, + ring, + ops, + } + }; + self.state = Some(state); + Ok(()) + } + + /// Wait out every in-flight decode submission of the current session. + fn drain_gpu(&mut self) -> Result<(), VkDecodeError> { + let Some(state) = &self.state else { + return Ok(()); + }; + if let Some((sem, value)) = state.last_submit { + // SAFETY: live device; the token is a pool image's semaphore. + unsafe { wait_timeline(self.dev.ash(), sem, value, "session drain")? }; + } + Ok(()) + } +} + +impl Drop for VkAv1Decoder { + fn drop(&mut self) { + // Best-effort decode drain so the pools' Drop impls never destroy in-flight + // decode work; a wedged driver falls through after the bounded timeout. + // Presenter-side sampling of graveyarded/held images is the CALLER's + // teardown contract (the H.264 decoder's Drop docs). + if let Err(e) = self.drain_gpu() { + debug!(error = %e, "drain on drop failed; tearing down anyway"); + } + if !self.graveyard.is_empty() { + debug!( + pools = self.graveyard.len(), + "graveyard not fully token-drained at decoder drop — destroying anyway \ + (upstream teardown forfeited its bounded wait)" + ); + } + } +} + +/// Turn a failed AV1 capabilities query into an error that names the likely cause. +/// +/// A device that decodes AV1 but not the FILM-GRAIN profile answers the very first +/// query with a profile-unsupported result, and that is by far the most probable +/// reason a caps query fails at all here (every other input to it is a shape the +/// profile builder already gated). Saying so is what turns a bare `VkResult` in a +/// field log into the ladder's named demote — and the refusal is deliberate: the +/// alternative, re-querying with grain turned off, would decode the stream's +/// pictures and silently drop the grain the encoder relied on. +fn caps_query_error(r: vk::Result, key: Av1ProfileKey) -> VkDecodeError { + if key.film_grain { + VkDecodeError::Unsupported(format!( + "AV1 decode capabilities query failed with {r:?}; this stream applies film \ + grain, and a device that cannot host the film-grain AV1 decode profile \ + fails exactly here — decoding it without grain is not offered" + )) + } else { + VkDecodeError::from(r) + } +} + +/// The Vulkan profile this frame's stream needs — Std profile, sampling, bit depth +/// and the sequence's film-grain flag, all of which the sequence header carries and +/// the profile must restate. +fn profile_key_for(plan: &AuPlan) -> Result { + Av1ProfileKey::from_stream( + plan.sequence.seq_profile as u8, + plan.picture.chroma_format_idc, + plan.picture.bit_depth, + plan.sequence.film_grain_params_present, + ) + .map_err(VkDecodeError::ParamsAv1) +} + +/// Whether this plan ends an outstanding [`VkAv1Decoder::awaiting_key`] wait. +/// +/// A DECODED key frame, specifically: it references nothing and refreshes all +/// eight reference slots, so it re-anchors both the planner's store and this +/// decoder's ledger in one step. A `show_existing_frame` OF a key frame resets the +/// planner's store too (7.20) while decoding nothing — resuming there would leave +/// an empty ledger against a full store and fail on the very next inter frame. +fn clears_awaiting_key(plan: &AuPlan) -> bool { + plan.picture.is_key && plan.dpb.stored.is_some() +} + +/// Did a temporal unit of `planned` frames produce NOTHING because every one of +/// them was skipped waiting for a key frame — the +/// [`VkDecodeError::AwaitingKeyAv1`] condition? +/// +/// A named function rather than the expression inlined at the call site because +/// both of its edges are load-bearing and neither is obvious: +/// +/// * `planned == 0` is not a skip. A temporal unit can plan no frames at all (one +/// carrying only metadata or a sequence header), and that is an ordinary +/// `Ok(None)` — turning it into an error would fail access units on a perfectly +/// healthy stream. +/// * `skipped < planned` is not a skip either, and this is the case an early +/// return inside the loop would have got wrong: a key frame may sit BEHIND a +/// skipped frame in the same unit, clears the wait when it is reached, and +/// decodes. Reporting the unit as skipped there would answer an error for an +/// access unit that really did decode a picture. +/// +/// Pure, so the aggregation is CPU-testable without a device. +fn whole_unit_skipped(planned: usize, skipped: usize) -> bool { + planned > 0 && skipped == planned +} + +/// The stream's level, as the sequence header's FIRST operating point states it. +/// +/// Operating point 0 is the full stream — the one a non-scalable decoder decodes +/// and the one the vendored parser selects by default. A punktfunk host emits a +/// single operating point. +fn stream_level_idx(plan: &AuPlan) -> u8 { + plan.sequence.operating_points[0].seq_level_idx +} + +/// The extent the decode output has: AV1's superres upscales horizontally AFTER +/// reconstruction, so a superres frame is coded at `frame_width` and comes out at +/// `upscaled_width`, and it is the output the pool images have to hold. +/// +/// It is also the ONE extent a session generation has. Every picture resource a +/// coding scope binds — the setup slot, this frame's references, the other held +/// slots — is described with it, which is sound because `ensure_state` rebuilds +/// the session the moment the extent changes: within a generation no two pictures +/// were decoded at different sizes. +/// +/// The cost of that is worth stating: AV1 permits a mid-sequence frame-size change +/// (`frame_size_override_flag`) with references SCALED to the new size, and this +/// rung answers it with a session rebuild — a fresh, empty slot ledger against a +/// planner store that still holds the old pictures, so the stream re-anchors on the +/// next key frame ([`VkAv1Decoder::awaiting_key`]). Reference scaling is outside +/// the punktfunk envelope (a host renegotiates with a new sequence header, which +/// rebuilds anyway); a stream that used it would decode, with a hitch at each size +/// change rather than a wrong picture. +fn coded_extent(plan: &AuPlan) -> vk::Extent2D { + vk::Extent2D { + width: plan.picture.upscaled_width, + height: plan.picture.frame_height, + } +} + +/// Empty the three per-slot ledgers a recovery resets: DPB residency, the +/// slot→image bindings and the cached per-slot reference info. Returns the pool +/// image indices the cleared bindings were pinning, for the caller to unbind (pure +/// over the ledgers so the recovery is testable without a device — the pool is the +/// one piece that needs one). +/// +/// All three are emptied TOGETHER on purpose: leaving reference info behind would +/// let [`build_scope_av1`] bind a slot the planner no longer knows about, which is +/// the same "plausible-looking picture in the wrong place" the unbound-reference +/// refusal exists to prevent. +fn reset_slot_bindings( + slots: &mut SlotMap, + slot_image: &mut [Option], + slot_refs: &mut [Option], +) -> Vec { + // `release` is the only way a slot is freed (SlotMap docs); the collect is + // because `held` borrows the map the releases mutate. + for (_slot, id) in slots.held().collect::>() { + slots.release(id); + } + let unbound = slot_image.iter_mut().filter_map(Option::take).collect(); + for cached in slot_refs.iter_mut() { + *cached = None; + } + unbound +} + +/// Coincide-mode binding sync, run once per frame between the plan conversion and +/// the decode target's allocation: a slot the ledger no longer holds stops binding +/// its pool image, and the setup slot's PREVIOUS binding is cleared before it binds +/// fresh. Returns the pool images that lost a binding — the caller clears their +/// `bound` flag, which is what puts them back in reach of the free list. +/// +/// The pictures themselves are untouched: one still `pending` or held by a consumer +/// stays off the free list on those flags alone (the decoupled-pool contract in +/// [`crate::images`]). +/// +/// A free function rather than four lines inline, because it is half of the +/// invariant `build_scope_av1` refuses on: a slot this frame REFERENCES must still +/// bind an image once this has run. Driving the two together over the vendored +/// vector is what `slot_recycling_waits_for_the_decode_op` does, and what no +/// hardware-free test could do while this lived inside `decode_planned`. +fn sync_slot_bindings( + slots: &SlotMap, + slot_image: &mut [Option], + setup_slot: u8, +) -> Vec { + let mut held = vec![false; slot_image.len()]; + for (slot, _id) in slots.held() { + held[usize::from(slot)] = true; + } + let setup = usize::from(setup_slot); + let mut unbound = Vec::new(); + for (slot, binding) in slot_image.iter_mut().enumerate() { + if binding.is_some() && (!held[slot] || slot == setup) { + unbound.extend(binding.take()); + } + } + unbound +} + +/// The picture resource view bound for DPB `slot`: the bound pool image (coincide) +/// or the DPB array layer (distinct). +fn slot_view(state: &SessionStateAv1, slot: u8) -> Option { + match &state.dpb { + Some(dpb) => Some(dpb.dpb_view(slot)), + None => state.slot_image[usize::from(slot)].map(|p| state.pool.pictures[p].view), + } +} + +/// One entry of a coding scope's bound-slot list: the DPB slot index it binds +/// (`-1` for the setup ACTIVATION entry), the picture resource view, and the codec +/// reference info that slot's association carries. +/// (No derived equality: `StdVideoDecodeAV1ReferenceInfo` is a plain-C bindgen +/// struct without it. Assertions compare the fields that carry meaning.) +#[derive(Debug, Clone, Copy)] +struct ScopeEntryAv1 { + slot_index: i32, + view: vk::ImageView, + std: hh::StdVideoDecodeAV1ReferenceInfo, +} + +/// Build the coding scope's bound-slot list and say how many leading entries are +/// this frame's references. +/// +/// The layout: +/// +/// 1. every entry of `refs`, IN ORDER — the decode op takes exactly this prefix; +/// 2. every other still-held slot, so its association survives the scope; +/// 3. the setup slot as the activation entry, slot index `-1`. +/// +/// Two things fail the whole op rather than being skipped: +/// +/// - a reference whose slot binds no image — `referenceNameSlotIndices` names DPB +/// SLOTS, and dropping the entry that binds one leaves the hardware with a named +/// slot this op never bound; +/// - a `referenceNameSlotIndices` entry naming a slot the reference list does NOT +/// bind. That is the Vulkan rule stated the other way round (every non-negative +/// entry must equal the `slotIndex` of one of `pReferenceSlots`), and checking it +/// here is what would have caught the HEVC RPS class at the point of submission +/// rather than on a driver. +#[allow(clippy::too_many_arguments)] +fn build_scope_av1( + refs: &[VkRefAv1], + reference_name_slot_indices: &[i32], + held_slots: impl Iterator, + setup_slot: u8, + setup_view: vk::ImageView, + setup_ref: hh::StdVideoDecodeAV1ReferenceInfo, + slot_refs: &[Option], + view_of: impl Fn(u8) -> Option, +) -> Result<(Vec, usize), VkDecodeError> { + let mut scope: Vec = Vec::with_capacity(refs.len() + slot_refs.len() + 1); + for r in refs { + match view_of(r.slot) { + Some(view) => scope.push(ScopeEntryAv1 { + slot_index: i32::from(r.slot), + view, + std: r.std, + }), + None => return Err(VkDecodeError::UnboundReferenceSlot { slot: r.slot }), + } + } + let reference_count = scope.len(); + // Every reference NAME must resolve to a slot the op binds. + for name in reference_name_slot_indices { + if *name == REFERENCE_NAME_UNUSED { + continue; + } + // Negative-but-not-UNUSED is not a slot at all; `u8::MAX` is a slot no + // session of this crate has (the ledger tops out at nine), so the refusal + // reads as "a name nothing binds" — which is what it is. + let Ok(slot) = u8::try_from(*name) else { + return Err(VkDecodeError::UnboundReferenceSlot { slot: u8::MAX }); + }; + if !refs.iter().any(|r| r.slot == slot) { + return Err(VkDecodeError::UnboundReferenceSlot { slot }); + } + } + for slot in held_slots { + if slot == setup_slot || refs.iter().any(|r| r.slot == slot) { + continue; + } + match ( + slot_refs.get(usize::from(slot)).copied().flatten(), + view_of(slot), + ) { + (Some(std), Some(view)) => scope.push(ScopeEntryAv1 { + slot_index: i32::from(slot), + view, + std, + }), + // Unreachable in practice: every held slot was a setup slot once. + _ => trace!( + slot, + "held slot without reference info/binding — left unbound" + ), + } + } + scope.push(ScopeEntryAv1 { + slot_index: -1, + view: setup_view, + std: setup_ref, + }); + Ok((scope, reference_count)) +} + +/// Record one AV1 decode op into the chosen command buffer and submit it under the +/// queue lock: image waits per the pool contract, the dst image's timeline signal +/// at `signal_value`. +/// +/// # Safety +/// +/// Live device; `state` is the current session generation with `vk_plan` derived +/// against its `SlotMap`, `dst` a free pool image, the tile OBUs resident in +/// `upload`'s ring slot, and the command buffer's previous submission completed +/// (caller waited its mark). +#[allow(clippy::too_many_arguments)] +unsafe fn record_and_submit_av1( + dev: &DecodeDevice, + lock: &dyn QueueLock, + state: &mut SessionStateAv1, + vk_plan: &DecodePlanVkAv1, + tiles: &SubmittedTiles, + upload: &UploadedAu, + dst: usize, + cmd_index: usize, + query_index: u32, + waits: &[(vk::Semaphore, u64)], + signal_value: u64, +) -> Result<(), VkDecodeError> { + let device = dev.ash(); + let cmd = state.ops.cmds[cmd_index]; + let coded_extent = state.coded_extent; + let coincide = state.dpb.is_none(); + + // ---- the reference layout, decided BEFORE anything is recorded ---- + let setup_view = if coincide { + state.pool.pictures[dst].view + } else { + state + .dpb + .as_ref() + .expect("distinct mode") + .dpb_view(vk_plan.setup_slot) + }; + let held_slots: Vec = state.slots.held().map(|(slot, _id)| slot).collect(); + let (scope, reference_count) = build_scope_av1( + &vk_plan.refs, + &vk_plan.reference_name_slot_indices, + held_slots.into_iter(), + vk_plan.setup_slot, + setup_view, + vk_plan.setup_ref, + &state.slot_refs, + |slot| slot_view(state, slot), + )?; + + let begin_info = + vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); + // SAFETY: the buffer's previous submission completed (fn contract) and its + // pool allows per-buffer reset, so begin implicitly resets it. + unsafe { + device + .begin_command_buffer(cmd, &begin_info) + .map_err(VkDecodeError::from)? + }; + + // ---- barriers (outside the video coding scope) ---- + let memory_barriers = [vk::MemoryBarrier2::default() + .src_stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + .src_access_mask(vk::AccessFlags2::VIDEO_DECODE_WRITE_KHR) + .dst_stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + .dst_access_mask( + vk::AccessFlags2::VIDEO_DECODE_READ_KHR | vk::AccessFlags2::VIDEO_DECODE_WRITE_KHR, + )]; + // Decode targets are fully overwritten: discard via UNDEFINED with an + // execution+memory dependency on earlier ops that touched them. + let decode_layer_barrier = |image: vk::Image, layer: u32, new_layout: vk::ImageLayout| { + vk::ImageMemoryBarrier2::default() + .src_stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + .src_access_mask( + vk::AccessFlags2::VIDEO_DECODE_READ_KHR | vk::AccessFlags2::VIDEO_DECODE_WRITE_KHR, + ) + .dst_stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + .dst_access_mask( + vk::AccessFlags2::VIDEO_DECODE_READ_KHR | vk::AccessFlags2::VIDEO_DECODE_WRITE_KHR, + ) + .old_layout(vk::ImageLayout::UNDEFINED) + .new_layout(new_layout) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(image) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: layer, + layer_count: 1, + }) + }; + let dst_image = state.pool.pictures[dst].image; + let mut image_barriers = Vec::new(); + if coincide { + // The dst pool image IS the setup DPB picture. + image_barriers.push(decode_layer_barrier( + dst_image, + 0, + vk::ImageLayout::VIDEO_DECODE_DPB_KHR, + )); + } else { + let dpb = state.dpb.as_ref().expect("distinct mode"); + let (setup_image, setup_layer) = dpb.dpb_target(vk_plan.setup_slot); + image_barriers.push(decode_layer_barrier( + setup_image, + setup_layer, + vk::ImageLayout::VIDEO_DECODE_DPB_KHR, + )); + image_barriers.push(decode_layer_barrier( + dst_image, + 0, + vk::ImageLayout::VIDEO_DECODE_DST_KHR, + )); + } + let dependency = vk::DependencyInfo::default() + .memory_barriers(&memory_barriers) + .image_memory_barriers(&image_barriers); + // SAFETY: recording into the begun buffer; synchronization2 is enabled per + // the DeviceHandles feature contract. + unsafe { device.cmd_pipeline_barrier2(cmd, &dependency) }; + + // This op's status query slot, reset before the coding scope (encoder idiom). + // None on drivers without queryResultStatusSupport (RADV — recording a query + // there hangs the VCN; OpRing docs). NEVER remove this gate. + if let Some(query_pool) = state.ops.query_pool { + // SAFETY: recording; `query_index` is within the pool's count (fn contract). + unsafe { device.cmd_reset_query_pool(cmd, query_pool, query_index, 1) }; + } + + // ---- bound-slot staging ---- + // Staged arrays over the scope decided above: resources → std infos → codec + // slot infos → slot infos. Each vector is fully built before the next borrows + // it, so nothing reallocates under a stored pointer. + let resources: Vec> = scope + .iter() + .map(|entry| { + vk::VideoPictureResourceInfoKHR::default() + .coded_extent(coded_extent) + .base_array_layer(0) + .image_view_binding(entry.view) + }) + .collect(); + let std_refs: Vec = + scope.iter().map(|entry| entry.std).collect(); + let mut dpb_infos: Vec> = std_refs + .iter() + .map(|std| vk::VideoDecodeAV1DpbSlotInfoKHR::default().std_reference_info(std)) + .collect(); + let mut begin_slots: Vec> = Vec::with_capacity(scope.len()); + for (index, entry) in scope.iter().enumerate() { + begin_slots.push( + vk::VideoReferenceSlotInfoKHR::default() + .slot_index(entry.slot_index) + .picture_resource(&resources[index]), + ); + } + for (slot_info, dpb_info) in begin_slots.iter_mut().zip(dpb_infos.iter_mut()) { + *slot_info = (*slot_info).push_next(dpb_info); + } + // The decode op's reference list: exactly this frame's references, in `refs` + // order. `referenceNameSlotIndices` does NOT index into it — it names DPB slots + // — but every slot it names has to BE in it, which build_scope_av1 checked. + let decode_refs: Vec> = + begin_slots[..reference_count].to_vec(); + + // The setup slot as the decode op sees it: its REAL index (the begin list's + // twin entry carries -1), same resource, its own codec info chain. + let setup_std = vk_plan.setup_ref; + let mut setup_dpb = vk::VideoDecodeAV1DpbSlotInfoKHR::default().std_reference_info(&setup_std); + let setup_resource = resources[scope.len() - 1]; + let setup_slot_info = vk::VideoReferenceSlotInfoKHR::default() + .slot_index(i32::from(vk_plan.setup_slot)) + .picture_resource(&setup_resource) + .push_next(&mut setup_dpb); + + // Decode destination: the setup picture itself (coincide) or the pool image + // (distinct). + let dst_resource = if coincide { + setup_resource + } else { + vk::VideoPictureResourceInfoKHR::default() + .coded_extent(coded_extent) + .base_array_layer(0) + .image_view_binding(state.pool.pictures[dst].view) + }; + + let mut av1_pic = av1_picture_info( + vk_plan.pic.std(), + vk_plan.reference_name_slot_indices, + tiles, + ); + let mut decode_info = vk::VideoDecodeInfoKHR::default() + .src_buffer(state.ring.buffer()) + .src_buffer_offset(upload.offset) + .src_buffer_range(upload.range) + .dst_picture_resource(dst_resource) + .setup_reference_slot(&setup_slot_info) + .push_next(&mut av1_pic); + if reference_count > 0 { + decode_info = decode_info.reference_slots(&decode_refs); + } + + let begin_coding = vk::VideoBeginCodingInfoKHR::default() + .video_session(state.session.session()) + .video_session_parameters(state.session.parameters()) + .reference_slots(&begin_slots); + // The one-shot session RESET, consumed HERE but re-armed on every error path + // below — a RESET recorded into a command buffer that never reaches the queue + // initialized nothing, and the next successful recording must carry it or the + // session runs its whole life uninitialized. + let did_reset = state.session.take_needs_reset(); + // SAFETY: recording into the begun buffer, through end_command_buffer; every + // pointed-to struct above is a local (or session-state field) that outlives the + // calls; the session/parameters handles are this generation's own. + let recorded: Result<(), vk::Result> = unsafe { + (dev.video_queue().fp().cmd_begin_video_coding_khr)(cmd, &begin_coding); + if did_reset { + // Session first-use initialization — ONCE, before its first decode. + let control = vk::VideoCodingControlInfoKHR::default() + .flags(vk::VideoCodingControlFlagsKHR::RESET); + (dev.video_queue().fp().cmd_control_video_coding_khr)(cmd, &control); + } + if let Some(query_pool) = state.ops.query_pool { + device.cmd_begin_query(cmd, query_pool, query_index, vk::QueryControlFlags::empty()); + } + (dev.video_decode_queue().fp().cmd_decode_video_khr)(cmd, &decode_info); + if let Some(query_pool) = state.ops.query_pool { + device.cmd_end_query(cmd, query_pool, query_index); + } + (dev.video_queue().fp().cmd_end_video_coding_khr)( + cmd, + &vk::VideoEndCodingInfoKHR::default(), + ); + device.end_command_buffer(cmd) + }; + if let Err(e) = recorded { + if did_reset { + state.session.re_arm_reset(); + } + return Err(VkDecodeError::from(e)); + } + + // ---- submit, under the caller's queue lock ---- + let cmd_infos = [vk::CommandBufferSubmitInfo::default().command_buffer(cmd)]; + let wait_infos: Vec> = waits + .iter() + .map(|&(semaphore, value)| { + vk::SemaphoreSubmitInfo::default() + .semaphore(semaphore) + .value(value) + .stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + }) + .collect(); + let signals = [vk::SemaphoreSubmitInfo::default() + .semaphore(state.pool.pictures[dst].semaphore) + .value(signal_value) + .stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS)]; + let submits = [vk::SubmitInfo2::default() + .command_buffer_infos(&cmd_infos) + .wait_semaphore_infos(&wait_infos) + .signal_semaphore_infos(&signals)]; + let guard = QueueSubmitGuard::acquire(lock); + // SAFETY: the decode queue is the device's own (DeviceHandles contract) and + // externally synchronized by the guard; the submit arrays are locals. + let result = unsafe { device.queue_submit2(dev.decode_queue(), &submits, vk::Fence::null()) }; + drop(guard); + if let Err(e) = result { + // The recorded RESET never executed: the next recording must redo it. + if did_reset { + state.session.re_arm_reset(); + } + return Err(VkDecodeError::from(e)); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use ash::vk::Handle as _; + use cros_codecs::bitstream_utils::IvfIterator; + use cros_codecs::codec::av1::parser::ObuAction; + use cros_codecs::codec::av1::parser::ParsedObu; + use cros_codecs::codec::av1::parser::Parser; + + use super::*; + + const AV1_25FPS: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1" + ); + + /// A reference-info value carrying just the fields the assertions read. + fn std_ref(order_hint: u8, frame_type: u8) -> hh::StdVideoDecodeAV1ReferenceInfo { + // SAFETY: StdVideoDecodeAV1ReferenceInfo is a plain-C bindgen struct of a + // bitfield word, three small integers and a byte array; all-zero is valid + // for every field. + let mut std: hh::StdVideoDecodeAV1ReferenceInfo = unsafe { std::mem::zeroed() }; + std.OrderHint = order_hint; + std.frame_type = frame_type; + std + } + + fn vk_ref(slot: u8, order_hint: u8) -> VkRefAv1 { + VkRefAv1 { + slot, + std: std_ref(order_hint, 1), + id: u64::from(slot) + 100, + } + } + + /// A distinguishable fake view per slot (never dereferenced — the scope only + /// carries handles around). + fn fake_view(slot: u8) -> vk::ImageView { + vk::ImageView::from_raw(u64::from(slot) + 1) + } + + /// Names 0..7 pointing at `slots`, `-1` for the rest. + fn names(slots: &[u8]) -> [i32; 7] { + let mut out = [REFERENCE_NAME_UNUSED; 7]; + for (name, slot) in slots.iter().enumerate() { + out[name] = i32::from(*slot); + } + out + } + + #[test] + fn the_scopes_leading_entries_are_the_refs_in_plan_order() { + // `refs` is the plan's DEDUPED reference list in first-appearance order, + // which is neither slot order nor name order — the scope must not sort or + // re-order it, because `pReferenceSlots` is exactly this prefix. + let refs = vec![vk_ref(5, 40), vk_ref(1, 60), vk_ref(3, 8)]; + let slot_refs = vec![Some(std_ref(0, 1)); 9]; + let (scope, reference_count) = build_scope_av1( + &refs, + &names(&[5, 1, 3, 5, 1, 3, 5]), + [1u8, 3, 5, 7].into_iter(), + 2, + fake_view(2), + std_ref(50, 0), + &slot_refs, + |slot| Some(fake_view(slot)), + ) + .unwrap(); + + assert_eq!(reference_count, 3, "exactly this frame's references lead"); + assert_eq!( + scope[..reference_count] + .iter() + .map(|e| e.slot_index) + .collect::>(), + vec![5, 1, 3], + "plan order, not slot order" + ); + for (entry, r) in scope.iter().zip(&refs) { + assert_eq!(entry.view, fake_view(r.slot)); + assert_eq!(entry.std.OrderHint, r.std.OrderHint); + } + + // Then the other still-held slot (7), then the setup ACTIVATION entry. + assert_eq!(scope[3].slot_index, 7); + let last = scope.last().unwrap(); + assert_eq!( + last.slot_index, -1, + "the setup slot binds its resource without a current association" + ); + assert_eq!(last.view, fake_view(2)); + assert_eq!(last.std.OrderHint, 50); + assert_eq!( + scope.len(), + 5, + "3 refs + 1 other held slot + the activation" + ); + } + + #[test] + fn a_reference_slot_without_a_bound_image_fails_the_whole_op() { + let refs = vec![vk_ref(4, 10), vk_ref(6, 20)]; + let slot_refs = vec![Some(std_ref(0, 1)); 9]; + let err = build_scope_av1( + &refs, + &names(&[4, 6]), + [4u8, 6].into_iter(), + 0, + fake_view(0), + std_ref(30, 1), + &slot_refs, + |slot| (slot != 6).then(|| fake_view(slot)), + ) + .unwrap_err(); + assert!( + matches!(err, VkDecodeError::UnboundReferenceSlot { slot: 6 }), + "{err}" + ); + } + + #[test] + fn a_reference_name_pointing_outside_the_bound_slots_fails_the_whole_op() { + // The HEVC class, stated for AV1: a name resolving to a slot the decode op + // does not bind is unresolvable for the hardware — it can only answer by + // guessing. Refuse at submission time rather than discover it on a driver. + let refs = vec![vk_ref(4, 10)]; + let slot_refs = vec![Some(std_ref(0, 1)); 9]; + let err = build_scope_av1( + &refs, + // Name 1 points at slot 7, which `refs` does not contain. + &names(&[4, 7]), + [4u8, 7].into_iter(), + 0, + fake_view(0), + std_ref(30, 1), + &slot_refs, + |slot| Some(fake_view(slot)), + ) + .unwrap_err(); + assert!( + matches!(err, VkDecodeError::UnboundReferenceSlot { slot: 7 }), + "{err}" + ); + + // And the same list with slot 7 actually bound is fine — so the assertion + // above measures the name check, not an unrelated refusal. + let refs = vec![vk_ref(4, 10), vk_ref(7, 11)]; + let (_scope, reference_count) = build_scope_av1( + &refs, + &names(&[4, 7]), + [4u8, 7].into_iter(), + 0, + fake_view(0), + std_ref(30, 1), + &slot_refs, + |slot| Some(fake_view(slot)), + ) + .unwrap(); + assert_eq!(reference_count, 2); + } + + #[test] + fn held_slots_are_bound_once_and_the_setup_slot_never_twice() { + // Slot 3 is BOTH a reference and still held; slot 2 is the setup slot and + // also held (the previous picture in it). Neither may appear twice: a + // duplicate slot index in one coding scope is invalid. + let refs = vec![vk_ref(3, 12)]; + let slot_refs = vec![Some(std_ref(99, 1)); 9]; + let (scope, reference_count) = build_scope_av1( + &refs, + &names(&[3, 3, 3, 3, 3, 3, 3]), + [1u8, 2, 3].into_iter(), + 2, + fake_view(2), + std_ref(24, 1), + &slot_refs, + |slot| Some(fake_view(slot)), + ) + .unwrap(); + assert_eq!(reference_count, 1); + let indices: Vec = scope.iter().map(|e| e.slot_index).collect(); + assert_eq!(indices, vec![3, 1, -1]); + assert_eq!( + indices.iter().filter(|&&i| i == 3).count(), + 1, + "a referenced slot is bound exactly once even when seven names use it" + ); + assert!( + !indices.contains(&2), + "the setup slot is bound only as the -1 activation entry" + ); + } + + #[test] + fn a_key_frames_scope_is_the_activation_entry_alone() { + let slot_refs: Vec> = vec![None; 9]; + let (scope, reference_count) = build_scope_av1( + &[], + &names(&[]), + std::iter::empty(), + 0, + fake_view(0), + std_ref(0, 0), + &slot_refs, + |slot| Some(fake_view(slot)), + ) + .unwrap(); + assert_eq!(reference_count, 0, "a key frame references nothing"); + assert_eq!( + scope.iter().map(|e| e.slot_index).collect::>(), + vec![-1] + ); + } + + #[test] + fn resetting_the_slot_bindings_frees_every_ledger_and_hands_back_the_pinned_images() { + let mut slots = SlotMap::new(NUM_REF_SLOTS); + slots.assign(100).unwrap(); + slots.assign(200).unwrap(); + let mut slot_image: Vec> = vec![Some(7), Some(8), None, None]; + let mut slot_refs: Vec> = + vec![Some(std_ref(10, 1)); 4]; + + let unbound = reset_slot_bindings(&mut slots, &mut slot_image, &mut slot_refs); + assert_eq!( + unbound, + vec![7, 8], + "the pool images the stale bindings pinned go back on the free list" + ); + assert_eq!(slots.active(), 0); + assert_eq!( + slots.capacity(), + REQUIRED_SLOTS as usize, + "capacity survives — no session rebuild" + ); + assert!(slot_image.iter().all(Option::is_none)); + assert!( + slot_refs.iter().all(Option::is_none), + "cached reference info goes too, or build_scope_av1 could bind a slot \ + the planner no longer knows about" + ); + } + + /// The submission-final picture info, built by the PRODUCTION function. + /// + /// Two things are load-bearing here and neither is visible without a driver: + /// + /// * `pTileOffsets` / `pTileSizes` must address 256 readable entries, because + /// RADV reads that many whatever `tileCount` says. So the arrays are checked + /// past the tile count, and the tail must be zero rather than whatever was in + /// the previous frame's allocation; + /// * `tileCount` must nevertheless be the REAL count. ash's `tile_offsets()` + /// and `tile_sizes()` both write it from their slice length, so the + /// production function assigns it afterwards — and this test would catch a + /// refactor that dropped that line, because it would read 256. + #[test] + fn the_picture_info_carries_padded_tile_arrays_with_the_real_tile_count() { + let packed = pack_av1_tiles(&[100..1000, 1000..1600, 1600..2100]).expect("fits u32"); + let tiles = submitted_tiles(&packed).expect("three tiles fit"); + assert_eq!(tiles.count, 3); + assert_eq!(&tiles.offsets[..3], &[0, 900, 1500]); + assert_eq!(&tiles.sizes[..3], &[900, 600, 500]); + + // SAFETY: StdVideoDecodeAV1PictureInfo is a plain-C bindgen struct of a + // bitfield word, integers, byte arrays and const pointers; all-zero is + // valid and no pointer is dereferenced here. + let mut std_pic: hh::StdVideoDecodeAV1PictureInfo = unsafe { std::mem::zeroed() }; + std_pic.OrderHint = 42; + let picture_info = av1_picture_info(&std_pic, names(&[5, 1, 3]), &tiles); + + assert_eq!( + picture_info.tile_count, 3, + "tileCount is the real count, not the array length ash's setters would \ + have written" + ); + assert_eq!( + picture_info.frame_header_offset, FRAME_HEADER_OFFSET, + "the buffer holds tile payloads only, so there is no header to point at" + ); + assert_eq!( + picture_info.s_type, + vk::StructureType::VIDEO_DECODE_AV1_PICTURE_INFO_KHR + ); + assert_eq!(picture_info.reference_name_slot_indices[0], 5); + assert_eq!(picture_info.reference_name_slot_indices[6], -1); + // SAFETY: the three pointers were taken from `tiles`/`std_pic`, both alive + // for this scope; the arrays behind the first two are AV1_MAX_NUM_TILES + // long by construction, which is exactly the length read here. + unsafe { + let offsets = + std::slice::from_raw_parts(picture_info.p_tile_offsets, AV1_MAX_NUM_TILES); + let sizes = std::slice::from_raw_parts(picture_info.p_tile_sizes, AV1_MAX_NUM_TILES); + assert_eq!(&offsets[..3], &[0, 900, 1500]); + assert_eq!(&sizes[..3], &[900, 600, 500]); + assert!( + offsets[3..].iter().all(|o| *o == 0) && sizes[3..].iter().all(|s| *s == 0), + "the tail a driver reads past tileCount must be zeroed, not \ + whatever the allocator handed back" + ); + assert_eq!((*picture_info.p_std_picture_info).OrderHint, 42); + } + + // And a DPB slot info chains the AV1 reference info, not another codec's. + let std = std_ref(17, 1); + let dpb_info = vk::VideoDecodeAV1DpbSlotInfoKHR::default().std_reference_info(&std); + assert_eq!( + dpb_info.s_type, + vk::StructureType::VIDEO_DECODE_AV1_DPB_SLOT_INFO_KHR + ); + // SAFETY: the pointer was just taken from `std`, alive for this scope. + unsafe { + assert_eq!((*dpb_info.p_std_reference_info).OrderHint, 17); + } + } + + #[test] + fn packed_tiles_land_end_to_end_and_more_than_the_arrays_hold_is_refused() { + let packed = pack_av1_tiles(&[100..200, 500..560]).unwrap(); + assert_eq!(packed.offsets, vec![0, 100]); + let tiles = submitted_tiles(&packed).expect("two tiles fit"); + assert_eq!(tiles.count, 2); + assert_eq!(&tiles.offsets[..2], &[0, 100]); + assert_eq!(&tiles.sizes[..2], &[100, 60]); + + // Exactly full is fine; one more is refused rather than truncated — a + // silently dropped tile decodes as garbage in that part of the frame. + let ranges: Vec> = (0..AV1_MAX_NUM_TILES).map(|i| i * 4..i * 4 + 4).collect(); + let full = submitted_tiles(&pack_av1_tiles(&ranges).unwrap()).expect("256 tiles fit"); + assert_eq!(full.count, AV1_MAX_NUM_TILES as u32); + let ranges: Vec> = (0..AV1_MAX_NUM_TILES + 1) + .map(|i| i * 4..i * 4 + 4) + .collect(); + assert_eq!( + submitted_tiles(&pack_av1_tiles(&ranges).unwrap()), + Err(Av1TileError::TooManyTiles { + tiles: AV1_MAX_NUM_TILES + 1 + }) + ); + } + + /// Every tile of the vendored vector, split and cross-checked against the + /// vendored PARSER's own per-tile figures. + /// + /// This is the anti-vacuity assertion for [`plan_bitstream`]: the walk it does + /// (OBU header, frame header length, tile-group header, `tile_size_minus_1` + /// fields) is re-derived here from the parser's `Tile::tile_offset` / + /// `Tile::tile_size` — which are computed by an INDEPENDENT code path inside + /// cros-codecs — and the two must agree byte for byte on all 274 frames. A + /// split that merely "looked plausible" (whole OBUs, say, or an off-by-the-OBU- + /// header start) fails here rather than on a driver. + #[test] + fn every_tile_of_the_vector_splits_to_the_parsers_own_offsets_and_sizes() { + let mut planner = Av1Planner::new(); + // A SECOND parser instance, walking the same bytes to recover the tile + // ranges the plan does not carry. Its `Cow::Borrowed` payload slices point + // into the packet, so their absolute offsets come out of the pointer + // difference — no unsafe, and no re-implementation of the walk. + let mut reference = Parser::default(); + let (mut frames, mut tiles_checked) = (0u32, 0u32); + let mut frame_obus = 0u32; + + for packet in IvfIterator::new(AV1_25FPS) { + // What the parser says the tiles are, in decode order. + let mut expected: Vec> = Vec::new(); + let mut consumed = 0usize; + while consumed < packet.len() { + let action = reference + .read_obu(&packet[consumed..]) + .expect("the clean vector parses"); + let obu = match action { + ObuAction::Process(obu) => obu, + ObuAction::Drop(n) => { + consumed += n as usize; + continue; + } + }; + consumed += obu.bytes_used; + match reference.parse_obu(obu).expect("the clean vector parses") { + ParsedObu::Frame(frame) => { + frame_obus += 1; + let payload = frame.tile_group.obu.as_ref(); + let base = payload.as_ptr() as usize - packet.as_ptr() as usize; + for tile in &frame.tile_group.tiles { + let start = base + tile.tile_offset as usize; + expected.push(start..start + tile.tile_size as usize); + } + // The parser keeps its own reference state and needs it + // advanced, exactly as `Av1Planner` does, or every later + // inter frame fails to parse. + if !frame.header.show_existing_frame { + reference + .ref_frame_update(&frame.header) + .expect("the clean vector updates"); + } + } + ParsedObu::TileGroup(tg) => { + let payload = tg.obu.as_ref(); + let base = payload.as_ptr() as usize - packet.as_ptr() as usize; + for tile in &tg.tiles { + let start = base + tile.tile_offset as usize; + expected.push(start..start + tile.tile_size as usize); + } + } + ParsedObu::FrameHeader(fh) if !fh.show_existing_frame => { + reference + .ref_frame_update(&fh) + .expect("the clean vector updates"); + } + _ => {} + } + } + + let mut produced: Vec> = Vec::new(); + for plan in planner.plan_au(packet).expect("the clean vector plans") { + if plan.dpb.stored.is_none() { + continue; + } + frames += 1; + let bitstream = plan_bitstream(packet, &plan.tiles, &plan.header) + .expect("every tile group splits"); + produced.extend(bitstream.tiles); + } + tiles_checked += produced.len() as u32; + assert_eq!( + produced, expected, + "the split disagrees with the parser's own tile offsets/sizes" + ); + } + + assert_eq!(frames, 274, "every frame of the vector must split"); + assert_eq!( + tiles_checked, 274, + "this vector is one tile per frame; the count pins that the comparison \ + above actually compared something" + ); + assert!( + frame_obus > 0, + "the vector must exercise the OBU_FRAME path — where the frame header \ + sits INSIDE the tile OBU and the split has to step over it" + ); + } + + /// Over the vector: the ring slot must contain the TILE PAYLOADS AND NOTHING + /// ELSE, and every submitted offset must land exactly on its tile's first byte + /// inside it. + /// + /// The "nothing else" half is the layout assertion. Uploading whole OBUs also + /// produced correct per-tile offsets — it is what this rung did until the M7 + /// review — so an offsets-only check passes against either layout. What + /// distinguishes them is the slot LENGTH: libavcodec's layout uploads the sum + /// of the tile sizes, and the OBU layout uploads the OBU headers, the frame + /// headers and the `tile_size_minus_1` fields with them. + #[test] + fn the_ring_slot_holds_the_tile_payloads_and_nothing_else() { + let mut planner = Av1Planner::new(); + let (mut checked, mut bytes_saved) = (0u32, 0usize); + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("plans") { + if plan.dpb.stored.is_none() { + continue; + } + let bitstream = plan_bitstream(packet, &plan.tiles, &plan.header).expect("splits"); + let packed = pack_av1_tiles(&bitstream.tiles).expect("fits u32"); + let tiles = submitted_tiles(&packed).expect("within the tile limit"); + + // Build the slot bytes exactly as the ring would. + let mut slot: Vec = Vec::new(); + for segment in &packed.segments { + slot.extend_from_slice(&packet[segment.clone()]); + } + let obu_bytes: usize = plan.tiles.iter().map(|t| t.data.len()).sum(); + assert_eq!( + slot.len(), + bitstream.tiles.iter().map(Range::len).sum::(), + "the slot is the tile payloads exactly" + ); + assert!( + slot.len() < obu_bytes, + "the tile payloads must be SHORTER than the OBUs that carried \ + them, or this frame proves nothing about the layout" + ); + bytes_saved += obu_bytes - slot.len(); + + assert_eq!(tiles.count as usize, bitstream.tiles.len()); + for (i, range) in bitstream.tiles.iter().enumerate() { + let start = tiles.offsets[i] as usize; + let end = start + tiles.sizes[i] as usize; + assert!(end <= slot.len(), "a tile range reaches past the slot"); + assert_eq!( + &slot[start..end], + &packet[range.clone()], + "the submitted offset does not address this tile's bytes" + ); + checked += 1; + } + } + } + assert_eq!(checked, 274, "every tile of the vector was addressed"); + eprintln!("bytes not uploaded across the vector: {bytes_saved}"); + } + + /// A hand-built TWO-tile tile group: the only way the coded-size arithmetic + /// gets exercised at all. + /// + /// The vendored vector is one tile per frame, so every `tile_size_minus_1` + /// read, the `tile_start_and_end_present_flag` bit and the tile-group header's + /// byte alignment are dead code as far as + /// `every_tile_of_the_vector_splits_to_the_parsers_own_offsets_and_sizes` is + /// concerned. This builds the bytes by hand from the spec's `tile_group_obu()` + /// layout and checks the ranges land on the payloads. + fn two_tile_group( + flag_present: bool, + ) -> (Vec, FrameHeaderObu, Vec) { + let mut header = FrameHeaderObu::default(); + header.tile_info.tile_cols = 2; + header.tile_info.tile_rows = 1; + header.tile_info.tile_cols_log2 = 1; + header.tile_info.tile_rows_log2 = 0; + header.tile_info.tile_size_bytes = 2; + + // tile_group_obu(): NumTiles = 2 > 1, so tile_start_and_end_present_flag + // is coded. Clear ⇒ the group is the whole frame (tg 0..1) and the header + // is one bit padded to one byte; set ⇒ tg_start/tg_end follow at + // (tile_cols_log2 + tile_rows_log2) = 1 bit each, so 3 bits, still one byte. + let tg_header: u8 = if flag_present { + // flag=1, tg_start=0, tg_end=1 ⇒ bits 1 0 1 from the MSB. + 0b1010_0000 + } else { + 0b0000_0000 + }; + let tile0 = [0xA1u8, 0xA2, 0xA3]; + let tile1 = [0xB1u8, 0xB2]; + let mut payload = vec![tg_header]; + // le(TileSizeBytes = 2) of tile_size_minus_1 for every tile but the last. + payload.extend_from_slice(&[(tile0.len() as u8) - 1, 0]); + payload.extend_from_slice(&tile0); + payload.extend_from_slice(&tile1); + + // obu_header(): type = OBU_TILE_GROUP (4), no extension, has_size_field. + let mut au = vec![0x22u8, payload.len() as u8]; + let payload_start = au.len(); + au.extend_from_slice(&payload); + let tiles = vec![pf_bitstream::av1::TilePlan { + data: 0..au.len(), + tg_start: 0, + tg_end: 1, + }]; + assert_eq!(payload_start, 2); + (au, header, tiles) + } + + #[test] + fn a_multi_tile_group_splits_at_the_coded_tile_sizes() { + for flag_present in [false, true] { + let (au, header, tiles) = two_tile_group(flag_present); + let bitstream = plan_bitstream(&au, &tiles, &header).expect("splits"); + let ranges = bitstream.tiles; + // 2 OBU header bytes + 1 tile-group header byte + 2 size bytes = 5. + assert_eq!(ranges, vec![5..8, 8..10], "flag_present={flag_present}"); + assert_eq!(&au[ranges[0].clone()], &[0xA1, 0xA2, 0xA3]); + assert_eq!(&au[ranges[1].clone()], &[0xB1, 0xB2]); + } + + // A coded size that OVERSHOOTS the payload is refused rather than + // producing a range past the OBU. (A size that UNDERSHOOTS cannot be + // caught — the last tile absorbs it; plan_bitstream's docs say so.) + let (mut au, header, tiles) = two_tile_group(false); + au[3] = 0x40; // tile_size_minus_1 = 64 ⇒ 65 bytes in an 8-byte payload + assert_eq!( + plan_bitstream(&au, &tiles, &header), + Err(Av1TileError::Truncated { obu: 0 }) + ); + + // `TileSizeBytes` is only CODED for a multi-tile frame, so it is only + // checked there — a width of 0 (what the parser leaves on a single-tile + // frame) would shift by 0..0 and read nothing. + let (au, mut header, tiles) = two_tile_group(false); + header.tile_info.tile_size_bytes = 0; + assert_eq!( + plan_bitstream(&au, &tiles, &header), + Err(Av1TileError::Overflow) + ); + header.tile_info.tile_size_bytes = 9; + assert_eq!( + plan_bitstream(&au, &tiles, &header), + Err(Av1TileError::Overflow), + "a width past 4 would overflow the shift" + ); + + // A tile group claiming more tiles than the frame has is malformed. + let (au, header, mut tiles) = two_tile_group(false); + tiles[0].tg_end = 7; + assert_eq!( + plan_bitstream(&au, &tiles, &header), + Err(Av1TileError::Truncated { obu: 0 }) + ); + } + + #[test] + fn an_obu_whose_declared_size_disagrees_with_the_plans_range_is_refused() { + let mut planner = Av1Planner::new(); + let packet = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let plan = planner + .plan_au(packet) + .expect("plans") + .into_iter() + .next() + .expect("a frame"); + // The clean article splits. + assert!(plan_bitstream(packet, &plan.tiles, &plan.header).is_ok()); + + // Now lie about the OBU's extent by one byte. `obu_size` inside the + // bitstream still says the old end, and the disagreement is caught — the + // one cross-check a walk with an implicit last tile size can have. + let mut damaged = plan.tiles.clone(); + damaged[0].data.end -= 1; + assert!( + matches!( + plan_bitstream(packet, &damaged, &plan.header), + Err(Av1TileError::SizeMismatch { .. }) + ), + "a range disagreeing with obu_size must be refused" + ); + + // An OBU type that carries no tiles at all is named rather than walked. + let start = plan.tiles[0].data.start; + let mut au = packet.to_vec(); + // OBU_METADATA (5) in the type field. + au[start] = (au[start] & !0x78) | (5 << 3); + assert_eq!( + plan_bitstream(&au, &plan.tiles, &plan.header), + Err(Av1TileError::UnexpectedObu { + obu: 0, + obu_type: 5 + }) + ); + + // And a frame header claiming no tiles refuses before any byte is read. + let mut no_tiles = (*plan.header).clone(); + no_tiles.tile_info.tile_cols = 0; + assert_eq!( + plan_bitstream(packet, &plan.tiles, &no_tiles), + Err(Av1TileError::NoTiles) + ); + } + + #[test] + fn a_leb128_without_a_terminator_is_refused_rather_than_read_forever() { + // Nine continuation bytes: the AV1 spec caps leb128() at eight. + let au = [0x80u8; 16]; + assert_eq!(leb128(&au, 0), None); + // A well-formed multi-byte value reads back exactly. + let au = [0x81u8, 0x02]; + assert_eq!(leb128(&au, 0), Some((0x101, 2))); + // And a value running off the end is a miss, not a panic. + assert_eq!(leb128(&[0x80], 0), None); + assert_eq!(leb128(&[], 0), None); + } + + /// The extents and the level the session is shaped by, read off real plans. + #[test] + fn the_session_shape_comes_off_the_stream_not_a_constant() { + let mut planner = Av1Planner::new(); + let packet = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let plan = planner + .plan_au(packet) + .expect("plans") + .into_iter() + .next() + .expect("a frame"); + + let extent = coded_extent(&plan); + assert_eq!( + (extent.width, extent.height), + (plan.picture.upscaled_width, plan.picture.frame_height), + "the decode output is the POST-superres width" + ); + assert!(extent.width > 0 && extent.height > 0); + + // The vector is Main 4:2:0 8-bit without film grain. + let key = profile_key_for(&plan).expect("inside the envelope"); + assert_eq!(key.output_format(), Some(crate::caps::NV12)); + assert!(!key.film_grain); + + // The level gate reads operating point 0 and stays inside the Std range. + assert!(stream_level_idx(&plan) <= 23); + } + + #[test] + fn only_a_decoded_key_frame_ends_the_wait_for_one() { + let mut planner = Av1Planner::new(); + let packet = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let first = planner + .plan_au(packet) + .expect("plans") + .into_iter() + .next() + .expect("a frame"); + assert!(first.picture.is_key, "the vector opens on a key frame"); + assert!( + clears_awaiting_key(&first), + "a decoded key frame is what resumes decoding" + ); + + // The same key frame as a `show_existing_frame` plan decodes nothing, so + // it must NOT resume: the planner's store would be full and this decoder's + // ledger empty, and the next inter frame would fail immediately. + let mut shown = first.clone(); + shown.dpb.stored = None; + assert!(shown.picture.is_key); + assert!(!clears_awaiting_key(&shown)); + + // An ordinary inter frame never resumes either. + let inter = planner + .plan_au(IvfIterator::new(AV1_25FPS).nth(1).expect("a second packet")) + .expect("plans") + .into_iter() + .next() + .expect("a frame"); + assert!(!inter.picture.is_key); + assert!(!clears_awaiting_key(&inter)); + } + + /// A recovery WAIT must reach the consumer as an ERROR, once per access unit — + /// the same answer H.264/H.265 give through their planners' + /// `PlanError::AwaitingIdr`, and the reason [`VkAv1Decoder::awaiting_key`]'s + /// docs carry: a clean `Ok(None)` resets the consumer's demotion streak once + /// per frame, so a rung whose every key frame fails (film grain on a device + /// without the grain profile; a level above `maxLevelIdc`; a sequence header + /// disagreeing with the negotiation) would never demote and the session would + /// hold a frozen screen with a clean bill of health. + /// + /// What this pins is the AGGREGATION, which is where the naive fix goes wrong: + /// the error is per ACCESS UNIT while the skip is per FRAME, because a key + /// frame can sit behind a skipped frame in the same temporal unit — the + /// vendored vector has 24 units carrying two frames each. + #[test] + fn a_unit_reports_the_key_frame_wait_only_when_it_decoded_nothing_at_all() { + // The wait itself: every frame of the unit skipped. + assert!(whole_unit_skipped(1, 1), "a single-frame unit"); + assert!(whole_unit_skipped(2, 2), "and a two-frame one"); + + // A key frame arrived partway through the unit and decoded: NOT the wait, + // whatever came before it. An early return at the first skip would have + // answered an error here and never reached the key frame at all. + assert!(!whole_unit_skipped(2, 1)); + assert!(!whole_unit_skipped(3, 2)); + // Nothing was skipped: the ordinary decoding case. + assert!(!whole_unit_skipped(2, 0)); + // A unit that planned no frames (metadata / a sequence header on its own) + // is a clean `Ok(None)`, never an error. + assert!(!whole_unit_skipped(0, 0)); + } + + /// The wait's error must be DISTINGUISHABLE from the failure that started it — + /// a support engineer reading a field log has to be able to tell "the AU could + /// not be decoded" from "the decoder is waiting to re-anchor", and the two ride + /// the same `Err` channel. + #[test] + fn the_key_frame_wait_names_itself_in_the_error_text() { + let waiting = format!("{}", VkDecodeError::AwaitingKeyAv1); + assert!(waiting.contains("key frame"), "{waiting}"); + assert!(waiting.contains("skipped"), "{waiting}"); + // …and it is not the same message as the loss that latched the recovery. + let lost = format!( + "{}", + VkDecodeError::MissingReferenceAv1 { + slot: 3, + ref_index: 2 + } + ); + assert_ne!(waiting, lost); + } + + /// The `refresh_frame_flags == 0` leg is real AV1 and this vector has none of + /// it — which is worth PROVING rather than assuming, because it is exactly the + /// sort of "cannot happen" that quietly exhausts a nine-slot ledger in the + /// field. The measurement is the point: it says plainly which arm the vendored + /// vector exercises and which one only the code review covers. + #[test] + fn every_frame_of_the_vector_refreshes_a_slot_so_the_orphan_arm_is_review_only() { + let mut planner = Av1Planner::new(); + let (mut frames, mut orphans) = (0u32, 0u32); + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("plans") { + if plan.dpb.stored.is_none() { + continue; + } + frames += 1; + if plan.header.refresh_frame_flags == 0 { + orphans += 1; + } + } + } + assert_eq!(frames, 274); + assert_eq!( + orphans, 0, + "if this ever fires the orphan release IS exercised — turn this into a \ + ledger-occupancy assertion rather than deleting it" + ); + } + + /// The refusal predicate itself — [`lost_reference`], the PRODUCTION function + /// `decode_planned` calls. + /// + /// It used to be re-implemented inline here, which meant deleting the real + /// refusal left this green: the test asserted that a `find_map` over a + /// hand-built array found what the array contained. The guard it is supposed + /// to cover is the one that keeps a frame from being decoded against a + /// reference the DPB does not hold. + #[test] + fn a_lost_reference_is_the_condition_the_decoder_refuses_on() { + assert_eq!( + lost_reference(&[ + PlanWarning::TruncatedAu { offset: 12 }, + PlanWarning::MissingReference { + slot: 3, + ref_index: 2, + }, + ]), + Some((3, 2)), + "a missing reference must be found even behind another warning" + ); + + // A truncated tail alone is NOT this condition — it is concealment + // material the planner already accounted for, and refusing on it would + // turn every clipped AU into a keyframe request. + assert_eq!( + lost_reference(&[PlanWarning::TruncatedAu { offset: 12 }]), + None + ); + assert_eq!( + lost_reference(&[PlanWarning::MissingShowExisting { slot: 4 }]), + None, + "a show_existing_frame naming an empty slot decodes nothing, so there \ + is no reference set to be wrong about" + ); + assert_eq!(lost_reference(&[]), None); + } + + /// And the whole vector goes through that predicate without tripping it — the + /// anti-vacuity half: if the clean vector DID report a lost reference, every + /// frame of it would be refused and the tests above would be measuring a + /// decoder that decodes nothing. + #[test] + fn no_frame_of_the_clean_vector_trips_the_refusal() { + let mut planner = Av1Planner::new(); + let mut frames = 0u32; + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("plans") { + frames += 1; + assert_eq!( + lost_reference(&plan.warnings), + None, + "frame {frames} of a clean conformance vector must not be refused" + ); + } + } + assert_eq!(frames, 274); + } + + /// The whole vendored vector through the DPB bookkeeping `decode_planned` + /// runs — conversion, [`sync_slot_bindings`], [`build_scope_av1`] — with no + /// GPU anywhere. + /// + /// This is the test that was missing, and the defect it closes reached an + /// RTX 5070 Ti before anything on this machine noticed: 172 unit tests, clippy + /// clean, and `AU 4: DPB slot 2 is referenced by this AU but binds no image` + /// on the first hardware contact. Everything needed to see it was on the CPU. + /// What was not on the CPU was a test that ran the three pieces TOGETHER: the + /// conversion was tested against a `SlotMap`, the scope builder against + /// hand-made reference lists, and the binding sync against nothing at all (it + /// was four lines inline in `decode_planned`). Each was right about its own + /// half and the seam between them was where the picture went missing. + /// + /// So this walks the real vector through the real functions and asserts what + /// the hardware asserts: + /// + /// * every slot this frame references still binds an image when the scope is + /// built (the refusal that fired on the driver); + /// * the image it binds is the one that reference was DECODED into — the + /// assertion that matters more, because a slot recycled into the setup + /// picture is *bound*, just to the wrong picture, and the hardware would + /// have predicted from the frame it was in the middle of writing without + /// ever reporting an error; + /// * no held slot is left without a binding, which `build_scope_av1` only + /// traces as "unreachable in practice". + #[test] + fn slot_recycling_waits_for_the_decode_op() { + #[derive(Clone, Default)] + struct SimPicture { + bound: bool, + pending: bool, + held: u32, + } + // A distinguishable view per POOL IMAGE (never dereferenced), so a scope + // entry can be traced back to the picture that image holds. + let image_view = |picture: usize| vk::ImageView::from_raw(picture as u64 + 1); + + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let mut slot_image: Vec> = vec![None; REQUIRED_SLOTS as usize]; + let mut slot_refs: Vec> = + vec![None; REQUIRED_SLOTS as usize]; + let mut pictures = + vec![SimPicture::default(); (REQUIRED_SLOTS + crate::images::HOLD_HEADROOM) as usize]; + // Decoded pictures awaiting an output verdict, and the pool image each + // picture was decoded into (for as long as anything can reference it). + let mut pending: BTreeMap = BTreeMap::new(); + let mut image_of: BTreeMap = BTreeMap::new(); + + let (mut frames, mut deferring, mut scope_refs) = (0u32, 0u32, 0u32); + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("the clean vector plans") { + let Some(setup_id) = plan.dpb.stored else { + // show_existing_frame decodes nothing; `decode_planned` + // settles it and releases its removals directly. + let (ready, dropped) = + settle_dpb_ids(&mut pending, &plan.dpb.outputs, &plan.dpb.removed); + for image in ready.into_iter().chain(dropped) { + pictures[image].pending = false; + } + for &id in &plan.dpb.removed { + slots.release(id); + image_of.remove(&id); + } + continue; + }; + frames += 1; + + let vk = plan_to_vk_av1(&plan, &mut slots).expect("the clean vector converts"); + let setup = usize::from(vk.setup_slot); + if !vk.release_after_decode.is_empty() { + deferring += 1; + } + + for picture in sync_slot_bindings(&slots, &mut slot_image, vk.setup_slot) { + pictures[picture].bound = false; + } + let dst = pictures + .iter() + .position(|p| !p.bound && !p.pending && p.held == 0) + .unwrap_or_else(|| panic!("frame {frames}: picture pool exhausted")); + + // Every held slot but the setup one must bind an image, or + // `build_scope_av1` silently drops it from the coding scope. + for (slot, _id) in slots.held() { + if usize::from(slot) == setup { + continue; + } + assert!( + slot_image[usize::from(slot)].is_some(), + "frame {frames}: held slot {slot} binds no image" + ); + } + + let held_slots: Vec = slots.held().map(|(slot, _id)| slot).collect(); + let (scope, reference_count) = build_scope_av1( + &vk.refs, + &vk.reference_name_slot_indices, + held_slots.iter().copied(), + vk.setup_slot, + image_view(dst), + vk.setup_ref, + &slot_refs, + |slot| slot_image[usize::from(slot)].map(image_view), + ) + .unwrap_or_else(|e| { + panic!( + "frame {frames}: {e}\n setup_slot={setup} setup_id={setup_id}\n \ + refs={:?}\n names={:?}\n bindings={slot_image:?}", + vk.refs.iter().map(|r| (r.slot, r.id)).collect::>(), + vk.reference_name_slot_indices, + ) + }); + + // The scope binds every held slot but the setup one exactly once, + // plus the setup slot as the `-1` activation entry — nothing + // dropped, nothing duplicated. (The setup slot is always held by + // now: `plan_to_vk_av1` assigned it.) + assert_eq!(reference_count, vk.refs.len()); + let mut bound: Vec = scope.iter().map(|e| e.slot_index).collect(); + assert_eq!(bound.pop(), Some(-1), "frame {frames}: no activation entry"); + bound.sort_unstable(); + let mut expected: Vec = held_slots + .iter() + .filter(|slot| usize::from(**slot) != setup) + .map(|slot| i32::from(*slot)) + .collect(); + expected.sort_unstable(); + assert_eq!( + bound, expected, + "frame {frames}: the coding scope must bind exactly the held \ + slots, once each" + ); + + // THE assertion: each reference's scope entry must carry the image + // that reference was decoded into. A slot recycled into the setup + // picture binds an image too — the wrong one — and nothing but this + // would say so. + for (entry, r) in scope[..reference_count].iter().zip(&vk.refs) { + let decoded_into = image_of[&r.id]; + assert_eq!( + entry.view, + image_view(decoded_into), + "frame {frames}: reference picture {} (slot {}) binds pool \ + image {:?}, but it was decoded into image {decoded_into}", + r.id, + r.slot, + slot_image[usize::from(r.slot)] + ); + assert_ne!( + decoded_into, dst, + "frame {frames}: reference picture {} resolves to the image \ + this very frame is decoding into", + r.id + ); + scope_refs += 1; + } + + // Post-submit bookkeeping, in `decode_planned`'s order. + pictures[dst].pending = true; + pictures[dst].bound = true; + slot_image[setup] = Some(dst); + slot_refs[setup] = Some(vk.setup_ref); + for r in &vk.refs { + slot_refs[usize::from(r.slot)] = Some(r.std); + } + for &id in &vk.release_after_decode { + assert!(slots.release(id), "frame {frames}: deferred release missed"); + } + pending.insert(setup_id, dst); + image_of.insert(setup_id, dst); + + let (ready, dropped) = + settle_dpb_ids(&mut pending, &plan.dpb.outputs, &plan.dpb.removed); + for image in ready.into_iter().chain(dropped) { + // A consumer that displays and releases at once: the harshest + // case for the pool, because an image comes back free the + // instant nothing else pins it. + pictures[image].pending = false; + } + for &id in &plan.dpb.removed { + image_of.remove(&id); + } + if plan.header.refresh_frame_flags == 0 { + slots.release(setup_id); + if let Some(image) = pending.remove(&setup_id) { + pictures[image].pending = false; + } + image_of.remove(&setup_id); + } + } + } + + assert_eq!(frames, 274, "every frame of the vector must decode"); + // Anti-vacuity. Releasing a displaced reference eagerly — the shape every + // codec in this crate shipped with — gave its slot to the decode target on + // 268 of these 274 frames, measured, the first at frame 6 (AU 4 of the + // stream, which is the AU the driver refused). So if this count ever + // reaches 0 the vector stopped exercising the case and the assertions above + // are comparing empty lists. + assert_eq!( + deferring, 268, + "268 of 274 frames displace a picture they are reading; at zero, \ + `release_after_decode` could be deleted and nothing here would fail" + ); + assert_eq!( + scope_refs, 1616, + "the references actually bound into a coding scope across the vector" + ); + eprintln!( + "frames {frames} · scope references {scope_refs} · deferred releases {deferring}" + ); + } +} diff --git a/crates/pf-vkdecode/src/decoder_h265.rs b/crates/pf-vkdecode/src/decoder_h265.rs new file mode 100644 index 00000000..893dde8d --- /dev/null +++ b/crates/pf-vkdecode/src/decoder_h265.rs @@ -0,0 +1,1942 @@ +//! [`VkH265Decoder`]: the assembled native H.265 decoder — [`crate::decoder`] one +//! codec over, over pf-bitstream's H.265 planner and M3's CPU half. +//! +//! Per AU: `plan_au` → `plan_to_vk_h265` → slices-only upload into the bitstream +//! ring → record (barriers, `vkCmdBeginVideoCodingKHR` with every bound DPB slot, +//! the one-time session RESET control, a caps-gated `RESULT_STATUS_ONLY` query +//! bracketing `vkCmdDecodeVideoKHR`) → submit on the decode queue under the +//! caller's [`QueueLock`] with a per-image timeline signal. +//! +//! Everything codec-agnostic is SHARED with the H.264 decoder rather than +//! re-implemented: the picture pool and its zero-copy hand-off contract +//! ([`crate::images`]), the bitstream ring and its slices-only packing, the op ring +//! (command buffers + status queries), the pending/ready/graveyard bookkeeping, +//! and `settle_dpb`/`build_frame` from [`crate::decoder`]. What is genuinely +//! H.265's own lives here: +//! +//! - **The picture format is the stream's, not a constant.** Main decodes to NV12, +//! Main 10 to P010, RExt 4:4:4 to the two-plane 4:4:4 formats — and the same +//! facts shape the Vulkan profile every object is created against +//! ([`H265ProfileKey`]). A device that cannot host the combination is refused +//! BEFORE a session exists, so the ladder demotes cleanly. +//! - **Every referenced slot must be BOUND by this decode op.** +//! `StdVideoDecodeH265PictureInfo`'s `RefPicSetStCurrBefore`/`StCurrAfter`/ +//! `LtCurr` arrays name DPB SLOT INDICES ([`crate::pic_h265`] builds them, and +//! its `DecodePlanVkH265::std_pic` docs carry the full argument for why they are +//! slots rather than positions in the reference list). The recording below lays +//! the op's references out in exactly [`DecodePlanVkH265::refs`] order — the set +//! of slots those arrays can name — and FAILS CLOSED if any of them has no bound +//! image: a named slot the op does not bind is unresolvable for the hardware. +//! (H.264 has no such arrays and only traces the case.) +//! - **Slice SEGMENT offsets, rebased and prefix-normalised.** The plan's offsets +//! are AU-relative and start-code-inclusive; the ring carries the slice NALUs +//! ALONE (non-VCL NALUs inside the decode range hang VCN firmware — the +//! `vcn_unified_0` ring timeout), so `pSliceSegmentOffsets` gets +//! [`pack_slices`]' ([`crate::ring`]) output, which also trims each segment to a +//! three-byte Annex-B prefix so drivers that reach the slice header by a fixed +//! `+3 +2` skip land on it. +//! - **RASL skips are not failures.** A RASL picture after an open-GOP CRA join is +//! undecodable by definition (8.1.3 NOTE); [`VkH265Decoder::decode`] answers it +//! like any other decode that produced no new picture — the next frame already +//! queued, or `None` — leaving the planner and the DPB untouched. No re-anchor, +//! no keyframe request: the very next AU plans normally. +//! +//! Codec dispatch (which decoder a stream gets) is the client wiring's job, not +//! this crate's: the public surface here mirrors [`crate::VkH264Decoder`] +//! method-for-method so the dispatch is a two-arm enum. + +use std::collections::BTreeMap; +use std::collections::VecDeque; + +use ash::vk; +use ash::vk::native as hh; +use pf_bitstream::h265::AuPlan; +use pf_bitstream::h265::H265Planner; +use pf_bitstream::h265::PicId; +use pf_bitstream::h265::PlanError; +use pf_bitstream::h265::PlanWarning; +use tracing::debug; +use tracing::trace; + +use crate::caps::DecodeCaps; +use crate::caps::DecodeProfile; +use crate::caps_h265::derive_caps_h265; +use crate::caps_h265::query_h265_caps; +use crate::caps_h265::H265ProfileKey; +use crate::decoder::build_frame; +use crate::decoder::settle_dpb; +use crate::decoder::wait_timeline; +use crate::decoder::DecodeStatus; +use crate::decoder::DecodedVkFrame; +use crate::decoder::OpRing; +use crate::decoder::PendingPic; +use crate::decoder::RetiredPool; +use crate::decoder::VkDecodeError; +use crate::device::DecodeDevice; +use crate::device::DeviceHandles; +use crate::device::QueueLock; +use crate::device::QueueSubmitGuard; +use crate::images::plan_pools; +use crate::images::DpbPool; +use crate::images::PicturePool; +use crate::params_h265::level_to_std as level_to_std_h265; +use crate::pic_h265::plan_to_vk_h265; +use crate::pic_h265::DecodePlanVkH265; +use crate::pic_h265::PlanToVkH265Error; +use crate::ring::pack_slices; +use crate::ring::BitstreamRing; +use crate::ring::RingLayout; +use crate::ring::UploadedAu; +use crate::ring::INITIAL_SLOT_SIZE; +use crate::ring::RING_SLOTS; +use crate::session_h265::ParamsActionH265; +use crate::session_h265::SessionConfigH265; +use crate::session_h265::VideoSessionH265; +use crate::session_h265::VpsSource; +use crate::slots::SlotMap; + +/// Everything tied to ONE H.265 session generation. A stream renegotiation +/// (extent, DPB depth, profile — including a bit-depth or chroma-format switch) +/// retires it and builds fresh. +struct SessionStateH265 { + session: VideoSessionH265, + slots: SlotMap, + /// Distinct mode's reference-only DPB backing; `None` in coincide mode (the + /// picture pool backs the DPB there). + dpb: Option, + pool: PicturePool, + ring: BitstreamRing, + ops: OpRing, + /// Last-known Std reference info per DPB slot — `vkCmdBeginVideoCodingKHR` + /// wants codec reference info for EVERY bound slot, including ones this AU's + /// RPS does not reference; refreshed from each plan's setup/ref entries so + /// marking transitions (short-term → long-term promotion, which in HEVC only + /// ever happens through a LATER picture's `RefPicSetLtCurr`) propagate. + slot_refs: Vec>, + /// Coincide mode: which pool image each DPB slot currently binds (rebound at + /// every activation — the decoupling that keeps delivered images safe). + slot_image: Vec>, + /// Per command-buffer completion tokens (reuse gate). + cmd_marks: Vec>, + /// Per query-slot submission ordinals (staleness validation). + query_marks: Vec, + /// Submissions recorded on this session (cmd/query indexing). + submitted: u64, + /// The newest submission's completion token (session drain). + last_submit: Option<(vk::Semaphore, u64)>, + /// The STREAM's coded extent (renegotiation comparison). + coded_extent: vk::Extent2D, + /// The granularity-aligned allocation extent (picture resources + frames). + image_extent: vk::Extent2D, +} + +/// The post-failure recovery latch: set when an AU failed after its planning had +/// already advanced, consumed by the next `decode`, which flushes to the next +/// IRAP before planning anything new. +/// +/// Why this exists at all — the fail-closed/recover split: +/// +/// This decoder FAILS CLOSED, and that stays: when an AU cannot be carried +/// through to a submitted decode, it returns an error rather than substituting a +/// reference or decoding against a slot whose image is gone. H.264's +/// soft-degrade (trace the missing binding, drop that reference, decode anyway) +/// is not available here because `StdVideoDecodeH265PictureInfo`'s +/// `RefPicSetStCurrBefore`/`StCurrAfter`/`LtCurr` arrays hold INDICES into the +/// decode op's reference array — dropping one entry re-points every later index +/// at the wrong picture, which is the corruption-hiding class this crate refuses. +/// +/// But failing closed once must not wedge the stream FOREVER, and without this +/// latch it did: by the time an AU reaches a failure exit, `plan_to_vk_h265` has +/// already mutated the [`SlotMap`] (releases + the setup assignment) and the +/// coincide binding sync has already cleared the setup slot's image binding. The +/// planner and the slot map then both believe picture N is resident while no +/// image holds it, so every later AU referencing N fails in +/// [`build_scope`] with `UnboundReferenceSlot` — a transient consumer +/// backpressure (`NoFreeSlot` on AU N) turned into a permanently dead stream. +/// +/// The recovery is a FLUSH TO THE NEXT IRAP, not H.264-style reference +/// substitution: `H265Planner::flush` drops the whole DPB and refuses everything +/// until the next IRAP, this decoder resets the slot ledger and image bindings to +/// match, and the integration layer — which already sets `want_keyframe` on EVERY +/// decode error — has an IDR on the way. So the stream re-anchors on real, +/// complete data instead of resuming over a DPB nobody can vouch for. +/// +/// Its own type (the [`crate::session::ResetArm`] idiom one module over) so the +/// latch/consume cycle is unit-testable without a live device. +#[derive(Debug, Default)] +pub(crate) struct RecoveryLatch(bool); + +impl RecoveryLatch { + /// Record that recovery is owed. Idempotent: two failures in a row still owe + /// exactly one flush. + pub(crate) fn latch(&mut self) { + self.0 = true; + } + + /// Whether recovery is owed, CLEARING the latch — the recovery runs once per + /// failure run, not on every later decode. + pub(crate) fn take(&mut self) -> bool { + std::mem::take(&mut self.0) + } + + /// Whether recovery is owed, without consuming it (state snapshots). + pub(crate) fn is_latched(&self) -> bool { + self.0 + } +} + +/// The native Vulkan Video H.265 decoder. Mirrors [`crate::VkH264Decoder`]'s +/// public surface exactly. +pub struct VkH265Decoder { + dev: DecodeDevice, + lock: Box, + planner: H265Planner, + /// Caps per profile key, queried once per profile (a Main→Main 10 switch is a + /// different key and re-queries). + caps: Option<(H265ProfileKey, DecodeCaps)>, + state: Option, + /// Decoded pictures awaiting their planner output verdict, keyed by [`PicId`]. + pending: BTreeMap, + /// Display-ready frames not yet handed out. + ready: VecDeque, + /// Retired generations' pools with consumer-held images (die on their last + /// release token). + graveyard: Vec, + /// The most recent plan's warnings ([`Self::take_warnings`]). + last_warnings: Vec, + /// The outstanding recovery point SEI, if any — see [`crate::recovery`]. + /// Named apart from [`Self::recovery`], which is this decoder's DPB-recovery + /// latch: the two are unrelated (one is a fact about the stream's prediction + /// structure, the other about this decoder's own wedged state). + recovery_watch: crate::recovery::RecoveryWatch, + /// Pictures planned so far — stamped onto each one as + /// [`DecodedVkFrame::decode_order`]. Survives session rebuilds for the same + /// reason the watch does. + decoded: u64, + /// Session generation: bumped on every rebuild, stamped into frames. + generation: u64, + device_lost: bool, + /// Recovery owed after a failed AU whose planning had already advanced + /// ([`RecoveryLatch`] docs for the whole argument). + recovery: RecoveryLatch, +} + +impl VkH265Decoder { + /// Wrap the borrowed device. Sessions/pools are built lazily from the first + /// AU's SPS (their shape is the stream's, not the device's). + /// + /// # Safety + /// + /// The full [`DeviceHandles`] caller contract (liveness, enabled extensions + /// and features, truthful queue families) — held for this decoder's whole + /// lifetime, not just this call. The device must additionally have been + /// created with `VK_KHR_video_decode_h265` enabled; that part of the contract + /// is checked below AS FAR AS IT CAN BE — the check reads the decode queue + /// family's advertised `videoCodecOperations`, which is the + /// device's own claim about the family, not proof that the client enabled the + /// extension at `vkCreateDevice`. (punktfunk's presenter enables h264 + h265 + + /// av1, filtered by what the device supports — `pf-presenter/src/vk/setup.rs` + /// — so the two coincide there.) Getting it wrong is undefined behaviour at + /// session creation rather than an error, which is why the family check runs + /// before anything is queried or created. + pub unsafe fn new( + handles: &DeviceHandles, + lock: Box, + ) -> Result { + // SAFETY: forwarded caller contract. + let dev = unsafe { DecodeDevice::wrap(handles)? }; + // Before anything is queried or created: does this queue family actually + // run H.265 decode ops? `query_h265_caps` would succeed on capable + // hardware regardless (physical-device query), and the first + // `vkCreateVideoSessionKHR` with a DECODE_H265 profile on a device that + // never enabled the extension is UB — this is the ladder's clean demote. + dev.require_codec_op(vk::VideoCodecOperationFlagsKHR::DECODE_H265, "H.265 decode")?; + Ok(Self { + dev, + lock, + planner: H265Planner::new(), + caps: None, + state: None, + pending: BTreeMap::new(), + ready: VecDeque::new(), + graveyard: Vec::new(), + last_warnings: Vec::new(), + recovery_watch: crate::recovery::RecoveryWatch::new(), + decoded: 0, + generation: 0, + device_lost: false, + recovery: RecoveryLatch::default(), + }) + } + + /// Ask the device, BEFORE a single AU is fed, whether it can decode a stream of + /// the negotiated (chroma format, bit depth) shape — the construction-time half + /// of what the lazy `ensure_state` path would otherwise only discover at the + /// first SPS. + /// + /// Why it exists: the session's picture format is the STREAM's, and a device + /// that advertises H.265 decode need not advertise a picture format for every + /// shape of it — 4:4:4 RExt is absent everywhere but NVIDIA, and 10-bit is + /// absent on some older silicon. Discovering that lazily makes the refusal a + /// mid-stream ERROR STREAK, which demotes past the FFmpeg rungs to + /// VAAPI/D3D11VA/software; discovering it here makes it a construction failure, + /// which the client's ladder answers by falling through to the next rung with + /// the session's hardware decode intact. Same query, same derivation, same + /// [`crate::CapsError`] — only the timing differs. + /// + /// The negotiated facts are a HINT (the in-band SPS is authoritative), so this + /// is deliberately not a promise that decode will succeed: the level ceiling and + /// an SPS that disagrees with the Welcome still surface at the first AU. What it + /// does guarantee is that a shape the device provably cannot host never gets a + /// session built for it. + pub fn probe_stream_support( + &self, + chroma_format_idc: u8, + bit_depth_luma_minus8: u8, + ) -> Result<(), VkDecodeError> { + let key = H265ProfileKey::from_negotiated(chroma_format_idc, bit_depth_luma_minus8)?; + let wanted = key + .output_format() + .expect("from_negotiated gated the chroma/depth combination"); + // SAFETY: the constructor's `DeviceHandles` contract holds for this + // decoder's whole lifetime, so the physical device is live — the same + // proof `ensure_state`'s identical call carries. + let raw = unsafe { query_h265_caps(&self.dev, key) }.map_err(VkDecodeError::from)?; + derive_caps_h265(&raw, wanted)?; + Ok(()) + } + + /// Decode one access unit. Returns the next display-ready frame, if the + /// planner declared one. + /// + /// A RASL picture the planner refuses after an open-GOP join is NOT an error: + /// it is undecodable BY DEFINITION (8.1.3 NOTE — its references precede the + /// join), so this returns `Ok` with whatever was already display-ready (a + /// frame an earlier AU decoded, or `None`) and leaves the planner, the DPB and + /// the slot ledger untouched; the next AU plans normally. Treating it as an + /// error would make every CRA join request a keyframe the host has no reason + /// to send. The warning ledger IS cleared, per [`Self::take_warnings`]. + /// + /// Never panics. `VkDecodeError::DeviceLost` latches: every later call fails + /// fast until the owner rebuilds the decoder on fresh handles. + pub fn decode(&mut self, au: &[u8]) -> Result, VkDecodeError> { + if self.device_lost { + return Err(VkDecodeError::DeviceLost); + } + let result = self.decode_inner(au); + if matches!(result, Err(VkDecodeError::DeviceLost)) { + self.device_lost = true; + } + result + } + + fn decode_inner(&mut self, au: &[u8]) -> Result, VkDecodeError> { + // A previous AU failed after its planning had advanced: clear the stale + // DPB residency BEFORE planning this one, or every AU referencing the + // stranded picture fails forever ([`RecoveryLatch`] docs). + if self.recovery.take() { + self.recover_dpb(); + } + // Cleared BEFORE planning for the same reason the RASL arm below clears it: + // "cleared by the next decode" must hold for an AU that fails to plan at + // all, or the previous AU's warnings could be re-read as fresh damage. The + // ledger is drained after every successful decode (`take_warnings` is a + // `mem::take`), so the failed-plan case is the only one that could carry + // over — a hole closed by construction, not a fix for a field symptom. + self.last_warnings.clear(); + let plan = match self.planner.plan_au(au) { + Ok(plan) => plan, + Err(PlanError::RaslSkipped { poc }) => { + trace!(poc, "RASL picture after a CRA join — skipped, not failed"); + return Ok(self.ready.pop_front()); + } + Err(e) => return Err(VkDecodeError::PlanH265(e)), + }; + for warning in &plan.warnings { + // The recovery verdict is the integration layer's + // ([`Self::take_warnings`]); never silent here though. + trace!(?warning, "plan warning"); + } + self.last_warnings = plan.warnings.clone(); + // One picture per AU under this envelope: stamp its DECODE-order ordinal + // before anything can reorder it (see `DecodedVkFrame::decode_order`). + self.decoded = self.decoded.saturating_add(1); + let decode_order = self.decoded; + // The recovery-point watch, folded ONCE per successfully planned AU and in + // DECODE order — the order the SEI's POC delta is measured in. The mark + // rides the pending picture into display order (crate::recovery). + let recovery = self.recovery_watch.note_h265( + plan.picture.pic_order_cnt, + plan.picture.is_irap, + plan.picture.recovery_point, + ); + if recovery != crate::recovery::RecoveryMark::NONE { + trace!( + sei = recovery.sei_here, + recovery_point = recovery.is_recovery_point, + poc = plan.picture.pic_order_cnt, + "recovery point SEI" + ); + } + + // From here the PLANNER has already advanced past this AU — its DPB holds + // the picture whatever happens next — so any failure below leaves the + // planner's DPB and this decoder's slot/image ledgers able to disagree. + // Latch the recovery for the next decode rather than returning into a + // permanently wedged state. (Deliberately wider than the paths that + // mutate the SlotMap: a failure BEFORE `plan_to_vk_h265` mutates it — + // `UnresolvedReference`, an `ensure_state` refusal — strands the picture + // the other way round, planner-resident with no slot at all, and wedges + // just as hard. One flush cures both.) + let result = self.decode_planned(&plan, au, recovery, decode_order); + if result.is_err() { + self.recovery.latch(); + } + result + } + + /// The submission half of one decode, from the point the planner has already + /// advanced. Split out so [`Self::decode_inner`] can latch recovery on ANY + /// failure past that line without threading a flag through every exit. + /// `au` is the same buffer `plan`'s slice ranges index into; `recovery` is the + /// recovery-point verdict already folded for this AU and `decode_order` its + /// decode-order ordinal (both advance in decode order, so neither can be + /// derived here — this path is not reached for every planned AU). + fn decode_planned( + &mut self, + plan: &AuPlan, + au: &[u8], + recovery: crate::recovery::RecoveryMark, + decode_order: u64, + ) -> Result, VkDecodeError> { + self.ensure_state(plan)?; + + // The VPS this SPS activates: the stream's own, or the fallback identity + // for a stream joined after its VPS NALU (session_h265 module docs). + let vps = VpsSource::for_sps(&plan.sps); + + // Convert, with ONE rebuild retry on CapacityMismatch — the designed + // trigger for a DPB-depth renegotiation (pic_h265.rs docs). + let mut vk_plan: Option = None; + for attempt in 0..2 { + // A parameters RECREATE destroys the old object, which an in-flight + // decode may still be executing against: drain first. + if self + .state + .as_ref() + .expect("ensure_state built it") + .session + .parameters_action(&vps, &plan.sps, &plan.pps) + == ParamsActionH265::Recreate + { + self.drain_gpu()?; + } + let state = self.state.as_mut().expect("ensure_state built it"); + // SAFETY: live device (constructor contract); the drain above + // satisfies ensure_parameters' Recreate contract, and Current/Add + // touch nothing a submitted decode reads. + unsafe { + state + .session + .ensure_parameters(&vps, &plan.sps, &plan.pps)? + }; + match plan_to_vk_h265(plan, &mut state.slots) { + Ok(converted) => { + vk_plan = Some(converted); + break; + } + Err(PlanToVkH265Error::CapacityMismatch { required, capacity }) if attempt == 0 => { + debug!( + required, + capacity, "DPB depth renegotiated — rebuilding session" + ); + self.rebuild_state(plan)?; + } + Err(e) => return Err(VkDecodeError::ConvertH265(e)), + } + } + let vk_plan = vk_plan.expect("the rebuilt session matches its own plan"); + + let state = self.state.as_mut().expect("ensured above"); + // The per-AU active-reference gate: the session was created with + // maxActiveReferencePictures; binding more in one decode op would be a + // silent VUID violation on the drivers that matter most. + let max_active = state.session.config.max_active_references as usize; + if vk_plan.refs.len() > max_active { + return Err(VkDecodeError::Unsupported(format!( + "AU references {} pictures, session allows {max_active} active references", + vk_plan.refs.len() + ))); + } + + // Coincide binding sync: slots the planner released no longer bind their + // images (the pictures may still be pending/held — untouched), and the + // setup slot's PREVIOUS binding is cleared before it binds fresh. + let setup = usize::from(vk_plan.setup_slot); + if state.dpb.is_none() { + let mut held = vec![false; state.slot_image.len()]; + for (slot, _id) in state.slots.held() { + held[usize::from(slot)] = true; + } + for (slot, binding) in state.slot_image.iter_mut().enumerate() { + if let Some(picture) = *binding { + if !held[slot] || slot == setup { + state.pool.pictures[picture].bound = false; + *binding = None; + } + } + } + } + + // The decode target: a FREE pool image (never one a consumer holds — the + // whole point of the pool model). + let Some(dst) = state.pool.free_index() else { + debug!( + held = state.pool.held_total(), + "picture pool exhausted — release_frame owed" + ); + return Err(VkDecodeError::NoFreeSlot); + }; + + // Cross-queue waits (the AVVkFrame contract): the dst image's last known + // timeline value (covers a presenter write-back after release), plus — + // coincide mode — every referenced image's value, so reference reads + // order after any presenter layout restore already reported back. + let mut waits: Vec<(vk::Semaphore, u64)> = Vec::new(); + { + let dst_pic = &state.pool.pictures[dst]; + if dst_pic.value > 0 { + waits.push((dst_pic.semaphore, dst_pic.value)); + } + } + if state.dpb.is_none() { + for r in &vk_plan.refs { + if let Some(picture) = state.slot_image[usize::from(r.slot)] { + let pic = &state.pool.pictures[picture]; + if pic.value > 0 && !waits.iter().any(|(sem, _)| *sem == pic.semaphore) { + waits.push((pic.semaphore, pic.value)); + } + } + } + } + let signal_value = state.pool.pictures[dst].value + 1; + + // Command buffer + query slot for this submission. + let submission = state.submitted; + let cmd_index = (submission % state.ops.cmds.len() as u64) as usize; + if let Some((sem, value)) = state.cmd_marks[cmd_index] { + // SAFETY: live device; the token is a pool image's semaphore. + unsafe { wait_timeline(self.dev.ash(), sem, value, "command buffer reuse")? }; + } + let query_index = (submission % u64::from(state.ops.query_count)) as u32; + + // Upload the AU (recycles/grows against submission-completion tokens). + let device = self.dev.ash().clone(); + let mut poll = |token: &(vk::Semaphore, u64)| -> Result { + // SAFETY: live device; the token's semaphore is a pool semaphore. + let current = unsafe { device.get_semaphore_counter_value(token.0) } + .map_err(VkDecodeError::from)?; + Ok(current >= token.1) + }; + let device2 = self.dev.ash().clone(); + let mut wait = |token: &(vk::Semaphore, u64)| -> Result<(), VkDecodeError> { + // SAFETY: as above. + unsafe { wait_timeline(&device2, token.0, token.1, "bitstream slot drain") } + }; + // SLICE SEGMENT NALUs only, concatenated — an HEVC AU opens with AUD/SEI + // (and, at IRAPs, VPS/SPS/PPS) NALUs, and feeding those to the VCN + // firmware inside the decode range HANGS it (the .25 `vcn_unified_0 ring + // timeout` the H.264 path was built around; the parameter sets ride the + // session parameters object instead). The plan's AU-relative offsets are + // rebased into the packed buffer. + let plan_segments: Vec> = + plan.slices.iter().map(|s| s.data.clone()).collect(); + // One rebased offset per slice segment, in plan order — the same count and + // order as `vk_plan.slice_offsets` (both are built by walking + // `plan.slices`), so `pSliceSegmentOffsets` and `sliceSegmentCount` agree + // by construction rather than by check. `pack_slices` additionally + // normalises each segment's Annex-B prefix to THREE bytes and computes the + // offsets from the normalised lengths, in one call, so the bytes and the + // offsets cannot drift apart (`crate::ring::three_byte_prefix` — the + // four-byte prefix this vector's 249 TRAIL segments carry is what shifted + // NVIDIA's slice-header parse by a byte). + let Some(packed) = pack_slices(au, &plan_segments) else { + return Err(VkDecodeError::Unsupported( + "packed slice data exceeds the u32 offsets Vulkan submits".into(), + )); + }; + let slice_offsets = packed.offsets; + // SAFETY: live device; the segments are the plan's own in-bounds slice + // ranges (narrowed by the prefix normalisation, so still in bounds); every + // pending token is the completion signal of the submission that consumed + // the slot. + let upload = unsafe { + state + .ring + .upload(&self.dev, au, &packed.segments, &mut poll, &mut wait)? + }; + + // Record + submit, signalling the dst image's next timeline value. + // SAFETY: live device; every handle recorded below belongs to this + // session generation, and the packed slices sit uploaded in the ring slot. + unsafe { + record_and_submit_h265( + &self.dev, + &*self.lock, + state, + &vk_plan, + &slice_offsets, + &upload, + dst, + cmd_index, + query_index, + &waits, + signal_value, + )?; + } + + // Post-submit bookkeeping. + let dst_sem = state.pool.pictures[dst].semaphore; + state.pool.pictures[dst].value = signal_value; + state.pool.pictures[dst].pending = true; + if state.dpb.is_none() { + state.pool.pictures[dst].bound = true; + state.slot_image[setup] = Some(dst); + } + state.cmd_marks[cmd_index] = Some((dst_sem, signal_value)); + state.query_marks[query_index as usize] = submission; + state.submitted += 1; + state.last_submit = Some((dst_sem, signal_value)); + state + .ring + .pending + .set_pending(upload.slot, (dst_sem, signal_value)); + + // Refresh the per-slot reference cache from this AU's facts. + state.slot_refs[setup] = Some(vk_plan.setup_ref); + for r in &vk_plan.refs { + state.slot_refs[usize::from(r.slot)] = Some(r.std); + } + + self.pending.insert( + vk_plan.setup_id, + PendingPic { + image: dst, + submission, + query_slot: query_index, + timeline_value: signal_value, + crop: plan.picture.display_crop, + colour: plan.picture.colour, + poc: plan.picture.pic_order_cnt, + is_idr: plan.picture.is_idr, + recovery, + decode_order, + }, + ); + + // The plan's DPB verdicts over the pending map: outputs become ready + // frames (their images move pending → held until released); + // removed-but-never-output pictures free their images. + let (ready, dropped) = settle_dpb(&mut self.pending, &plan.dpb); + let state = self.state.as_mut().expect("ensured above"); + for entry in ready { + let frame = build_frame( + &mut state.pool, + state.dpb.is_none(), + state.image_extent, + &entry, + self.generation, + ); + self.ready.push_back(frame); + } + for entry in dropped { + debug!( + poc = entry.poc, + "picture removed without output — freeing its image" + ); + state.pool.pictures[entry.image].pending = false; + } + Ok(self.ready.pop_front()) + } + + /// Hand a delivered frame back. `presenter_signaled` reports whether the + /// consumer SAMPLED the image (and therefore enqueued the `value + 1` + /// timeline signal per the [`DecodedVkFrame`] contract) — the decoder then + /// waits that write-back before the image's next use. Every frame + /// `decode`/`take_ready` returns must come back exactly once, including + /// stale-generation frames (their retired pool dies on its last token). + pub fn release_frame( + &mut self, + frame: &DecodedVkFrame, + presenter_signaled: bool, + ) -> Result<(), VkDecodeError> { + let pool = if frame.generation == self.generation { + match &mut self.state { + Some(state) => &mut state.pool, + None => { + return Err(VkDecodeError::StaleFrame { + frame_generation: frame.generation, + current_generation: self.generation, + }) + } + } + } else { + match self + .graveyard + .iter_mut() + .find(|r| r.generation == frame.generation) + { + Some(retired) => &mut retired.pool, + None => { + return Err(VkDecodeError::StaleFrame { + frame_generation: frame.generation, + current_generation: self.generation, + }) + } + } + }; + let index = frame.picture as usize; + if index >= pool.pictures.len() { + return Err(VkDecodeError::StaleFrame { + frame_generation: frame.generation, + current_generation: self.generation, + }); + } + let picture = &mut pool.pictures[index]; + match picture.held.checked_sub(1) { + Some(remaining) => picture.held = remaining, + None => { + debug!(index, "frame released more often than delivered"); + return Ok(()); + } + } + if presenter_signaled { + picture.value = picture.value.max(frame.value + 1); + } + // A retired pool dies on its last token (presenter fence-waited before + // the token per the release contract; decode work drained at retirement). + if frame.generation != self.generation { + self.graveyard + .retain(|r| r.generation != frame.generation || r.pool.held_total() > 0); + } + Ok(()) + } + + /// A display-ready frame beyond the one `decode` returned, if any. Drain after + /// every decode; frames left here still occupy pool images. + pub fn take_ready(&mut self) -> Option { + self.ready.pop_front() + } + + /// The warnings of the most recent successfully planned AU (concealment + /// signals — the integration layer's want_keyframe hook). Cleared by the + /// next `decode`. + pub fn take_warnings(&mut self) -> Vec { + std::mem::take(&mut self.last_warnings) + } + + /// The current session generation ([`DecodedVkFrame::generation`] of newly + /// delivered frames). + pub fn generation(&self) -> u64 { + self.generation + } + + /// The DECODE-order ordinal of the most recently planned picture — the + /// watermark a consumer compares [`DecodedVkFrame::decode_order`] against to + /// tell a frame decoded before a loss from one decoded after it. Especially + /// load-bearing here: [`Self::recover_dpb`] flushes every buffered picture + /// into `ready` at once, so a pre-loss picture routinely reaches the consumer + /// after the loss that flushed it. 0 before the first AU plans. + pub fn decode_order(&self) -> u64 { + self.decoded + } + + /// One-line state snapshot for failure paths and field logs (not a stable + /// format). + pub fn debug_snapshot(&self) -> String { + // A latched recovery is the single most useful thing to see next to a + // failure: it says the NEXT decode flushes to an IRAP rather than + // resuming (RecoveryLatch docs). + let recovery = if self.recovery.is_latched() { + " recovery=owed" + } else { + "" + }; + match &self.state { + None => format!("gen={}{recovery} ", self.generation), + Some(state) => { + let occupancy: Vec = state + .pool + .pictures + .iter() + .enumerate() + .map(|(i, p)| { + format!( + "{i}:{}{}h{}", + if p.bound { "B" } else { "-" }, + if p.pending { "P" } else { "-" }, + p.held + ) + }) + .collect(); + format!( + "h265 gen={}{recovery} mode={} slots_held={}/{} pool=[{}] pending={} \ + ready={} graveyard={}", + self.generation, + if state.dpb.is_none() { + "coincide" + } else { + "distinct" + }, + state.slots.active(), + state.slots.capacity(), + occupancy.join(" "), + self.pending.len(), + self.ready.len(), + self.graveyard.len(), + ) + } + } + } + + /// Read `frame`'s decode status WITHOUT waiting. + /// + /// [`DecodeStatus::Failed`] covers driver-reported errors AND a query slot + /// re-armed before it was read (the status is then unprovable — same + /// conservative verdict). + /// + /// On drivers whose decode family lacks `queryResultStatusSupport` (RADV) + /// there is no per-op verdict to read: `Ok` then means "the decode op + /// COMPLETED on the timeline" — the same information FFmpeg has on every + /// driver, no worse. + pub fn poll_status(&mut self, frame: &DecodedVkFrame) -> DecodeStatus { + self.read_status(frame, false) + } + + /// Does this decode queue family answer per-op `RESULT_STATUS` queries at all? + /// See [`crate::VkH264Decoder::status_queries`] — the fact is the DEVICE's, identical + /// for both codecs, and it is what tells a clean integrity report apart from + /// an undetectable one. + pub fn status_queries(&self) -> bool { + self.dev.result_status_queries() + } + + /// [`Self::poll_status`], but WAITs for the op to complete first. + pub fn wait_status(&mut self, frame: &DecodedVkFrame) -> DecodeStatus { + self.read_status(frame, true) + } + + fn read_status(&mut self, frame: &DecodedVkFrame, block: bool) -> DecodeStatus { + if frame.generation != self.generation { + trace!( + frame_generation = frame.generation, + current = self.generation, + "status asked for a stale-generation frame — Failed, without \ + touching the new pools" + ); + return DecodeStatus::Failed; + } + let Some(state) = &self.state else { + return DecodeStatus::Failed; + }; + let Some(query_pool) = state.ops.query_pool else { + // No queries on this driver: the verdict degrades to timeline + // completion (poll_status docs). + if block { + // SAFETY: live device; pool-owned semaphore. + return match unsafe { + wait_timeline(self.dev.ash(), frame.semaphore, frame.value, "status wait") + } { + Ok(()) => DecodeStatus::Ok, + Err(VkDecodeError::DeviceLost) => { + self.device_lost = true; + DecodeStatus::Failed + } + Err(_) => DecodeStatus::Failed, + }; + } + // SAFETY: live device; pool-owned semaphore. + return match unsafe { self.dev.ash().get_semaphore_counter_value(frame.semaphore) } { + Ok(current) if current >= frame.value => DecodeStatus::Ok, + Ok(_) => DecodeStatus::Pending, + Err(vk::Result::ERROR_DEVICE_LOST) => { + self.device_lost = true; + DecodeStatus::Failed + } + Err(_) => DecodeStatus::Failed, + }; + }; + let slot = frame.query_slot as usize; + if slot >= state.query_marks.len() || state.query_marks[slot] != frame.submission { + trace!( + slot, + "status query slot re-armed before it was read — unprovable, reported Failed" + ); + return DecodeStatus::Failed; + } + let flags = if block { + vk::QueryResultFlags::WAIT | vk::QueryResultFlags::WITH_STATUS_KHR + } else { + vk::QueryResultFlags::WITH_STATUS_KHR + }; + let mut status = [0i32; 1]; + // SAFETY: live device; the query pool is this session generation's own and + // `frame.query_slot` indexes within its count (checked above against the + // marks array it is sized to). + let result = unsafe { + self.dev + .ash() + .get_query_pool_results(query_pool, frame.query_slot, &mut status, flags) + }; + match result { + // VkQueryResultStatusKHR: >0 complete, 0 not ready, <0 error. + Ok(()) if status[0] > 0 => DecodeStatus::Ok, + Ok(()) if status[0] == 0 => DecodeStatus::Pending, + Ok(()) => DecodeStatus::Failed, + Err(vk::Result::NOT_READY) => DecodeStatus::Pending, + Err(vk::Result::ERROR_DEVICE_LOST) => { + self.device_lost = true; + DecodeStatus::Failed + } + Err(r) => { + debug!(?r, "status query read failed"); + DecodeStatus::Failed + } + } + } + + /// Wait — bounded by `timeout_ns` — for a delivered frame's decode-complete + /// signal. Pure measurement (the integration layer's sampled decode-latency + /// stat): touches no decoder state. `frame` must be unreleased, which pins its + /// pool — and with it the semaphore — alive. + pub fn wait_decoded(&self, frame: &DecodedVkFrame, timeout_ns: u64) -> bool { + if frame.generation != self.generation { + return false; + } + let semaphores = [frame.semaphore]; + let values = [frame.value]; + let info = vk::SemaphoreWaitInfo::default() + .semaphores(&semaphores) + .values(&values); + // SAFETY: live device (constructor contract); the semaphore is a pool + // semaphore the unreleased frame keeps alive (fn docs); the info arrays + // are locals outliving the call. + unsafe { self.dev.ash().wait_semaphores(&info, timeout_ns) }.is_ok() + } + + /// Drain the planner (teardown / stream discontinuity): every buffered + /// picture becomes display-ready via [`Self::take_ready`] (zero-copy — the + /// images already hold the content), all DPB slots free, and any picture + /// removed without ever reaching output frees its image. + pub fn flush(&mut self) { + let update = self.planner.flush(); + let (ready, dropped) = settle_dpb(&mut self.pending, &update); + if let Some(state) = &mut self.state { + state.slots.apply(&update); + for entry in ready { + let frame = build_frame( + &mut state.pool, + state.dpb.is_none(), + state.image_extent, + &entry, + self.generation, + ); + self.ready.push_back(frame); + } + for entry in dropped { + state.pool.pictures[entry.image].pending = false; + } + // Defensive: a pending picture neither output nor removed should not + // exist after a flush; free any leftover. + for (_, entry) in std::mem::take(&mut self.pending) { + debug!(poc = entry.poc, "pending picture survived a flush — freed"); + state.pool.pictures[entry.image].pending = false; + } + } else { + self.pending.clear(); + } + } + + /// Clear the DPB state a failed AU left behind, so planning resumes at the + /// next IRAP instead of erroring on residency nothing can honour. + /// + /// Three ledgers have to agree and, after a post-planning failure, do not: + /// the PLANNER's DPB, this decoder's [`SlotMap`], and the slot→image + /// bindings. [`Self::flush`] settles the first (and hands back any picture + /// that did reach output — those frames are real and are still delivered), + /// then [`reset_slot_bindings`] empties the other two. Pool images the stale + /// bindings pinned go back on the free list; images a consumer still HOLDS + /// stay pinned by their own `held` counts, exactly as they would across a + /// session rebuild. + /// + /// Deliberately not a session rebuild: the session, pools and ring are all + /// still valid — only the DPB bookkeeping is stale — and a rebuild would + /// churn every image allocation for a condition an IDR fixes anyway. + fn recover_dpb(&mut self) { + debug!( + snapshot = %self.debug_snapshot(), + "recovering from a failed AU — flushing the H.265 DPB to the next IRAP" + ); + self.flush(); + if let Some(state) = &mut self.state { + let unbound = reset_slot_bindings( + &mut state.slots, + &mut state.slot_image, + &mut state.slot_refs, + ); + for picture in unbound { + state.pool.pictures[picture].bound = false; + } + } + } + + /// Session/caps for THIS plan exist and match its extent + profile, and the + /// stream sits inside the device's level ceiling. DPB-depth mismatches + /// surface later as `plan_to_vk_h265`'s `CapacityMismatch` (the designed + /// trigger) and take the same rebuild path. + fn ensure_state(&mut self, plan: &AuPlan) -> Result<(), VkDecodeError> { + let key = profile_key_for(plan)?; + if self.caps.as_ref().map(|(k, _)| *k) != Some(key) { + let wanted = key + .output_format() + .expect("from_stream gated the chroma/depth combination"); + // SAFETY: live device (constructor contract). + let raw = unsafe { query_h265_caps(&self.dev, key) }.map_err(VkDecodeError::from)?; + self.caps = Some((key, derive_caps_h265(&raw, wanted)?)); + } + // The level gate: a stream above the device's maxLevelIdc is refused up + // front (within one codec the Std code points ascend with the level, so + // the comparison is numeric), never submitted on a hope. The ceiling came + // from an H.265 caps query, so it is compared against an H.265 code point + // — the pairing MaxLevelIdc's tag exists to keep honest. + let caps_max_level = self.caps.as_ref().expect("queried above").1.max_level_idc; + let stream_level = level_to_std_h265(plan.picture.level_idc); + if stream_level > caps_max_level.code_point() { + return Err(VkDecodeError::Unsupported(format!( + "stream level (Std code point {stream_level}) above the device's \ + maxLevelIdc ({caps_max_level})" + ))); + } + let coded = vk::Extent2D { + width: plan.picture.coded_width, + height: plan.picture.coded_height, + }; + match &self.state { + Some(state) if state.coded_extent == coded && state.session.config.profile == key => { + Ok(()) + } + _ => self.rebuild_state(plan), + } + } + + /// Tear down the current session generation (draining its decode work, + /// retiring its picture pool to the graveyard when the consumer still holds + /// images) and build a fresh one shaped by `plan`, bumping + /// [`Self::generation`] so frames of the old one route to the graveyard. + /// + /// The renegotiation-safety argument is [`crate::VkH264Decoder`]'s, unchanged: + /// pools with consumer holds retire INTACT to the graveyard, every frame and + /// token carries its generation, and the session objects (query pool included) + /// die only after [`Self::drain_gpu`] with no consumer-facing handle pointing + /// at them. + fn rebuild_state(&mut self, plan: &AuPlan) -> Result<(), VkDecodeError> { + self.drain_gpu()?; + if let Some(state) = self.state.take() { + debug!("rebuilding H.265 decode session (stream renegotiation)"); + let SessionStateH265 { mut pool, .. } = state; + for frame in self.ready.drain(..) { + let picture = &mut pool.pictures[frame.picture as usize]; + picture.held = picture.held.saturating_sub(1); + } + for (_, entry) in std::mem::take(&mut self.pending) { + pool.pictures[entry.image].pending = false; + } + for picture in &mut pool.pictures { + picture.bound = false; + } + let held = pool.held_total(); + if held > 0 { + debug!( + held, + generation = self.generation, + "consumer still holds images of the retired generation — graveyarding" + ); + self.graveyard.push(RetiredPool { + generation: self.generation, + pool, + }); + } + } + self.generation += 1; + + let (key, caps) = self.caps.as_ref().expect("ensure_state queried caps"); + let key = *key; + let required_slots = plan.picture.max_dpb_frames as u32 + 1; + if required_slots > caps.max_dpb_slots { + return Err(VkDecodeError::Unsupported(format!( + "stream needs {required_slots} DPB slots, device caps at {}", + caps.max_dpb_slots + ))); + } + let coded = vk::Extent2D { + width: plan.picture.coded_width, + height: plan.picture.coded_height, + }; + // Bounds-checked at the ALLOCATION extent (granularity-rounded): that is + // what the images are created at and what maxCodedExtent must cover. + let image_extent = caps.aligned_extent(coded); + if coded.width < caps.min_coded_extent.width + || coded.height < caps.min_coded_extent.height + || image_extent.width > caps.max_coded_extent.width + || image_extent.height > caps.max_coded_extent.height + { + return Err(VkDecodeError::Unsupported(format!( + "coded extent {}x{} (allocated {}x{}) outside device range {}x{}..{}x{}", + coded.width, + coded.height, + image_extent.width, + image_extent.height, + caps.min_coded_extent.width, + caps.min_coded_extent.height, + caps.max_coded_extent.width, + caps.max_coded_extent.height + ))); + } + + let config = SessionConfigH265 { + max_coded_extent: image_extent, + max_dpb_slots: required_slots, + max_active_references: (required_slots - 1).min(caps.max_active_references), + profile: key, + }; + let mut pool_plan = plan_pools(caps, required_slots); + // TEST-ONLY readback hook, exactly as the H.264 decoder's: the parity + // test copies decoded pictures back to hash them, and + // `vkCmdCopyImageToBuffer` needs TRANSFER_SRC on the source — a bit the + // zero-copy production pools deliberately do not carry. + if std::env::var("PF_VKD_TEST_READBACK").is_ok_and(|v| v == "1") { + pool_plan.picture_usage |= vk::ImageUsageFlags::TRANSFER_SRC; + } + let decode_profile = DecodeProfile::H265(key); + // SAFETY: live device per the constructor contract, for every create in + // this block; each created half is owned by a Drop type the moment it + // exists, so a mid-build failure unwinds cleanly. + let state = unsafe { + let session = VideoSessionH265::create(&self.dev, caps, config)?; + let dpb = if caps.coincide { + None + } else { + Some( + DpbPool::create(&self.dev, caps, &pool_plan, image_extent, decode_profile) + .map_err(VkDecodeError::from)?, + ) + }; + let pool = + PicturePool::create(&self.dev, caps, &pool_plan, image_extent, decode_profile) + .map_err(VkDecodeError::from)?; + let ring = BitstreamRing::create( + &self.dev, + RingLayout::new( + INITIAL_SLOT_SIZE, + RING_SLOTS, + caps.min_bitstream_offset_alignment, + caps.min_bitstream_size_alignment, + ), + decode_profile, + ) + .map_err(VkDecodeError::from)?; + let ops = OpRing::create( + &self.dev, + decode_profile, + pool_plan.picture_count, + RING_SLOTS, + ) + .map_err(VkDecodeError::from)?; + SessionStateH265 { + session, + slots: SlotMap::new(plan.picture.max_dpb_frames), + slot_refs: vec![None; required_slots as usize], + slot_image: vec![None; required_slots as usize], + cmd_marks: vec![None; RING_SLOTS as usize], + query_marks: vec![u64::MAX; pool_plan.picture_count as usize], + submitted: 0, + last_submit: None, + coded_extent: coded, + image_extent, + dpb, + pool, + ring, + ops, + } + }; + self.state = Some(state); + Ok(()) + } + + /// Wait out every in-flight decode submission of the current session. + fn drain_gpu(&mut self) -> Result<(), VkDecodeError> { + let Some(state) = &self.state else { + return Ok(()); + }; + if let Some((sem, value)) = state.last_submit { + // SAFETY: live device; the token is a pool image's semaphore. + unsafe { wait_timeline(self.dev.ash(), sem, value, "session drain")? }; + } + Ok(()) + } +} + +impl Drop for VkH265Decoder { + fn drop(&mut self) { + // Best-effort decode drain so the pools' Drop impls never destroy + // in-flight decode work; a wedged driver falls through after the bounded + // timeout. Presenter-side sampling of graveyarded/held images is the + // CALLER's teardown contract (the H.264 decoder's Drop docs). + if let Err(e) = self.drain_gpu() { + debug!(error = %e, "drain on drop failed; tearing down anyway"); + } + if !self.graveyard.is_empty() { + debug!( + pools = self.graveyard.len(), + "graveyard not fully token-drained at decoder drop — destroying anyway \ + (upstream teardown forfeited its bounded wait)" + ); + } + } +} + +/// The Vulkan profile this AU's stream needs — profile idc plus the chroma format +/// and bit depths, all of which the SPS carries and the profile must restate. +/// (`separate_colour_plane_flag` comes off the active SPS rather than the picture +/// plan, which does not carry it: the planner has no use for it, the profile gate +/// does.) +fn profile_key_for(plan: &AuPlan) -> Result { + H265ProfileKey::from_stream( + plan.picture.general_profile_idc, + plan.picture.chroma_format_idc, + plan.sps.separate_colour_plane_flag, + plan.picture.bit_depth_luma_minus8, + plan.picture.bit_depth_chroma_minus8, + ) + .map_err(VkDecodeError::ParamsH265) +} + +/// Empty the three per-slot ledgers a recovery resets: DPB residency, the +/// slot→image bindings and the cached per-slot reference info. Returns the pool +/// image indices the cleared bindings were pinning, for the caller to unbind +/// (pure over the ledgers so the recovery is testable without a device — the pool +/// is the one piece that needs one). +/// +/// All three are emptied TOGETHER on purpose: leaving reference info behind would +/// let [`build_scope`] bind a slot the planner no longer knows about, which is the +/// same "plausible-looking picture in the wrong place" the unbound-reference +/// refusal exists to prevent. +fn reset_slot_bindings( + slots: &mut SlotMap, + slot_image: &mut [Option], + slot_refs: &mut [Option], +) -> Vec { + // `release` is the only way a slot is freed (SlotMap docs); the collect is + // because `held` borrows the map the releases mutate. + for (_slot, id) in slots.held().collect::>() { + slots.release(id); + } + let unbound = slot_image.iter_mut().filter_map(Option::take).collect(); + for cached in slot_refs.iter_mut() { + *cached = None; + } + unbound +} + +/// The picture resource view bound for DPB `slot`: the bound pool image +/// (coincide) or the DPB array layer (distinct). +fn slot_view(state: &SessionStateH265, slot: u8) -> Option { + match &state.dpb { + Some(dpb) => Some(dpb.dpb_view(slot)), + None => state.slot_image[usize::from(slot)].map(|p| state.pool.pictures[p].view), + } +} + +/// One entry of a coding scope's bound-slot list: the DPB slot index it binds +/// (`-1` for the setup ACTIVATION entry), the picture resource view, and the +/// codec reference info that slot's association carries. +/// (No derived equality: `StdVideoDecodeH265ReferenceInfo` is a plain-C bindgen +/// struct without it. Assertions compare the fields that carry meaning.) +#[derive(Debug, Clone, Copy)] +struct ScopeEntry { + slot_index: i32, + view: vk::ImageView, + std: hh::StdVideoDecodeH265ReferenceInfo, +} + +/// Build the coding scope's bound-slot list and say how many leading entries are +/// THIS AU's references. +/// +/// The layout: +/// +/// 1. every entry of `refs`, IN ORDER — the decode op takes exactly this prefix; +/// 2. every other still-held slot, so its association survives the scope (their +/// resources must stay bound even when this AU does not reference them); +/// 3. the setup slot as the activation entry, slot index `-1`. +/// +/// A reference whose slot binds no image is a hard error, never a skip: +/// `StdVideoDecodeH265PictureInfo`'s `RefPicSetStCurrBefore`/`StCurrAfter`/ +/// `LtCurr` arrays name DPB slots, and every slot they name is one of `refs`' +/// ([`crate::pic_h265`]) — so dropping an entry leaves the hardware with a named +/// slot this op never bound, which it can only answer by guessing or failing. +/// Output that looks plausible and is wrong is the outcome this refusal exists to +/// prevent. +fn build_scope( + refs: &[crate::pic_h265::VkRefH265], + held_slots: impl Iterator, + setup_slot: u8, + setup_view: vk::ImageView, + setup_ref: hh::StdVideoDecodeH265ReferenceInfo, + slot_refs: &[Option], + view_of: impl Fn(u8) -> Option, +) -> Result<(Vec, usize), VkDecodeError> { + let mut scope: Vec = Vec::with_capacity(refs.len() + slot_refs.len() + 1); + for r in refs { + match view_of(r.slot) { + Some(view) => scope.push(ScopeEntry { + slot_index: i32::from(r.slot), + view, + std: r.std, + }), + None => return Err(VkDecodeError::UnboundReferenceSlot { slot: r.slot }), + } + } + let reference_count = scope.len(); + for slot in held_slots { + if slot == setup_slot || refs.iter().any(|r| r.slot == slot) { + continue; + } + match ( + slot_refs.get(usize::from(slot)).copied().flatten(), + view_of(slot), + ) { + (Some(std), Some(view)) => scope.push(ScopeEntry { + slot_index: i32::from(slot), + view, + std, + }), + // Unreachable in practice: every held slot was a setup slot once. + _ => trace!( + slot, + "held slot without reference info/binding — left unbound" + ), + } + } + scope.push(ScopeEntry { + slot_index: -1, + view: setup_view, + std: setup_ref, + }); + Ok((scope, reference_count)) +} + +/// Record one H.265 decode op into the chosen command buffer and submit it under +/// the queue lock: image waits per the pool contract, the dst image's timeline +/// signal at `signal_value`. +/// +/// # Safety +/// +/// Live device; `state` is the current session generation with `vk_plan` derived +/// against its `SlotMap`, `dst` a free pool image, the AU resident in `upload`'s +/// ring slot, and the command buffer's previous submission completed (caller +/// waited its mark). +#[allow(clippy::too_many_arguments)] +unsafe fn record_and_submit_h265( + dev: &DecodeDevice, + lock: &dyn QueueLock, + state: &mut SessionStateH265, + vk_plan: &DecodePlanVkH265, + slice_offsets: &[u32], + upload: &UploadedAu, + dst: usize, + cmd_index: usize, + query_index: u32, + waits: &[(vk::Semaphore, u64)], + signal_value: u64, +) -> Result<(), VkDecodeError> { + let device = dev.ash(); + let cmd = state.ops.cmds[cmd_index]; + let coded_extent = state.coded_extent; + let coincide = state.dpb.is_none(); + + // ---- the reference layout, decided BEFORE anything is recorded ---- + // `refs` order is the contract (build_scope docs): the Std picture info's RPS + // arrays index into this exact array, so a missing entry is fatal, not + // skippable — and it must fail before the command buffer is even begun. + let setup_view = if coincide { + state.pool.pictures[dst].view + } else { + state + .dpb + .as_ref() + .expect("distinct mode") + .dpb_view(vk_plan.setup_slot) + }; + let held_slots: Vec = state.slots.held().map(|(slot, _id)| slot).collect(); + let (scope, reference_count) = build_scope( + &vk_plan.refs, + held_slots.into_iter(), + vk_plan.setup_slot, + setup_view, + vk_plan.setup_ref, + &state.slot_refs, + |slot| slot_view(state, slot), + )?; + + let begin_info = + vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); + // SAFETY: the buffer's previous submission completed (fn contract) and its + // pool allows per-buffer reset, so begin implicitly resets it. + unsafe { + device + .begin_command_buffer(cmd, &begin_info) + .map_err(VkDecodeError::from)? + }; + + // ---- barriers (outside the video coding scope) ---- + // Prior reconstructions must be visible to this op's reference reads. + let memory_barriers = [vk::MemoryBarrier2::default() + .src_stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + .src_access_mask(vk::AccessFlags2::VIDEO_DECODE_WRITE_KHR) + .dst_stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + .dst_access_mask( + vk::AccessFlags2::VIDEO_DECODE_READ_KHR | vk::AccessFlags2::VIDEO_DECODE_WRITE_KHR, + )]; + // Decode targets are fully overwritten: discard via UNDEFINED with an + // execution+memory dependency on earlier ops that touched them. + let decode_layer_barrier = |image: vk::Image, layer: u32, new_layout: vk::ImageLayout| { + vk::ImageMemoryBarrier2::default() + .src_stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + .src_access_mask( + vk::AccessFlags2::VIDEO_DECODE_READ_KHR | vk::AccessFlags2::VIDEO_DECODE_WRITE_KHR, + ) + .dst_stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + .dst_access_mask( + vk::AccessFlags2::VIDEO_DECODE_READ_KHR | vk::AccessFlags2::VIDEO_DECODE_WRITE_KHR, + ) + .old_layout(vk::ImageLayout::UNDEFINED) + .new_layout(new_layout) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(image) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: layer, + layer_count: 1, + }) + }; + let dst_image = state.pool.pictures[dst].image; + let mut image_barriers = Vec::new(); + if coincide { + // The dst pool image IS the setup DPB picture. + image_barriers.push(decode_layer_barrier( + dst_image, + 0, + vk::ImageLayout::VIDEO_DECODE_DPB_KHR, + )); + } else { + let dpb = state.dpb.as_ref().expect("distinct mode"); + let (setup_image, setup_layer) = dpb.dpb_target(vk_plan.setup_slot); + image_barriers.push(decode_layer_barrier( + setup_image, + setup_layer, + vk::ImageLayout::VIDEO_DECODE_DPB_KHR, + )); + image_barriers.push(decode_layer_barrier( + dst_image, + 0, + vk::ImageLayout::VIDEO_DECODE_DST_KHR, + )); + } + let dependency = vk::DependencyInfo::default() + .memory_barriers(&memory_barriers) + .image_memory_barriers(&image_barriers); + // SAFETY: recording into the begun buffer; synchronization2 is enabled per + // the DeviceHandles feature contract. + unsafe { device.cmd_pipeline_barrier2(cmd, &dependency) }; + + // This op's status query slot, reset before the coding scope (encoder idiom). + // None on drivers without queryResultStatusSupport (RADV — recording a query + // there hangs the VCN; OpRing docs). NEVER remove this gate. + if let Some(query_pool) = state.ops.query_pool { + // SAFETY: recording; `query_index` is within the pool's count (fn contract). + unsafe { device.cmd_reset_query_pool(cmd, query_pool, query_index, 1) }; + } + + // ---- bound-slot staging ---- + // Staged arrays over the scope decided above: resources → std infos → codec + // slot infos → slot infos. Each vector is fully built before the next borrows + // it, so nothing reallocates under a stored pointer. + let resources: Vec> = scope + .iter() + .map(|entry| { + vk::VideoPictureResourceInfoKHR::default() + .coded_extent(coded_extent) + .base_array_layer(0) + .image_view_binding(entry.view) + }) + .collect(); + let std_refs: Vec = + scope.iter().map(|entry| entry.std).collect(); + let mut dpb_infos: Vec> = std_refs + .iter() + .map(|std| vk::VideoDecodeH265DpbSlotInfoKHR::default().std_reference_info(std)) + .collect(); + let mut begin_slots: Vec> = Vec::with_capacity(scope.len()); + for (index, entry) in scope.iter().enumerate() { + begin_slots.push( + vk::VideoReferenceSlotInfoKHR::default() + .slot_index(entry.slot_index) + .picture_resource(&resources[index]), + ); + } + for (slot_info, dpb_info) in begin_slots.iter_mut().zip(dpb_infos.iter_mut()) { + *slot_info = (*slot_info).push_next(dpb_info); + } + // The decode op's reference list: exactly this AU's references, in `refs` + // order — `RefPicSetStCurrBefore`/`StCurrAfter`/`LtCurr` index into THIS + // array (module docs), which is why the entries were built refs-first and why + // a missing binding failed the whole op above rather than compacting. + let decode_refs: Vec> = + begin_slots[..reference_count].to_vec(); + + // The setup slot as the decode op sees it: its REAL index (the begin list's + // twin entry carries -1), same resource, its own codec info chain. + let setup_std = vk_plan.setup_ref; + let mut setup_dpb = vk::VideoDecodeH265DpbSlotInfoKHR::default().std_reference_info(&setup_std); + let setup_resource = resources[scope.len() - 1]; + let setup_slot_info = vk::VideoReferenceSlotInfoKHR::default() + .slot_index(i32::from(vk_plan.setup_slot)) + .picture_resource(&setup_resource) + .push_next(&mut setup_dpb); + + // Decode destination: the setup picture itself (coincide) or the pool image + // (distinct). + let dst_resource = if coincide { + setup_resource + } else { + vk::VideoPictureResourceInfoKHR::default() + .coded_extent(coded_extent) + .base_array_layer(0) + .image_view_binding(state.pool.pictures[dst].view) + }; + + let std_pic = vk_plan.std_pic; + // Offsets rebased into the packed slices-only buffer (NOT the plan's + // AU-absolute offsets — the AU's non-slice NALUs were never uploaded). + let mut h265_pic = vk::VideoDecodeH265PictureInfoKHR::default() + .std_picture_info(&std_pic) + .slice_segment_offsets(slice_offsets); + let mut decode_info = vk::VideoDecodeInfoKHR::default() + .src_buffer(state.ring.buffer()) + .src_buffer_offset(upload.offset) + .src_buffer_range(upload.range) + .dst_picture_resource(dst_resource) + .setup_reference_slot(&setup_slot_info) + .push_next(&mut h265_pic); + if reference_count > 0 { + decode_info = decode_info.reference_slots(&decode_refs); + } + + let begin_coding = vk::VideoBeginCodingInfoKHR::default() + .video_session(state.session.session()) + .video_session_parameters(state.session.parameters()) + .reference_slots(&begin_slots); + // The one-shot session RESET, consumed HERE but re-armed on every error path + // below — a RESET recorded into a command buffer that never reaches the + // queue initialized nothing, and the next successful recording must carry it + // or the session runs its whole life uninitialized. + let did_reset = state.session.take_needs_reset(); + // SAFETY: recording into the begun buffer, through end_command_buffer; every + // pointed-to struct above is a local (or session-state field) that outlives + // the calls; the session/parameters handles are this generation's own. + let recorded: Result<(), vk::Result> = unsafe { + (dev.video_queue().fp().cmd_begin_video_coding_khr)(cmd, &begin_coding); + if did_reset { + // Session first-use initialization — ONCE, before its first decode. + let control = vk::VideoCodingControlInfoKHR::default() + .flags(vk::VideoCodingControlFlagsKHR::RESET); + (dev.video_queue().fp().cmd_control_video_coding_khr)(cmd, &control); + } + if let Some(query_pool) = state.ops.query_pool { + device.cmd_begin_query(cmd, query_pool, query_index, vk::QueryControlFlags::empty()); + } + (dev.video_decode_queue().fp().cmd_decode_video_khr)(cmd, &decode_info); + if let Some(query_pool) = state.ops.query_pool { + device.cmd_end_query(cmd, query_pool, query_index); + } + (dev.video_queue().fp().cmd_end_video_coding_khr)( + cmd, + &vk::VideoEndCodingInfoKHR::default(), + ); + device.end_command_buffer(cmd) + }; + if let Err(e) = recorded { + if did_reset { + state.session.re_arm_reset(); + } + return Err(VkDecodeError::from(e)); + } + + // ---- submit, under the caller's queue lock ---- + let cmd_infos = [vk::CommandBufferSubmitInfo::default().command_buffer(cmd)]; + let wait_infos: Vec> = waits + .iter() + .map(|&(semaphore, value)| { + vk::SemaphoreSubmitInfo::default() + .semaphore(semaphore) + .value(value) + .stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + }) + .collect(); + let signals = [vk::SemaphoreSubmitInfo::default() + .semaphore(state.pool.pictures[dst].semaphore) + .value(signal_value) + .stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS)]; + let submits = [vk::SubmitInfo2::default() + .command_buffer_infos(&cmd_infos) + .wait_semaphore_infos(&wait_infos) + .signal_semaphore_infos(&signals)]; + let guard = QueueSubmitGuard::acquire(lock); + // SAFETY: the decode queue is the device's own (DeviceHandles contract) and + // externally synchronized by the guard; the submit arrays are locals. + let result = unsafe { device.queue_submit2(dev.decode_queue(), &submits, vk::Fence::null()) }; + drop(guard); + if let Err(e) = result { + // The recorded RESET never executed: the next recording must redo it. + if did_reset { + state.session.re_arm_reset(); + } + return Err(VkDecodeError::from(e)); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use ash::vk::Handle as _; + + use super::*; + use crate::pic_h265::VkRefH265; + + /// A reference-info value carrying just the two fields the assertions read. + fn std_ref(poc: i32, long_term: bool) -> hh::StdVideoDecodeH265ReferenceInfo { + // SAFETY: StdVideoDecodeH265ReferenceInfo is a plain-C bindgen struct of a + // bitfield word and one integer; all-zero is valid for every field. + let mut std: hh::StdVideoDecodeH265ReferenceInfo = unsafe { std::mem::zeroed() }; + std.PicOrderCntVal = poc; + std.flags + .set_used_for_long_term_reference(u32::from(long_term)); + std + } + + fn vk_ref(slot: u8, poc: i32, long_term: bool) -> VkRefH265 { + VkRefH265 { + slot, + std: std_ref(poc, long_term), + id: u64::from(slot) + 100, + } + } + + /// A distinguishable fake view per slot (never dereferenced — the scope only + /// carries handles around). + fn fake_view(slot: u8) -> vk::ImageView { + vk::ImageView::from_raw(u64::from(slot) + 1) + } + + #[test] + fn the_scopes_leading_entries_are_the_refs_in_plan_order() { + // The plan's refs are NOT in slot order (they are in RPS set order: + // StCurrBefore, StCurrAfter, LtCurr) — and the Std index arrays point at + // positions in THAT order, so the scope must not sort or dedup them. + let refs = vec![ + vk_ref(5, 40, false), + vk_ref(1, 60, false), + vk_ref(3, 8, true), + ]; + let slot_refs = vec![Some(std_ref(0, false)); 8]; + let (scope, reference_count) = build_scope( + &refs, + [1u8, 3, 5, 7].into_iter(), + 2, + fake_view(2), + std_ref(50, false), + &slot_refs, + |slot| Some(fake_view(slot)), + ) + .unwrap(); + + assert_eq!(reference_count, 3, "exactly this AU's references lead"); + assert_eq!( + scope[..reference_count] + .iter() + .map(|e| e.slot_index) + .collect::>(), + vec![5, 1, 3], + "plan order, not slot order — the RPS index arrays depend on it" + ); + for (entry, r) in scope.iter().zip(&refs) { + assert_eq!(entry.view, fake_view(r.slot)); + assert_eq!(entry.std.PicOrderCntVal, r.std.PicOrderCntVal); + assert_eq!( + entry.std.flags.used_for_long_term_reference(), + r.std.flags.used_for_long_term_reference(), + "the long-term marking rides with the binding" + ); + } + + // Then the other still-held slot (7), then the setup ACTIVATION entry. + assert_eq!(scope[3].slot_index, 7); + let last = scope.last().unwrap(); + assert_eq!( + last.slot_index, -1, + "the setup slot binds its resource without a current association" + ); + assert_eq!(last.view, fake_view(2)); + assert_eq!(last.std.PicOrderCntVal, 50); + assert_eq!( + scope.len(), + 5, + "3 refs + 1 other held slot + the activation" + ); + } + + #[test] + fn a_reference_slot_without_a_bound_image_fails_the_whole_op() { + // Compacting past it would shift every later RefPicSetStCurr* index onto + // the wrong picture — plausible-looking, wrong output. Fail closed. + let refs = vec![vk_ref(4, 10, false), vk_ref(6, 20, false)]; + let slot_refs = vec![Some(std_ref(0, false)); 8]; + let err = build_scope( + &refs, + [4u8, 6].into_iter(), + 0, + fake_view(0), + std_ref(30, false), + &slot_refs, + |slot| (slot != 6).then(|| fake_view(slot)), + ) + .unwrap_err(); + assert!( + matches!(err, VkDecodeError::UnboundReferenceSlot { slot: 6 }), + "{err}" + ); + } + + #[test] + fn held_slots_are_bound_once_and_the_setup_slot_never_twice() { + // Slot 3 is BOTH a reference and still held; slot 2 is the setup slot and + // also held (the previous picture in it). Neither may appear twice: a + // duplicate slot index in one coding scope is invalid, and a second entry + // for a reference would also break the index arrays. + let refs = vec![vk_ref(3, 12, false)]; + let slot_refs = vec![Some(std_ref(99, false)); 8]; + let (scope, reference_count) = build_scope( + &refs, + [1u8, 2, 3].into_iter(), + 2, + fake_view(2), + std_ref(24, false), + &slot_refs, + |slot| Some(fake_view(slot)), + ) + .unwrap(); + assert_eq!(reference_count, 1); + let indices: Vec = scope.iter().map(|e| e.slot_index).collect(); + assert_eq!(indices, vec![3, 1, -1]); + assert_eq!( + indices.iter().filter(|&&i| i == 3).count(), + 1, + "a referenced slot is bound exactly once" + ); + assert!( + !indices.contains(&2), + "the setup slot is bound only as the -1 activation entry" + ); + } + + #[test] + fn a_held_slot_with_no_cached_reference_info_is_left_unbound_not_faked() { + // Only reachable if a slot was never a setup slot on this session; the + // scope drops it rather than binding zeroed reference info (which would + // claim POC 0, short-term, for a picture that is neither). + let refs: Vec = Vec::new(); + let mut slot_refs: Vec> = vec![None; 4]; + slot_refs[1] = Some(std_ref(7, false)); + let (scope, reference_count) = build_scope( + &refs, + [1u8, 3].into_iter(), + 0, + fake_view(0), + std_ref(9, false), + &slot_refs, + |slot| Some(fake_view(slot)), + ) + .unwrap(); + assert_eq!(reference_count, 0, "an IRAP references nothing"); + assert_eq!( + scope.iter().map(|e| e.slot_index).collect::>(), + vec![1, -1], + "slot 3 had no cached info and is simply not bound" + ); + } + + #[test] + fn a_post_mutation_failure_wedges_every_later_au_until_the_ledgers_are_reset() { + // The exact shape the recovery exists for. An AU planned, `plan_to_vk_h265` + // assigned it slot 2 in the SlotMap, the coincide binding sync cleared + // slot 2's image binding — and THEN the decode failed (pool exhausted, ring + // upload, submit, any of them). Nothing restores the state, so the planner + // and the SlotMap both believe the picture is resident while no image holds + // it. + let mut slots = SlotMap::new(3); + slots.assign(100).unwrap(); // slot 0, an older reference, image bound + slots.assign(200).unwrap(); // slot 1, another, image bound + slots.assign(300).unwrap(); // slot 2, THIS AU's setup — binding cleared + let mut slot_image: Vec> = vec![Some(7), Some(8), None, None]; + let mut slot_refs: Vec> = + vec![Some(std_ref(10, false)); 4]; + + // Every later AU that references slot 2 fails, forever: build_scope will + // not silently compact past an unbound reference (the RPS index arrays). + let err = build_scope( + &[vk_ref(2, 30, false)], + [0u8, 1, 2].into_iter(), + 0, + fake_view(0), + std_ref(40, false), + &slot_refs, + |slot| slot_image[usize::from(slot)].map(|_| fake_view(slot)), + ) + .unwrap_err(); + assert!( + matches!(err, VkDecodeError::UnboundReferenceSlot { slot: 2 }), + "{err}" + ); + + // The recovery: flush to the next IRAP and empty all three ledgers. + let unbound = reset_slot_bindings(&mut slots, &mut slot_image, &mut slot_refs); + assert_eq!( + unbound, + vec![7, 8], + "the pool images the stale bindings pinned go back on the free list" + ); + assert_eq!(slots.active(), 0, "no picture is DPB-resident any more"); + assert_eq!( + slots.capacity(), + 4, + "capacity survives — no session rebuild" + ); + assert!(slot_image.iter().all(Option::is_none)); + assert!( + slot_refs.iter().all(Option::is_none), + "cached reference info goes too, or build_scope could bind a slot the \ + planner no longer knows about" + ); + + // And the IRAP that follows plans against an empty DPB: it references + // nothing, takes the lowest slot, and its scope builds. + let setup_slot = slots.assign(400).unwrap(); + assert_eq!(setup_slot, 0, "the freed slots are assignable again"); + slot_image[usize::from(setup_slot)] = Some(9); + let (scope, reference_count) = build_scope( + &[], + slots.held().map(|(slot, _id)| slot), + setup_slot, + fake_view(setup_slot), + std_ref(0, false), + &slot_refs, + |slot| slot_image[usize::from(slot)].map(|_| fake_view(slot)), + ) + .unwrap(); + assert_eq!(reference_count, 0, "an IRAP references nothing"); + assert_eq!( + scope.iter().map(|e| e.slot_index).collect::>(), + vec![-1], + "only the setup activation entry — the stream is decoding again" + ); + } + + #[test] + fn the_recovery_latch_is_owed_once_and_consumed_by_exactly_one_decode() { + // Two failures in a row still owe ONE flush, and the decode that performs + // it clears the debt — otherwise every later decode would re-flush and the + // stream could never build a DPB again. + let mut latch = RecoveryLatch::default(); + assert!(!latch.is_latched(), "a fresh decoder owes nothing"); + assert!(!latch.take()); + + latch.latch(); + latch.latch(); + assert!( + latch.is_latched(), + "visible in debug_snapshot before it runs" + ); + assert!(latch.take(), "the next decode recovers"); + assert!(!latch.is_latched()); + assert!(!latch.take(), "and the one after that just decodes"); + } + + #[test] + fn the_picture_info_carries_the_rebased_offsets_and_the_h265_std_struct() { + // The submission-final wiring, without a device: `pSliceSegmentOffsets` + // must be the REBASED array (one entry per slice segment, counted by + // ash from the slice's length), and the picture info must point at the + // plan's own Std struct. + let mut au = vec![0xAAu8; 2000]; + for start in [40usize, 900, 1500] { + au[start..start + 3].copy_from_slice(&[0, 0, 1]); + } + let offsets = pack_slices(&au, &[40..900, 900..1500, 1500..2000]) + .unwrap() + .offsets; + assert_eq!(offsets, vec![0, 860, 1460]); + + // SAFETY: StdVideoDecodeH265PictureInfo is a plain-C bindgen struct of a + // bitfield word, integers and byte arrays; all-zero is valid. + let mut std_pic: hh::StdVideoDecodeH265PictureInfo = unsafe { std::mem::zeroed() }; + std_pic.PicOrderCntVal = 42; + let picture_info = vk::VideoDecodeH265PictureInfoKHR::default() + .std_picture_info(&std_pic) + .slice_segment_offsets(&offsets); + assert_eq!(picture_info.slice_segment_count, 3); + assert_eq!( + picture_info.s_type, + vk::StructureType::VIDEO_DECODE_H265_PICTURE_INFO_KHR + ); + // SAFETY: the two pointers were just taken from `offsets` and `std_pic`, + // both alive for this scope. + unsafe { + assert_eq!( + std::slice::from_raw_parts(picture_info.p_slice_segment_offsets, 3), + &offsets[..] + ); + assert_eq!((*picture_info.p_std_picture_info).PicOrderCntVal, 42); + } + + // And a DPB slot info chains the H.265 reference info, not the H.264 one. + let std = std_ref(17, true); + let dpb_info = vk::VideoDecodeH265DpbSlotInfoKHR::default().std_reference_info(&std); + assert_eq!( + dpb_info.s_type, + vk::StructureType::VIDEO_DECODE_H265_DPB_SLOT_INFO_KHR + ); + // SAFETY: the pointer was just taken from `std`, alive for this scope. + unsafe { + assert_eq!((*dpb_info.p_std_reference_info).PicOrderCntVal, 17); + assert_eq!( + (*dpb_info.p_std_reference_info) + .flags + .used_for_long_term_reference(), + 1 + ); + } + } +} diff --git a/crates/pf-vkdecode/src/device.rs b/crates/pf-vkdecode/src/device.rs new file mode 100644 index 00000000..ce5bddf8 --- /dev/null +++ b/crates/pf-vkdecode/src/device.rs @@ -0,0 +1,577 @@ +//! Borrowed-device wrap: the presenter's live Vulkan handles loaded into ash +//! function tables, plus the queue-lock contract every queue submission runs under. +//! +//! Ownership: everything in [`DeviceHandles`] is BORROWED. This crate never creates +//! and never destroys the instance/device — [`DecodeDevice`]'s ash wrappers are +//! function tables over foreign handles, and dropping them destroys nothing. The +//! objects this crate does create (sessions, images, buffers, pools) are destroyed +//! by their owning structs' `Drop` impls, all of which must run before the borrowed +//! device dies — the same liveness contract FFmpeg's decoder had over the identical +//! handle bundle (`pf-client-core`'s `VulkanDecodeDevice`), now written down. + +use ash::vk; +use ash::vk::Handle; + +/// The borrowed handles of the presenter's decode-capable device, as raw integers so +/// the type stays FFI-plain (mirrors `pf-client-core`'s `VulkanDecodeDevice`, which +/// adapts into this in WP-C — pf-vkdecode deliberately does not depend on it). +/// +/// Caller contract (checked where cheap, otherwise trusted): +/// - All four handles are live, and stay live for the lifetime of every object this +/// crate builds from them (the presenter outlives every session pump). +/// - The instance/device were created with the Vulkan Video decode stack enabled: +/// `VK_KHR_video_queue`, `VK_KHR_video_decode_queue`, and the per-codec extension +/// of every decoder that will be built on the bundle +/// (`VK_KHR_video_decode_h264` for [`crate::VkH264Decoder`], +/// `VK_KHR_video_decode_h265` for [`crate::VkH265Decoder`]), plus the +/// `synchronization2` and `timelineSemaphore` features (the presenter's device +/// meets all of this when it advertises `video_decode`). +/// - `decode_qf`/`decode_queue_index` name a queue with `VIDEO_DECODE_KHR` ops. +/// Which CODEC operations that family advertises is not trusted but READ +/// ([`DecodeDevice::decode_codec_ops`]) and each decoder refuses up front when +/// its own is missing — a physical-device caps query answers for hardware +/// regardless of which extensions the device was created with, so this is the +/// only thing standing between a wrong bundle and `vkCreateVideoSessionKHR` on +/// an unenabled codec. `graphics_qf` is the family the presenter samples on +/// (image sharing crosses the two when they differ). +#[derive(Debug, Clone)] +pub struct DeviceHandles { + /// `PFN_vkGetInstanceProcAddr` from the loader; everything else is resolved + /// through it. + pub get_instance_proc_addr: usize, + pub instance: usize, + pub physical_device: usize, + pub device: usize, + /// The video-decode queue family. + pub decode_qf: u32, + /// Queue index within `decode_qf` this decoder submits on. + pub decode_queue_index: u32, + /// The presenter's graphics+present family (the other side of image sharing). + pub graphics_qf: u32, +} + +/// External synchronization for `vkQueueSubmit`: the caller supplies the lock that +/// serializes EVERY submit on the shared device — in WP-C that is pf-client-core's +/// `QueueLock`, the same object the presenter holds around its own submits/presents +/// (the 2026-07-09 `VK_ERROR_DEVICE_LOST` race is why this is a first-class contract +/// and not an afterthought). Tests use [`NoopQueueLock`]. +/// +/// `lock` blocks until the queue is free and takes it; `unlock` releases it. Use +/// [`QueueSubmitGuard`] rather than calling the pair by hand. +pub trait QueueLock { + fn lock(&self); + fn unlock(&self); +} + +/// A [`QueueLock`] that guards nothing — for tests and for callers whose decode +/// queue is provably not shared with any other submitter. +#[derive(Debug, Default)] +pub struct NoopQueueLock; + +impl QueueLock for NoopQueueLock { + fn lock(&self) {} + fn unlock(&self) {} +} + +/// RAII scope over a [`QueueLock`]: acquired for exactly the duration of a queue +/// submission, released on drop (including unwinds — though this crate's own paths +/// never panic while holding it). +pub struct QueueSubmitGuard<'a> { + lock: &'a dyn QueueLock, +} + +impl<'a> QueueSubmitGuard<'a> { + /// Take the queue (blocking until free). + pub fn acquire(lock: &'a dyn QueueLock) -> Self { + lock.lock(); + Self { lock } + } +} + +impl Drop for QueueSubmitGuard<'_> { + fn drop(&mut self) { + self.lock.unlock(); + } +} + +/// A [`DeviceHandles`] bundle that cannot host the decoder being built. Caller +/// bugs (a half-filled bundle) and device gaps (a decode family that does not run +/// this codec) — never stream conditions. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeviceError { + /// One of the four raw handles is zero/null. + NullHandle(&'static str), + /// The decode queue family advertises no decode operation for the codec the + /// decoder needs — either the device was created without that codec's + /// extension, or `decode_qf` names the wrong family. Refusing here is what + /// keeps `vkCreateVideoSessionKHR` from being called with a profile the + /// device never enabled (the caps query alone would not catch it: it asks the + /// PHYSICAL device, which answers for the hardware). + NoCodecOperation { + family: u32, + /// The codec, spelled the way the caller would recognize it + /// (`"H.264 decode"` / `"H.265 decode"`). + wanted: &'static str, + }, +} + +/// A device allocation that cannot proceed. Wraps the raw Vulkan failure OR the +/// memory-type miss that used to be silently papered over with index 0 — a wrong +/// type index is at best an immediate validation error and at worst a mapping of +/// the wrong heap, so a miss is an ERROR here, never a fallback. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum AllocError { + Vk(vk::Result), + /// No memory type satisfies (`type_bits`, `flags`) on this device. + NoMemoryType { + type_bits: u32, + flags: vk::MemoryPropertyFlags, + }, +} + +impl From for AllocError { + fn from(r: vk::Result) -> Self { + AllocError::Vk(r) + } +} + +/// First memory type matching `bits` and `want` — an [`AllocError::NoMemoryType`] +/// when none does (the encoder's `find_mem` falls back to 0 there; here the miss +/// surfaces). +pub(crate) fn find_memory_type( + props: &vk::PhysicalDeviceMemoryProperties, + bits: u32, + want: vk::MemoryPropertyFlags, +) -> Result { + for i in 0..props.memory_type_count { + if (bits & (1 << i)) != 0 && props.memory_types[i as usize].property_flags.contains(want) { + return Ok(i); + } + } + Err(AllocError::NoMemoryType { + type_bits: bits, + flags: want, + }) +} + +/// First memory type matching `bits` that also carries `prefer`; when none does, +/// the first type matching `bits` at all. A driver constrains `memoryTypeBits` to +/// where the allocation can legally live — NVIDIA (610.88) reports some video- +/// session bindings host-visible-ONLY, which is spec-legal, so a hard `prefer` +/// requirement there is unsatisfiable by construction. Still an +/// [`AllocError::NoMemoryType`] when `bits` selects nothing whatsoever (that +/// miss-is-error contract stays; only the property preference softens). Mapped +/// staging paths (the bitstream ring) must NOT use this: they require +/// `HOST_VISIBLE|HOST_COHERENT` as a hard property, not a preference. +pub(crate) fn find_memory_type_preferring( + props: &vk::PhysicalDeviceMemoryProperties, + bits: u32, + prefer: vk::MemoryPropertyFlags, +) -> Result { + match find_memory_type(props, bits, prefer) { + Ok(index) => Ok(index), + Err(_) => find_memory_type(props, bits, vk::MemoryPropertyFlags::empty()), + } +} + +impl std::fmt::Display for DeviceError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DeviceError::NullHandle(which) => { + write!(f, "DeviceHandles.{which} is null — a half-filled bundle") + } + DeviceError::NoCodecOperation { family, wanted } => { + write!( + f, + "decode queue family {family} advertises no {wanted} operation \ + (extension not enabled on the device, or the wrong family)" + ) + } + } + } +} + +impl std::error::Error for DeviceError {} + +/// The borrowed device with ash function tables loaded: the object every other +/// module in this crate makes its Vulkan calls through. +/// +/// Clone is cheap-ish (ash tables are plain structs of function pointers) and safe: +/// clones share the same borrowed handles under the same liveness contract. +#[derive(Clone)] +pub struct DecodeDevice { + instance: ash::Instance, + device: ash::Device, + physical_device: vk::PhysicalDevice, + video_queue_instance: ash::khr::video_queue::Instance, + video_queue: ash::khr::video_queue::Device, + video_decode_queue: ash::khr::video_decode_queue::Device, + decode_queue: vk::Queue, + decode_qf: u32, + graphics_qf: u32, + /// The decode family advertises `queryResultStatusSupport`: per-op + /// RESULT_STATUS queries are legal in its video coding scopes. FALSE on RADV + /// (2026-08, .25: recording one anyway hangs the VCN ring) — the decoder + /// must skip queries entirely there and fall back to timeline-completion + /// verdicts. + result_status_queries: bool, + /// `VkQueueFamilyVideoPropertiesKHR::videoCodecOperations` of the decode + /// family — which codecs this queue can actually run decode ops for. + /// + /// Read from the SAME `vkGetPhysicalDeviceQueueFamilyProperties2` call as the + /// status-query support (one extra chained struct, no extra round trip). It + /// is the only honest answer to "may I create a DECODE_H265 session here?": + /// `vkGetPhysicalDeviceVideoCapabilitiesKHR` is a PHYSICAL-device query and + /// succeeds on capable hardware whether or not the VkDevice was created with + /// `VK_KHR_video_decode_h265` enabled, so caps derivation alone would happily + /// lead into `vkCreateVideoSessionKHR` on an unenabled codec — undefined + /// behaviour instead of a clean demote to the next decoder rung. + /// + /// Empty (no bits) is treated as "this family decodes nothing" and refuses. + /// That is safe to rely on because every driver hosting Vulkan Video fills + /// this struct — it is how applications pick a decode queue in the first + /// place (FFmpeg's own `vulkan_video.c` selects its family by exactly this + /// field, on the very drivers the shipping FFmpeg-Vulkan rung runs on). + decode_codec_ops: vk::VideoCodecOperationFlagsKHR, +} + +impl DecodeDevice { + /// Load ash function tables over the borrowed handles. + /// + /// # Safety + /// + /// The full [`DeviceHandles`] caller contract: live handles (outliving `self` + /// and everything created through it), the video-decode extensions/features + /// enabled at creation, and truthful queue-family fields. Null handles are + /// rejected here; everything else cannot be checked and is trusted. + pub unsafe fn wrap(handles: &DeviceHandles) -> Result { + if handles.get_instance_proc_addr == 0 { + return Err(DeviceError::NullHandle("get_instance_proc_addr")); + } + if handles.instance == 0 { + return Err(DeviceError::NullHandle("instance")); + } + if handles.physical_device == 0 { + return Err(DeviceError::NullHandle("physical_device")); + } + if handles.device == 0 { + return Err(DeviceError::NullHandle("device")); + } + + // SAFETY: the usize is non-zero (checked above) and the caller contract says + // it is the loader's PFN_vkGetInstanceProcAddr; fn pointers and usize share + // size/ABI on every supported target. + let gipa: vk::PFN_vkGetInstanceProcAddr = unsafe { + std::mem::transmute::( + handles.get_instance_proc_addr, + ) + }; + // SAFETY: `gipa` is a valid Vulkan-1.0-conformant loader entry point per the + // caller contract, valid for the returned Entry's lifetime (handle liveness). + let entry = unsafe { + ash::Entry::from_static_fn(ash::StaticFn { + get_instance_proc_addr: gipa, + }) + }; + // SAFETY: `handles.instance` is a live VkInstance created through this very + // loader (caller contract), so loading instance-level functions against it + // is exactly the ash::Instance::load contract. + let instance = unsafe { + ash::Instance::load( + entry.static_fn(), + vk::Instance::from_raw(handles.instance as u64), + ) + }; + // SAFETY: `handles.device` is a live VkDevice of that instance (caller + // contract) — the ash::Device::load contract. + let device = unsafe { + ash::Device::load( + instance.fp_v1_0(), + vk::Device::from_raw(handles.device as u64), + ) + }; + let video_queue_instance = ash::khr::video_queue::Instance::new(&entry, &instance); + let video_queue = ash::khr::video_queue::Device::new(&instance, &device); + let video_decode_queue = ash::khr::video_decode_queue::Device::new(&instance, &device); + // SAFETY: the caller contract guarantees `decode_qf`/`decode_queue_index` + // name a queue the device was created with. + let decode_queue = + unsafe { device.get_device_queue(handles.decode_qf, handles.decode_queue_index) }; + + // The two per-family video facts, from ONE query: whether RESULT_STATUS + // queries are legal here, and which codec operations this family can run + // (struct field docs for both). An out-of-range decode family — a bundle + // naming a queue this physical device does not have — answers "no" to + // both, and the codec check then refuses the decoder outright. + let physical_device = vk::PhysicalDevice::from_raw(handles.physical_device as u64); + // SAFETY: live physical device (caller contract); the two-call form fills + // the chained per-family structs. + let family_count = + unsafe { instance.get_physical_device_queue_family_properties2_len(physical_device) }; + let (result_status_queries, decode_codec_ops) = if (handles.decode_qf as usize) + < family_count + { + let mut status_props = + vec![vk::QueueFamilyQueryResultStatusPropertiesKHR::default(); family_count]; + let mut video_props = vec![vk::QueueFamilyVideoPropertiesKHR::default(); family_count]; + let mut families: Vec> = status_props + .iter_mut() + .zip(video_props.iter_mut()) + .map(|(status, video)| { + vk::QueueFamilyProperties2::default() + .push_next(status) + .push_next(video) + }) + .collect(); + // SAFETY: as above, arrays sized to the reported count. + unsafe { + instance + .get_physical_device_queue_family_properties2(physical_device, &mut families) + }; + drop(families); + let family = handles.decode_qf as usize; + ( + status_props[family].query_result_status_support != vk::FALSE, + video_props[family].video_codec_operations, + ) + } else { + (false, vk::VideoCodecOperationFlagsKHR::NONE) + }; + + // `entry` is only the ladder the tables above were loaded through; nothing + // needs it afterwards (ash tables own their function pointers). + drop(entry); + + Ok(Self { + instance, + device, + physical_device, + video_queue_instance, + video_queue, + video_decode_queue, + decode_queue, + decode_qf: handles.decode_qf, + graphics_qf: handles.graphics_qf, + result_status_queries, + decode_codec_ops, + }) + } + + pub(crate) fn ash(&self) -> &ash::Device { + &self.device + } + + /// Whether the decode family supports per-op RESULT_STATUS queries (struct + /// field docs — FALSE on RADV, where recording one hangs the VCN). + pub(crate) fn result_status_queries(&self) -> bool { + self.result_status_queries + } + + /// The codec operations the decode family advertises (struct field docs). + pub fn decode_codec_ops(&self) -> vk::VideoCodecOperationFlagsKHR { + self.decode_codec_ops + } + + /// Refuse unless the decode family advertises `op`. + /// + /// The decoders' first act, before any caps query: a physical-device caps + /// query answers for the HARDWARE and would happily green-light a codec the + /// VkDevice never enabled the extension for, at which point + /// `vkCreateVideoSessionKHR` is undefined behaviour. This turns that into the + /// ladder's clean, named demote. + pub(crate) fn require_codec_op( + &self, + op: vk::VideoCodecOperationFlagsKHR, + what: &'static str, + ) -> Result<(), DeviceError> { + if self.decode_codec_ops.contains(op) { + Ok(()) + } else { + Err(DeviceError::NoCodecOperation { + family: self.decode_qf, + wanted: what, + }) + } + } + + pub(crate) fn physical_device(&self) -> vk::PhysicalDevice { + self.physical_device + } + + pub(crate) fn video_queue_instance(&self) -> &ash::khr::video_queue::Instance { + &self.video_queue_instance + } + + pub(crate) fn video_queue(&self) -> &ash::khr::video_queue::Device { + &self.video_queue + } + + pub(crate) fn video_decode_queue(&self) -> &ash::khr::video_decode_queue::Device { + &self.video_decode_queue + } + + pub(crate) fn decode_queue(&self) -> vk::Queue { + self.decode_queue + } + + pub(crate) fn decode_qf(&self) -> u32 { + self.decode_qf + } + + /// The queue families image sharing spans: empty (EXCLUSIVE) when decode and + /// graphics are one family, both otherwise (CONCURRENT — the presenter samples + /// decode output on its own family and per-frame ownership transfers would buy + /// latency for nothing at punktfunk's frame rates). + pub(crate) fn sharing_families(&self) -> Vec { + if self.decode_qf == self.graphics_qf { + Vec::new() + } else { + vec![self.decode_qf, self.graphics_qf] + } + } + + /// The device's memory properties (queried fresh; cheap and stateless). + pub(crate) fn memory_properties(&self) -> vk::PhysicalDeviceMemoryProperties { + // SAFETY: `physical_device` is live per the DeviceHandles contract; the call + // fills a plain struct and touches nothing else. + unsafe { + self.instance + .get_physical_device_memory_properties(self.physical_device) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_half_filled_bundle_is_rejected_before_any_ffi() { + let mut handles = DeviceHandles { + get_instance_proc_addr: 0, + instance: 1, + physical_device: 1, + device: 1, + decode_qf: 0, + decode_queue_index: 0, + graphics_qf: 0, + }; + // SAFETY: wrap rejects the null handle before making any Vulkan call, so no + // part of the liveness contract is exercised. (`Err` matched by hand: the + // Ok side holds ash tables, which carry no Debug for unwrap_err.) + let result = unsafe { DecodeDevice::wrap(&handles) }; + let Err(err) = result else { + panic!("a null gipa must be rejected") + }; + assert_eq!(err, DeviceError::NullHandle("get_instance_proc_addr")); + + handles.get_instance_proc_addr = 1; + handles.device = 0; + // SAFETY: as above — the null device handle is rejected before any FFI. + let result = unsafe { DecodeDevice::wrap(&handles) }; + let Err(err) = result else { + panic!("a null device must be rejected") + }; + assert_eq!(err, DeviceError::NullHandle("device")); + } + + #[test] + fn a_memory_type_miss_is_an_error_never_a_fallback_to_index_zero() { + let mut props = vk::PhysicalDeviceMemoryProperties { + memory_type_count: 2, + ..Default::default() + }; + props.memory_types[0].property_flags = vk::MemoryPropertyFlags::DEVICE_LOCAL; + props.memory_types[1].property_flags = + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT; + + // A hit resolves to the matching index, not the first. + assert_eq!( + find_memory_type( + &props, + 0b11, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT + ), + Ok(1) + ); + // A type excluded by the requirement bits does not count as a hit. + assert_eq!( + find_memory_type(&props, 0b01, vk::MemoryPropertyFlags::HOST_VISIBLE), + Err(AllocError::NoMemoryType { + type_bits: 0b01, + flags: vk::MemoryPropertyFlags::HOST_VISIBLE + }) + ); + // Flags nothing advertises: an error carrying the miss, never index 0. + assert_eq!( + find_memory_type(&props, 0b11, vk::MemoryPropertyFlags::PROTECTED), + Err(AllocError::NoMemoryType { + type_bits: 0b11, + flags: vk::MemoryPropertyFlags::PROTECTED + }) + ); + } + + #[test] + fn preferring_picks_the_preferred_type_and_falls_back_inside_the_bits() { + let mut props = vk::PhysicalDeviceMemoryProperties { + memory_type_count: 4, + ..Default::default() + }; + props.memory_types[0].property_flags = + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT; + props.memory_types[1].property_flags = vk::MemoryPropertyFlags::DEVICE_LOCAL; + props.memory_types[2].property_flags = vk::MemoryPropertyFlags::DEVICE_LOCAL; + props.memory_types[3].property_flags = + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT; + + // The preferred property wins over a lower-indexed non-preferred type. + assert_eq!( + find_memory_type_preferring(&props, 0b0011, vk::MemoryPropertyFlags::DEVICE_LOCAL), + Ok(1) + ); + // The NVIDIA session-binding shape: `memoryTypeBits` names only a + // host-visible type — honor the bits instead of erroring. + assert_eq!( + find_memory_type_preferring(&props, 0b1000, vk::MemoryPropertyFlags::DEVICE_LOCAL), + Ok(3) + ); + // Bits selecting nothing remain a hard miss, never index 0. + assert_eq!( + find_memory_type_preferring(&props, 0b0000, vk::MemoryPropertyFlags::DEVICE_LOCAL), + Err(AllocError::NoMemoryType { + type_bits: 0b0000, + flags: vk::MemoryPropertyFlags::empty() + }) + ); + } + + #[test] + fn the_queue_submit_guard_brackets_the_lock() { + use std::sync::atomic::AtomicI32; + use std::sync::atomic::Ordering; + + #[derive(Default)] + struct CountingLock { + depth: AtomicI32, + peak: AtomicI32, + } + impl QueueLock for CountingLock { + fn lock(&self) { + let d = self.depth.fetch_add(1, Ordering::SeqCst) + 1; + self.peak.fetch_max(d, Ordering::SeqCst); + } + fn unlock(&self) { + self.depth.fetch_sub(1, Ordering::SeqCst); + } + } + + let lock = CountingLock::default(); + { + let _guard = QueueSubmitGuard::acquire(&lock); + assert_eq!(lock.depth.load(Ordering::SeqCst), 1); + } + assert_eq!(lock.depth.load(Ordering::SeqCst), 0, "released on drop"); + assert_eq!(lock.peak.load(Ordering::SeqCst), 1); + } +} diff --git a/crates/pf-vkdecode/src/fault.rs b/crates/pf-vkdecode/src/fault.rs new file mode 100644 index 00000000..6e46eb00 --- /dev/null +++ b/crates/pf-vkdecode/src/fault.rs @@ -0,0 +1,305 @@ +//! Deliberate decoder-input corruption — the fault injector (M4 of the +//! native-decode program). +//! +//! # Why a first-class tool +//! +//! The whole program started from a field corruption that was ARCHITECTURALLY +//! undetectable: FFmpeg's Vulkan decoder creates no status queries +//! (`nb_queries = 0`), never sets `AV_FRAME_FLAG_CORRUPT`, and reports trouble only +//! as `av_log` lines. Nobody could tell a healthy stream from a broken one without +//! looking at the screen. The native decoder now has the signals — plan warnings +//! from pf-bitstream and per-op `RESULT_STATUS` verdicts from the driver — but a +//! detector nobody can fire is exactly as trustworthy as no detector at all. This +//! is the trigger: a deterministic way to break decoder input on purpose, so +//! detection can be PROVEN rather than assumed, on a lab box or in CI. +//! +//! It is inert unless explicitly armed ([`AuFault::from_spec`] returns `None` for +//! an unset/unparsable spec) and it is pure — no I/O, no clock, no randomness — so +//! a reported fault is reproducible from the spec string alone. +//! +//! # The three modes, and which detector each one fires +//! +//! They are not variations on one idea. The native lane has TWO independent +//! detectors — pf-bitstream's planner, which reads syntax, and the driver's per-op +//! `RESULT_STATUS` query, which reads the decode itself — and the modes exist to +//! fire them separately, because a harness that can only trip one of them proves +//! only half the lane: +//! +//! * [`FaultMode::Drop`] — the AU never reaches the decoder. The NEXT AU then +//! references a picture that was never decoded, so the planner reports +//! `FrameNumGap`/`MissingReference`: **parser-visible** damage, caught before a +//! single macroblock is decoded. The everyday network-loss shape, and the one +//! mode whose detection is provable without a GPU. +//! * [`FaultMode::Truncate`] — the AU arrives short. Worth knowing, and initially +//! surprising: this is **NOT** parser-visible. Annex-B carries no NALU length, so +//! a slice cut at a byte boundary is simply a shorter slice — its header parses, +//! the picture plans, every later reference resolves against a DPB entry that +//! exists. (pf-bitstream's `TruncatedAu` warning is a narrower thing: a NALU +//! whose HEADER is malformed with real data still behind it.) What the hardware +//! gets is a slice whose bitstream ends mid-picture, which is a decode error it +//! can report — so this is the mode that fires the DRIVER's detector +//! deterministically. +//! * [`FaultMode::Flip`] — one byte deep inside the slice payload is altered. The +//! bitstream still parses, every reference still resolves, the planner has +//! nothing to say — and the picture decodes WRONG, possibly without the driver +//! minding either (an entropy decoder happily decodes garbage into macroblocks). +//! This is precisely the Xbox Ally X class: corruption that reaches the screen +//! with nothing in the pipeline objecting. It is the mode that shows what +//! `RESULT_STATUS` can and cannot promise. +//! +//! The consequence worth stating plainly, because it is the program's whole thesis: +//! two of the three modes are invisible to every FFmpeg rung by construction +//! (`nb_queries = 0`, no `AV_FRAME_FLAG_CORRUPT`), and invisible to the native lane +//! too on a driver without `queryResultStatusSupport` (RADV). A session that cannot +//! answer the status query is not a clean session; it is an unmeasured one, and the +//! telemetry says so rather than reporting zeros. +//! +//! # Invocation +//! +//! `PUNKTFUNK_AU_FAULT=[:]` on any desktop client — +//! `drop`, `truncate`, `flip`, default period 60 (once a second at 60 fps): +//! +//! ```text +//! PUNKTFUNK_AU_FAULT=drop:120 PUNKTFUNK_DECODER=native-vulkan punktfunk-session --connect host +//! ``` +//! +//! Every `period`-th AU is faulted, counting from the first one the decoder is +//! offered, so the parameter sets and opening IDR of a session ride through +//! untouched at any period above 1. +//! +//! # What the injector is NOT in the same lane as +//! +//! `PUNKTFUNK_AU_DUMP` (the client's `au_dump` fixture capture) writes the AU as +//! it arrives from the wire, and this injector runs LATER — at the native +//! backend's decode entry, the last point before pf-bitstream. So on a faulted +//! run the dumped fixture is the CLEAN bitstream, not the one the decoder saw; +//! replaying it will not reproduce the damage. That ordering is deliberate: the +//! dump is what the HOST sent (the artefact a host-side bug is diagnosed from), +//! and moving the injector above it would corrupt every backend's input rather +//! than only the lane whose detectors it exists to fire. To capture the damaged +//! bytes, reconstruct them from the spec — the injector is pure and deterministic, +//! which is precisely what makes that possible. + +/// What to do to a faulted access unit. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FaultMode { + /// Swallow the AU entirely — the decoder never sees it (network loss). + Drop, + /// Deliver a prefix of the AU: a picture whose slice data stops mid-frame. + Truncate, + /// Deliver the whole AU with one payload byte altered (in-picture corruption + /// no parser can see). + Flip, +} + +/// What the caller must do with the access unit it was about to decode. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FaultAction { + /// Untouched — feed the original bytes. The answer for every AU but one in + /// `period`, and for every AU of a session with no fault armed. + Pass, + /// Do not feed this AU at all. + Drop, + /// Feed these bytes instead. Owned because corruption is by definition not a + /// borrow of the input; allocated only on the faulted AU, never on the + /// streaming path. + Corrupt(Vec), +} + +/// The default fault period: one AU per second at 60 fps — frequent enough to see +/// within a few seconds of streaming, rare enough that recovery completes between +/// faults instead of the stream never leaving its post-loss freeze. +pub const DEFAULT_FAULT_PERIOD: u32 = 60; + +/// Where in a faulted AU the damage lands, as a fraction of its length. Deep +/// enough to be past the parameter sets and the first slice header (so `Flip` +/// really is invisible to the parser and `Truncate` really does cut mid-picture +/// rather than refusing the AU at byte 0), and expressed as a fraction so it holds +/// for a 700-byte P-frame and a 4 MB IDR alike. +const FAULT_POINT_NUMERATOR: usize = 3; +const FAULT_POINT_DENOMINATOR: usize = 4; + +/// The armed injector: a mode, a period, and the count of AUs offered so far. +#[derive(Debug, Clone, Copy)] +pub struct AuFault { + mode: FaultMode, + period: u32, + seen: u32, +} + +impl AuFault { + /// Parse a `PUNKTFUNK_AU_FAULT` spec: `[:]`. `None` for anything + /// unrecognized, which is what keeps the injector inert — a typo must leave a + /// user's stream alone rather than half-arm it. + pub fn from_spec(spec: &str) -> Option { + let spec = spec.trim(); + let (mode, period) = match spec.split_once(':') { + Some((m, p)) => (m.trim(), p.trim().parse::().ok()?), + None => (spec, DEFAULT_FAULT_PERIOD), + }; + // A zero period would fault EVERY AU including the opening IDR, which + // never produces a stream to damage in the first place. + if period == 0 { + return None; + } + let mode = match mode { + "drop" => FaultMode::Drop, + "truncate" => FaultMode::Truncate, + "flip" => FaultMode::Flip, + _ => return None, + }; + Some(AuFault { + mode, + period, + seen: 0, + }) + } + + /// Build one directly (tests and callers that resolve the spec themselves). + pub fn new(mode: FaultMode, period: u32) -> AuFault { + AuFault { + mode, + period: period.max(1), + seen: 0, + } + } + + pub fn mode(&self) -> FaultMode { + self.mode + } + + pub fn period(&self) -> u32 { + self.period + } + + /// Offer one access unit. Returns what the caller should feed the decoder. + /// + /// The counter advances on EVERY call, faulted or not, so the cadence is a + /// property of the stream rather than of the damage: `period` AUs of clean + /// stream, one fault, repeat. + pub fn apply(&mut self, au: &[u8]) -> FaultAction { + self.seen = self.seen.wrapping_add(1); + if self.seen % self.period != 0 { + return FaultAction::Pass; + } + // Too short to damage meaningfully — a handful of bytes is a parameter-set + // AU or a fragment, and cutting/flipping inside one tests the parser's + // error handling rather than the decoder's integrity signals. Pass it and + // let the next multiple carry the fault. + if au.len() < 16 { + return FaultAction::Pass; + } + let point = au.len() * FAULT_POINT_NUMERATOR / FAULT_POINT_DENOMINATOR; + match self.mode { + FaultMode::Drop => FaultAction::Drop, + FaultMode::Truncate => FaultAction::Corrupt(au[..point].to_vec()), + FaultMode::Flip => { + let mut bytes = au.to_vec(); + // XOR with a single high-ish bit rather than inverting the byte: + // it moves the sample values the entropy coder decodes without + // being especially likely to manufacture a `00 00 01` start code + // out of the surrounding bytes (which would turn a payload + // corruption into a framing corruption and quietly change which + // detector the mode is testing). + bytes[point] ^= 0x40; + FaultAction::Corrupt(bytes) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The knob is a support tool: it has to be exactly as inert as it looks when + /// unset or mistyped, because the alternative is a user's stream quietly + /// breaking on a typo'd environment variable. + #[test] + fn only_a_well_formed_spec_arms_the_injector() { + assert_eq!( + AuFault::from_spec("drop").map(|f| (f.mode(), f.period())), + Some((FaultMode::Drop, DEFAULT_FAULT_PERIOD)) + ); + assert_eq!( + AuFault::from_spec("truncate:5").map(|f| (f.mode(), f.period())), + Some((FaultMode::Truncate, 5)) + ); + assert_eq!( + AuFault::from_spec(" flip : 30 ").map(|f| (f.mode(), f.period())), + Some((FaultMode::Flip, 30)) + ); + for bad in [ + "", "1", "off", "drop:", "drop:0", "drop:x", "corrupt", "flip:-1", ":30", + ] { + assert!(AuFault::from_spec(bad).is_none(), "{bad:?} must not arm"); + } + } + + /// The cadence: `period - 1` clean AUs, then one faulted, forever. The count + /// starts at the first AU offered, so a period above 1 never touches the + /// session's opening parameter sets and IDR. + #[test] + fn every_nth_au_is_faulted_and_the_rest_pass_through_untouched() { + let au = vec![0xA5u8; 64]; + let mut f = AuFault::new(FaultMode::Drop, 3); + assert_eq!(f.apply(&au), FaultAction::Pass); + assert_eq!(f.apply(&au), FaultAction::Pass); + assert_eq!(f.apply(&au), FaultAction::Drop); + assert_eq!(f.apply(&au), FaultAction::Pass); + assert_eq!(f.apply(&au), FaultAction::Pass); + assert_eq!(f.apply(&au), FaultAction::Drop); + } + + /// Truncation delivers a real prefix — the shape a lost tail shard has, not a + /// zero-length AU (which is a different, uninteresting failure). + #[test] + fn truncation_delivers_a_prefix_of_the_original() { + let au: Vec = (0..100u8).collect(); + let mut f = AuFault::new(FaultMode::Truncate, 1); + let FaultAction::Corrupt(short) = f.apply(&au) else { + panic!("truncate must corrupt"); + }; + assert_eq!(short.len(), 75, "three quarters of the AU survive"); + assert_eq!(short[..], au[..75], "and they are the ORIGINAL bytes"); + } + + /// A flip alters exactly one byte, deep in the payload, deterministically — + /// the corruption a parser cannot see. Determinism is what makes a field + /// report reproducible from the spec string alone. + #[test] + fn a_flip_changes_exactly_one_deep_payload_byte_and_is_reproducible() { + let au: Vec = (0..=255u8).collect(); + let run = || { + let mut f = AuFault::new(FaultMode::Flip, 1); + match f.apply(&au) { + FaultAction::Corrupt(bytes) => bytes, + other => panic!("flip must corrupt, got {other:?}"), + } + }; + let bytes = run(); + assert_eq!( + bytes.len(), + au.len(), + "length is untouched — this is not a cut" + ); + let differing: Vec = (0..au.len()).filter(|&i| bytes[i] != au[i]).collect(); + assert_eq!(differing.len(), 1, "exactly one byte moves"); + let at = differing[0]; + assert_eq!(at, 192, "three quarters in — past the headers"); + assert_eq!(bytes[at], au[at] ^ 0x40); + assert_eq!(run(), bytes, "the same spec produces the same damage"); + } + + /// Tiny AUs (a lone parameter-set NALU, a fragment) are passed through: the + /// modes are about damaging a PICTURE, and cutting a 6-byte AU only tests the + /// parser's own bounds checks. + #[test] + fn an_au_too_short_to_damage_meaningfully_is_left_alone() { + let mut f = AuFault::new(FaultMode::Truncate, 1); + assert_eq!(f.apply(&[0u8; 8]), FaultAction::Pass); + // …and the counter still advanced, so the cadence does not stall waiting + // for a big enough AU. + assert!(matches!(f.apply(&[0u8; 64]), FaultAction::Corrupt(_))); + } +} diff --git a/crates/pf-vkdecode/src/images.rs b/crates/pf-vkdecode/src/images.rs new file mode 100644 index 00000000..139759b8 --- /dev/null +++ b/crates/pf-vkdecode/src/images.rs @@ -0,0 +1,584 @@ +//! Decode image pools — the FFmpeg pool model, zero-copy: +//! +//! The PICTURE POOL is decoupled from DPB slots. Images outnumber slots by +//! [`HOLD_HEADROOM`], and a DPB slot binds an image at ACTIVATION time — a +//! re-activated slot may bind a DIFFERENT free image (spec-legal with +//! `SEPARATE_REFERENCE_IMAGES`, which the caps derivation requires for coincide +//! mode). A picture the consumer still holds is therefore NEVER a decode target: +//! its image simply stays off the free list until the release token returns. +//! This is the exact contract the presenter already speaks on the AVVkFrame path, +//! re-implemented without FFmpeg in the middle. +//! +//! - **coincide** (RADV): pool images are DPB + decode output + sampled surface +//! in one (`DPB|DST|SAMPLED`, per-slot images). +//! - **distinct** (NVIDIA): a separate reference-only DPB array (layered or +//! per-slot — never delivered, so its slot↔layer mapping stays fixed) plus the +//! pool as decode outputs (`DST|SAMPLED`). +//! +//! Every pool image carries its OWN timeline semaphore (the AVVkFrame contract): +//! the decoder signals `value+1` when it writes the image; the presenter waits +//! that value, samples, restores the layout, and signals `value+1` again in the +//! same submission — the decoder's ledger learns of that write-back at +//! `release_frame` and waits it before the image's next use. + +use ash::vk; + +use crate::caps::DecodeCaps; +use crate::caps::DecodeProfile; +use crate::caps::COINCIDE_USAGE; +use crate::caps::DPB_USAGE; +use crate::caps::OUTPUT_USAGE; +use crate::device::find_memory_type_preferring; +use crate::device::AllocError; +use crate::device::DecodeDevice; + +/// Picture-pool headroom on top of the stream's DPB needs: how many decoded +/// pictures the CONSUMER may hold (delivered, unreleased) before the decoder +/// reports backpressure. The real client pipeline holds ~4-7 frames at steady +/// state (two bounded(2) channels, the FrameStore's 1..=3 preroll, the in-flight +/// present and the retired-frame slot), so 8 gives it a frame of slack; a +/// consumer holding MORE than this earns the `NoFreeSlot` error, which then +/// means exactly what it says. +/// +/// (The 2026-08 .25 field failure taught the sizing lesson the hard way: any +/// FIXED pool ignoring the stream's DPB depth starves on a clean stream — the +/// vendored 25fps vector alone keeps `max_dpb_frames + 1 = 8` pictures resident. +/// Pool size is always `required_slots + HOLD_HEADROOM`.) +pub const HOLD_HEADROOM: u32 = 8; + +/// The pure pool shape for one (caps, required-slots) pair. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PoolPlan { + /// Distinct-mode reference-only DPB array; 0 images in coincide mode (the + /// picture pool IS the DPB backing there). + pub dpb_image_count: u32, + pub dpb_layers_per_image: u32, + pub dpb_usage: vk::ImageUsageFlags, + /// The decoupled picture pool: decode outputs + (coincide) DPB bindings. + pub picture_count: u32, + pub picture_usage: vk::ImageUsageFlags, + pub picture_flags: vk::ImageCreateFlags, +} + +/// Decide the pool shape. Pure — unit-tested below. +/// +/// `required_slots` is the stream's `max_dpb_frames + 1`; the picture pool adds +/// [`HOLD_HEADROOM`] on top so consumer-held pictures never displace decode +/// targets. Layered-coincide never reaches here (the caps derivation rejects it). +pub fn plan_pools(caps: &DecodeCaps, required_slots: u32) -> PoolPlan { + let picture_count = required_slots + HOLD_HEADROOM; + let picture_flags = vk::ImageCreateFlags::MUTABLE_FORMAT; + if caps.coincide { + PoolPlan { + dpb_image_count: 0, + dpb_layers_per_image: 0, + dpb_usage: vk::ImageUsageFlags::empty(), + picture_count, + picture_usage: COINCIDE_USAGE, + picture_flags, + } + } else { + let (dpb_image_count, dpb_layers_per_image) = if caps.layered_dpb { + (1, required_slots) + } else { + (required_slots, 1) + }; + PoolPlan { + dpb_image_count, + dpb_layers_per_image, + dpb_usage: DPB_USAGE, + picture_count, + picture_usage: OUTPUT_USAGE, + picture_flags, + } + } +} + +/// One picture-pool image with its sync + occupancy ledger. +pub(crate) struct Picture { + pub image: vk::Image, + /// Full-picture view in the session's picture format (decode dst / DPB + /// binding). + pub view: vk::ImageView, + /// Per-plane views for the presenter's sampler path, in the formats + /// [`crate::caps::plane_formats`] resolved for the picture format (`R8`/`R8G8` + /// at 8 bits, the `R10X6` pair at 10). + pub plane_views: [vk::ImageView; 2], + /// The image's own timeline semaphore (AVVkFrame contract). + pub semaphore: vk::Semaphore, + /// Latest timeline value known signalled-or-enqueued: the decoder's write + /// signal, bumped to the presenter's write-back (`frame.value + 1`) when a + /// release token reports the frame was sampled. + pub value: u64, + /// A DPB slot currently binds this image (coincide mode). + pub bound: bool, + /// A decoded picture awaiting its output verdict lives here. + pub pending: bool, + /// Frames over this image not yet released (ready queue + consumer-held). + pub held: u32, +} + +impl Picture { + /// Free for a new decode target: no slot binds it, no pending picture lives + /// in it, no unreleased frame reads it. + pub(crate) fn is_free(&self) -> bool { + !self.bound && !self.pending && self.held == 0 + } +} + +/// The decoupled picture pool. Destroys everything it created on drop +/// (null-safe); a pool with consumer-held images is retired to the decoder's +/// graveyard instead of dropped, and dies when its last release token arrives. +pub(crate) struct PicturePool { + device: ash::Device, + memory: Vec, + /// The picture format every image in this pool was created with (the + /// caps-resolved `output_format`). Stashed here because it is the ONLY place + /// that knows it by the time a frame is built: the session's caps are keyed + /// by profile and a delivered frame outlives its generation's caps entry. + /// [`crate::decoder::build_frame`] stamps it into every + /// [`crate::decoder::DecodedVkFrame`] so the consumer can tell an NV12 + /// picture from a P010 or 4:4:4 one — the H.265 path makes the format the + /// STREAM's, not a constant. + pub(crate) format: vk::Format, + pub(crate) pictures: Vec, +} + +impl PicturePool { + /// Create `plan.picture_count` single-layer images at `extent` (the + /// granularity-ALIGNED allocation extent). + /// + /// # Safety + /// + /// `dev` wraps live handles ([`crate::DeviceHandles`] contract). + pub(crate) unsafe fn create( + dev: &DecodeDevice, + caps: &DecodeCaps, + plan: &PoolPlan, + extent: vk::Extent2D, + profile: DecodeProfile, + ) -> Result { + let mut pool = Self { + device: dev.ash().clone(), + memory: Vec::new(), + format: caps.output_format, + pictures: Vec::new(), + }; + let families = dev.sharing_families(); + for _ in 0..plan.picture_count { + // SAFETY: fn contract (live device); every created handle is parked + // in `pool` so a mid-build failure unwinds through Drop. + let (image, memory) = unsafe { + create_video_image( + dev, + caps.output_format, + extent, + 1, + plan.picture_usage, + plan.picture_flags, + &families, + profile, + )? + }; + pool.memory.push(memory); + // The picture is parked with null handles IMMEDIATELY (Drop ignores + // nulls), then each view/semaphore is filled as it is created — a + // failure anywhere unwinds everything created so far. + pool.pictures.push(Picture { + image, + view: vk::ImageView::null(), + plane_views: [vk::ImageView::null(); 2], + semaphore: vk::Semaphore::null(), + value: 0, + bound: false, + pending: false, + held: 0, + }); + let picture = pool.pictures.len() - 1; + // SAFETY: `image` was just created with layer 0 in range (holds for + // all three creates in this block); the plane formats are the ones + // derive_caps/derive_caps_h265 resolved for THIS picture format and + // are plane-compatible with it under MUTABLE_FORMAT (caps-gated). + unsafe { + pool.pictures[picture].view = create_view( + &pool.device, + image, + caps.output_format, + vk::ImageAspectFlags::COLOR, + 0, + )?; + pool.pictures[picture].plane_views[0] = create_view( + &pool.device, + image, + caps.plane_view_formats[0], + vk::ImageAspectFlags::PLANE_0, + 0, + )?; + pool.pictures[picture].plane_views[1] = create_view( + &pool.device, + image, + caps.plane_view_formats[1], + vk::ImageAspectFlags::PLANE_1, + 0, + )?; + } + let mut type_info = vk::SemaphoreTypeCreateInfo::default() + .semaphore_type(vk::SemaphoreType::TIMELINE) + .initial_value(0); + let sem_ci = vk::SemaphoreCreateInfo::default().push_next(&mut type_info); + // SAFETY: live device; timelineSemaphore enabled per the handles + // contract. + pool.pictures[picture].semaphore = + unsafe { pool.device.create_semaphore(&sem_ci, None)? }; + } + Ok(pool) + } + + /// Index of the first free image, if any. + pub(crate) fn free_index(&self) -> Option { + self.pictures.iter().position(Picture::is_free) + } + + /// Total frames not yet released across the pool (graveyard retirement key). + pub(crate) fn held_total(&self) -> u32 { + self.pictures.iter().map(|p| p.held).sum() + } +} + +impl Drop for PicturePool { + fn drop(&mut self) { + // SAFETY: every handle is this pool's own on the (contract-live) device; + // the owning decoder drains decode work before dropping/retiring, and a + // retired pool is only dropped once its last release token returned (the + // presenter's fence wait). Destroys ignore NULL (half-built unwinding). + unsafe { + for p in self.pictures.drain(..) { + self.device.destroy_image_view(p.view, None); + self.device.destroy_image_view(p.plane_views[0], None); + self.device.destroy_image_view(p.plane_views[1], None); + self.device.destroy_semaphore(p.semaphore, None); + self.device.destroy_image(p.image, None); + } + for memory in self.memory.drain(..) { + self.device.free_memory(memory, None); + } + } + } +} + +/// Distinct-mode reference-only DPB backing (fixed slot↔layer mapping — these +/// images are never delivered, so nothing consumer-side ever pins them). +pub(crate) struct DpbPool { + device: ash::Device, + images: Vec, + memory: Vec, + dpb_views: Vec, + dpb_location: Vec<(usize, u32)>, +} + +impl DpbPool { + /// # Safety + /// + /// `dev` wraps live handles ([`crate::DeviceHandles`] contract). + pub(crate) unsafe fn create( + dev: &DecodeDevice, + caps: &DecodeCaps, + plan: &PoolPlan, + extent: vk::Extent2D, + profile: DecodeProfile, + ) -> Result { + let mut pool = Self { + device: dev.ash().clone(), + images: Vec::new(), + memory: Vec::new(), + dpb_views: Vec::new(), + dpb_location: Vec::new(), + }; + let families = dev.sharing_families(); + for _ in 0..plan.dpb_image_count { + // SAFETY: fn contract (live device); parked in `pool` for unwinding. + let (image, memory) = unsafe { + create_video_image( + dev, + caps.dpb_format, + extent, + plan.dpb_layers_per_image, + plan.dpb_usage, + vk::ImageCreateFlags::empty(), + &families, + profile, + )? + }; + pool.images.push(image); + pool.memory.push(memory); + } + let slots = plan.dpb_image_count * plan.dpb_layers_per_image; + for slot in 0..slots { + let (image_index, layer) = if plan.dpb_image_count == 1 { + (0usize, slot) + } else { + (slot as usize, 0u32) + }; + // SAFETY: the image was created above with `layer` in range. + let view = unsafe { + create_view( + &pool.device, + pool.images[image_index], + caps.dpb_format, + vk::ImageAspectFlags::COLOR, + layer, + )? + }; + pool.dpb_views.push(view); + pool.dpb_location.push((image_index, layer)); + } + Ok(pool) + } + + /// The DPB binding view of `slot`. + pub(crate) fn dpb_view(&self, slot: u8) -> vk::ImageView { + self.dpb_views[usize::from(slot)] + } + + /// The image + array layer behind DPB `slot` (barrier targeting). + pub(crate) fn dpb_target(&self, slot: u8) -> (vk::Image, u32) { + let (image_index, layer) = self.dpb_location[usize::from(slot)]; + (self.images[image_index], layer) + } +} + +impl Drop for DpbPool { + fn drop(&mut self) { + // SAFETY: own handles on the contract-live device; the owning decoder + // drains decode work before dropping state (nothing consumer-side ever + // references these). Destroys ignore NULL. + unsafe { + for view in self.dpb_views.drain(..) { + self.device.destroy_image_view(view, None); + } + for image in self.images.drain(..) { + self.device.destroy_image(image, None); + } + for memory in self.memory.drain(..) { + self.device.free_memory(memory, None); + } + } + } +} + +/// One OPTIMAL-tiling video image bound to fresh DEVICE_LOCAL memory, profile-listed +/// (mirrors the encoder's `make_video_image`, minus its `&mut` profile-list plumbing). +/// +/// # Safety +/// +/// `dev` wraps live handles. +#[allow(clippy::too_many_arguments)] +unsafe fn create_video_image( + dev: &DecodeDevice, + format: vk::Format, + extent: vk::Extent2D, + layers: u32, + usage: vk::ImageUsageFlags, + flags: vk::ImageCreateFlags, + families: &[u32], + decode_profile: DecodeProfile, +) -> Result<(vk::Image, vk::DeviceMemory), AllocError> { + let mut chain = decode_profile.chain(); + let profile = chain.wire(); + let mut profile_list = + vk::VideoProfileListInfoKHR::default().profiles(std::slice::from_ref(profile)); + let mut ci = vk::ImageCreateInfo::default() + .flags(flags) + .image_type(vk::ImageType::TYPE_2D) + .format(format) + .extent(vk::Extent3D { + width: extent.width, + height: extent.height, + depth: 1, + }) + .mip_levels(1) + .array_layers(layers) + .samples(vk::SampleCountFlags::TYPE_1) + .tiling(vk::ImageTiling::OPTIMAL) + .usage(usage) + .initial_layout(vk::ImageLayout::UNDEFINED) + .push_next(&mut profile_list); + ci = if families.len() >= 2 { + ci.sharing_mode(vk::SharingMode::CONCURRENT) + .queue_family_indices(families) + } else { + ci.sharing_mode(vk::SharingMode::EXCLUSIVE) + }; + // SAFETY: live device; `ci` roots a chain of locals outliving the call. + let image = unsafe { dev.ash().create_image(&ci, None)? }; + // SAFETY: `image` was just created on this device. + let req = unsafe { dev.ash().get_image_memory_requirements(image) }; + let props = dev.memory_properties(); + // DEVICE_LOCAL preferred, any advertised type accepted (same rationale as the + // session bindings: `memoryTypeBits` is the driver's placement contract). + let type_index = match find_memory_type_preferring( + &props, + req.memory_type_bits, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + ) { + Ok(index) => index, + Err(e) => { + // SAFETY: destroying the just-created, never-bound image. + unsafe { dev.ash().destroy_image(image, None) }; + return Err(e); + } + }; + let alloc = vk::MemoryAllocateInfo::default() + .allocation_size(req.size) + .memory_type_index(type_index); + // SAFETY: live device; unwind destroys the unbound image so the error path + // leaks nothing. + let memory = match unsafe { dev.ash().allocate_memory(&alloc, None) } { + Ok(m) => m, + Err(e) => { + // SAFETY: destroying the just-created, never-bound image. + unsafe { dev.ash().destroy_image(image, None) }; + return Err(e.into()); + } + }; + // SAFETY: fresh image + fresh memory of the required size. + if let Err(e) = unsafe { dev.ash().bind_image_memory(image, memory, 0) } { + // SAFETY: unwinding the two objects created above. + unsafe { + dev.ash().destroy_image(image, None); + dev.ash().free_memory(memory, None); + } + return Err(e.into()); + } + Ok((image, memory)) +} + +/// One single-layer 2D view (`base_array_layer = layer`, identity swizzle). +/// +/// # Safety +/// +/// `image` is live on `device` with `layer` in range; `format`/`aspect` are +/// compatible with the image's creation (same format for COLOR, plane-compatible +/// under MUTABLE_FORMAT for the plane aspects). +unsafe fn create_view( + device: &ash::Device, + image: vk::Image, + format: vk::Format, + aspect: vk::ImageAspectFlags, + layer: u32, +) -> Result { + let ci = vk::ImageViewCreateInfo::default() + .image(image) + .view_type(vk::ImageViewType::TYPE_2D) + .format(format) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: aspect, + base_mip_level: 0, + level_count: 1, + base_array_layer: layer, + layer_count: 1, + }); + // SAFETY: the fn-level contract restates exactly what create_image_view needs. + unsafe { device.create_image_view(&ci, None) } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::caps::derive_caps; + use crate::caps::RawH264Caps; + use crate::caps::VideoFormat; + use crate::caps::NV12; + + fn caps(coincide: bool, layered: bool) -> DecodeCaps { + // Every entry advertises its role's full usage plus MUTABLE_FORMAT — the + // derivation gates on those; this module's decision table is downstream. + let entry = |usage: vk::ImageUsageFlags| VideoFormat { + format: NV12, + image_usage: usage, + image_create_flags: vk::ImageCreateFlags::MUTABLE_FORMAT, + ..Default::default() + }; + let raw = RawH264Caps { + capability_flags: if layered { + vk::VideoCapabilityFlagsKHR::empty() + } else { + vk::VideoCapabilityFlagsKHR::SEPARATE_REFERENCE_IMAGES + }, + decode_flags: if coincide { + vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_COINCIDE + } else { + vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_DISTINCT + }, + dpb_formats: vec![entry(DPB_USAGE)], + output_formats: vec![entry(OUTPUT_USAGE)], + coincide_formats: vec![entry(COINCIDE_USAGE)], + ..Default::default() + }; + derive_caps(&raw).unwrap() + } + + #[test] + fn coincide_pools_are_headroomed_dual_use_pictures_with_no_dpb_array() { + let plan = plan_pools(&caps(true, false), 8); + assert_eq!( + plan.dpb_image_count, 0, + "the picture pool IS the DPB backing" + ); + assert_eq!( + plan.picture_count, + 8 + HOLD_HEADROOM, + "the stream's DPB depth PLUS the consumer-hold headroom — a pool \ + sized to either alone starves (.25 field failure)" + ); + assert_eq!( + plan.picture_usage, COINCIDE_USAGE, + "pool pictures are DPB + decode dst + sampled surface in one" + ); + assert_eq!(plan.picture_flags, vk::ImageCreateFlags::MUTABLE_FORMAT); + } + + #[test] + fn distinct_keeps_a_fixed_dpb_array_and_headrooms_the_output_pool() { + let plan = plan_pools(&caps(false, true), 17); + assert_eq!( + (plan.dpb_image_count, plan.dpb_layers_per_image), + (1, 17), + "layered: one array, one layer per slot" + ); + assert_eq!(plan.dpb_usage, DPB_USAGE); + assert_eq!(plan.picture_count, 17 + HOLD_HEADROOM); + assert_eq!(plan.picture_usage, OUTPUT_USAGE); + + let plan = plan_pools(&caps(false, false), 3); + assert_eq!( + (plan.dpb_image_count, plan.dpb_layers_per_image), + (3, 1), + "separate reference images: one image per slot" + ); + assert_eq!(plan.picture_count, 3 + HOLD_HEADROOM); + } + + #[test] + fn picture_occupancy_frees_only_when_unbound_unpending_and_released() { + let mut p = Picture { + image: vk::Image::null(), + view: vk::ImageView::null(), + plane_views: [vk::ImageView::null(); 2], + semaphore: vk::Semaphore::null(), + value: 0, + bound: true, + pending: true, + held: 2, + }; + assert!(!p.is_free()); + p.bound = false; + assert!(!p.is_free(), "pending pictures are not decode targets"); + p.pending = false; + assert!(!p.is_free(), "held frames are not decode targets"); + p.held = 1; + assert!(!p.is_free()); + p.held = 0; + assert!(p.is_free()); + } +} diff --git a/crates/pf-vkdecode/src/integrity.rs b/crates/pf-vkdecode/src/integrity.rs new file mode 100644 index 00000000..35892e85 --- /dev/null +++ b/crates/pf-vkdecode/src/integrity.rs @@ -0,0 +1,171 @@ +//! Which planner warnings mean the PICTURE is damaged (M4 of the native-decode +//! program). +//! +//! The planners emit two very different kinds of thing through one warning +//! channel, and the split is what a consumer must branch on: +//! +//! * **Integrity** — a reference the DPB does not hold, a `frame_num` gap, an AU +//! whose NALU walk stopped early. The plan was completed with a SUBSTITUTE in +//! place of something that was lost, so the decoded picture is damaged: its +//! output must be released unshown and the stream must ask for a re-anchor. +//! * **Spec-legal envelope signals** — h265's `NonZeroReorder` (the activated SPS +//! sets `sps_max_num_reorder_pics > 0`) and h264's `Mmco5Rebase`. pf-bitstream +//! documents both as spec-legal and fully planned; they exist as the field +//! signal that a punktfunk-host ASSUMPTION broke, not as damage. `NonZeroReorder` +//! in particular fires on the AU that ACTIVATES an SPS — the opening IDR, and the +//! fresh IDR at every ABR resolution change — so treating it as concealment would +//! cost a released-unshown frame plus a keyframe round trip at every +//! renegotiation, on a stream the planner says it planned correctly. +//! +//! It lives here, in the crate the warnings are re-exported from, rather than in +//! the client that first needed it, because the fault-injection harness +//! ([`crate::fault`]) has to assert against the SAME predicate the client conceals +//! on. Two copies of this list would let a test prove detection that production +//! does not actually perform — the exact shape of the `nb_queries = 0` failure the +//! program exists to end. + +use crate::{Av1PlanWarning, H265PlanWarning, PlanWarning}; + +/// Does this H.264 planner warning mean the PICTURE is damaged? +/// +/// `Mmco5Rebase` does not: the AU carried an MMCO 5 and pf-bitstream planned it in +/// full (the plan holds the pre-rebase 8.2.1 values; later AUs reference the +/// rebased ones). +/// +/// Written as an EXHAUSTIVE match with no wildcard, deliberately. A `matches!` (or +/// a `_ => false`) makes "damage" the opt-in and silence the default, so a +/// `PlanWarning` added later — by definition one nobody here has classified — +/// would be reported as clean and its picture shown. Invisible damage is the bug +/// this whole program exists to end; the compiler is the only reviewer guaranteed +/// to be present when that variant is written, so it gets the decision. +pub fn is_integrity_warning(w: &PlanWarning) -> bool { + match w { + PlanWarning::FrameNumGap { .. } + | PlanWarning::MissingReference { .. } + | PlanWarning::TruncatedAu { .. } => true, + PlanWarning::Mmco5Rebase => false, + } +} + +/// The H.265 twin — the same set pf-bitstream's own `h265` conformance harness +/// calls integrity, `NonZeroReorder` deliberately excluded (module docs). +/// +/// Exhaustive for the same reason as [`is_integrity_warning`]: a new H.265 warning +/// must not be able to mean "damaged" and read as clean. +pub fn is_integrity_warning_h265(w: &H265PlanWarning) -> bool { + match w { + H265PlanWarning::MissingReference { .. } | H265PlanWarning::TruncatedAu { .. } => true, + H265PlanWarning::NonZeroReorder { .. } => false, + } +} + +/// The AV1 twin (M7). Every variant the AV1 planner has today IS damage, and that +/// is a fact about the codec rather than an oversight: AV1 puts nothing in this +/// channel that resembles h265's `NonZeroReorder` or h264's `Mmco5Rebase`. It has +/// no reorder envelope to report (no bumping process, no `max_num_reorder_pics`) +/// and no MMCO to rebase — the frame header states the whole reference update +/// outright — so the only things left to warn about are pictures that went +/// missing and an OBU walk that stopped early. +/// +/// `MissingShowExisting` is the one that could be argued, and it is damage: a +/// `show_existing_frame` naming an empty slot means the picture the STREAM chose +/// to display was lost upstream. Nothing is displayed for that frame, so the +/// screen keeps the previous one — exactly the "silently stale picture" state a +/// re-anchor exists to end. +/// +/// ⚠ `MissingReference` is classified here for completeness and does NOT normally +/// reach a consumer through this predicate: [`crate::VkAv1Decoder`] refuses the +/// whole access unit for it ([`crate::VkDecodeError::MissingReferenceAv1`]), +/// because AV1's `refs` array is indexed by reference NAME and there is no legal +/// substitute to write into a hole — a `-1` for a name the frame really references +/// is a spec violation whose firmware behaviour is undefined. So the AV1 rung +/// answers a lost reference as a REFUSAL, not as concealment, and it is the +/// refusal counter that moves. Classifying it as damage here anyway keeps the two +/// statements consistent for any consumer that does see the warning (and for the +/// fault harness, which asserts detection against exactly this list). +/// +/// Exhaustive for the same reason as [`is_integrity_warning`]: a new AV1 warning +/// must not be able to mean "damaged" and read as clean. +pub fn is_integrity_warning_av1(w: &Av1PlanWarning) -> bool { + match w { + Av1PlanWarning::MissingReference { .. } + | Av1PlanWarning::MissingShowExisting { .. } + | Av1PlanWarning::TruncatedAu { .. } => true, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The split, stated as the pair of lists it is. This test is the contract: + /// the client drops a frame for everything on the left and shows the picture + /// for everything on the right, and the fault harness asserts detection + /// against the very same predicate. + #[test] + fn damage_is_a_lost_reference_or_a_short_au_and_nothing_else() { + for w in [ + PlanWarning::FrameNumGap { + expected: 4, + got: 7, + }, + PlanWarning::MissingReference { + context: "list0", + detail: "poc 12".into(), + }, + PlanWarning::TruncatedAu { offset: 900 }, + ] { + assert!(is_integrity_warning(&w), "{w:?} is damage"); + } + assert!( + !is_integrity_warning(&PlanWarning::Mmco5Rebase), + "an MMCO 5 was planned in FULL — dropping its frame would hitch a \ + correct stream" + ); + + for w in [ + H265PlanWarning::MissingReference { + context: "StCurrBefore", + detail: "poc 12".into(), + }, + H265PlanWarning::TruncatedAu { offset: 900 }, + ] { + assert!(is_integrity_warning_h265(&w), "{w:?} is damage"); + } + assert!( + !is_integrity_warning_h265(&H265PlanWarning::NonZeroReorder { + max_num_reorder_pics: 1 + }), + "SPS activation is not damage — it fires on the opening IDR and on \ + every ABR renegotiation's IDR" + ); + } + + /// AV1's whole warning vocabulary is damage. Note plainly what this test does + /// and does not guard, because the two are easy to confuse: + /// + /// * A NEW variant is caught by the EXHAUSTIVE MATCH in + /// [`is_integrity_warning_av1`], not here — this loop enumerates the variants + /// by hand, so a fourth one would simply not appear in it. That is the whole + /// reason the function is written as a match with no `_` arm. + /// * What this test does guard is a RECLASSIFICATION: split one of these names + /// out of the `|` chain and give it a `false` arm — the shape a future + /// "spec-legal AV1 signal" would arrive in — and the assertion below fires. + /// `MissingShowExisting` is the one most likely to be argued down that way (a + /// frame that decoded nothing and displayed nothing reads as harmless), and + /// reading it as clean would leave the previous picture on the screen with no + /// re-anchor asked for. + #[test] + fn every_av1_warning_is_damage_because_av1_has_no_envelope_signal() { + for w in [ + Av1PlanWarning::MissingReference { + slot: 3, + ref_index: 1, + }, + Av1PlanWarning::MissingShowExisting { slot: 5 }, + Av1PlanWarning::TruncatedAu { offset: 900 }, + ] { + assert!(is_integrity_warning_av1(&w), "{w:?} is damage"); + } + } +} diff --git a/crates/pf-vkdecode/src/lib.rs b/crates/pf-vkdecode/src/lib.rs new file mode 100644 index 00000000..d46407ae --- /dev/null +++ b/crates/pf-vkdecode/src/lib.rs @@ -0,0 +1,258 @@ +//! Native Vulkan Video H.264 decode for the clients — M2 of the native-decode program +//! (design/client-native-decode.md §3.2). +//! +//! This crate sits between [`pf_bitstream`]'s per-AU planning and Vulkan Video +//! submission. +//! +//! WP-A — the CPU-testable half, everything runs without a GPU: +//! +//! - [`params`]: the vendored parser's `Sps`/`Pps` converted into the +//! `StdVideoH264*ParameterSet` structs session parameters are created from, behind +//! owning wrappers ([`OwnedStdSps`]/[`OwnedStdPps`]) because the Std structs embed +//! raw pointers. +//! - [`slots`]: [`SlotMap`], the hardware DPB slot ledger keyed by +//! [`pf_bitstream::h264::PicId`]. pf-bitstream's DPB decides what lives and dies; +//! this map only translates ids to slot indices and refuses to guess. +//! - [`pic`]: [`plan_to_vk`], one [`pf_bitstream::h264::AuPlan`] converted into the +//! `StdVideoDecodeH264PictureInfo`/`StdVideoDecodeH264ReferenceInfo` set plus slice +//! offsets and slot bindings a `vkCmdDecodeVideoKHR` call wants. +//! +//! WP-B — the GPU half, built ON the borrowed presenter device (this crate never +//! creates or destroys a VkDevice) with the decision logic split out pure so it +//! stays CPU-testable: +//! +//! - [`device`]: [`DeviceHandles`] (the borrowed handle bundle + its liveness +//! contract), the [`QueueLock`] trait every submit runs under, [`DecodeDevice`]. +//! - [`caps`]: one thin driver query + [`derive_caps`], the pure +//! coincide/distinct/layered decision table ([`DecodeCaps`]). +//! - [`session`]: `VkVideoSessionKHR` + versioned session parameters (pure ledger +//! decides Add-vs-Recreate; extent/DPB renegotiation rebuilds the session). +//! - [`images`]: the picture pool DECOUPLED from DPB slots (the zero-copy FFmpeg +//! pool model — a re-activated slot binds a fresh free image, so delivered +//! pictures are never decode targets), per-image timeline semaphores with the +//! presenter `value+1` write-back, per-plane views, [`HOLD_HEADROOM`] sizing. +//! - [`ring`]: the host-visible bitstream upload ring (pure alignment/growth +//! math) — SLICE NALUs only (feeding a whole AU hangs VCN firmware). +//! - [`decoder`]: [`VkH264Decoder`] — plan → convert → upload → record → submit, +//! with a per-op `RESULT_STATUS_ONLY` query ([`VkH264Decoder::poll_status`]) so +//! driver-reported corruption is finally observable (the Ally X class) — +//! caps-gated per queue family: where `queryResultStatusSupport` is absent +//! (RADV), verdicts degrade to timeline completion, FFmpeg parity. +//! +//! M3 (HEVC) — the CPU half, over [`pf_bitstream::h265`]'s WP-1 planner: +//! +//! - [`params_h265`]: VPS/SPS/PPS into the `StdVideoH265*ParameterSet` structs +//! behind owning wrappers ([`OwnedStdH265Vps`]/[`OwnedStdH265Sps`]/ +//! [`OwnedStdH265Pps`]) — Main/Main10/4:4:4 RExt fidelity carried through, the +//! rest of the envelope rejected typed. +//! - [`pic_h265`]: [`plan_to_vk_h265`], one [`pf_bitstream::h265::AuPlan`] into +//! `StdVideoDecodeH265PictureInfo`/`StdVideoDecodeH265ReferenceInfo` plus the +//! RPS index arrays, slice offsets and slot bindings — over the SAME +//! [`SlotMap`] (H.265's DPB ceiling is H.264's: 16 references + 1 setup). +//! +//! M3 (HEVC) — the GPU half, sharing every codec-agnostic piece with H.264 +//! (picture pool, bitstream ring, op ring, DPB settling, frame delivery) rather +//! than re-implementing them: +//! +//! - [`caps_h265`]: [`H265ProfileKey`] (the stream's profile idc, chroma format +//! and bit depths, which Vulkan wants stated on every object) and +//! [`derive_caps_h265`] — Main → NV12, Main 10 → P010, RExt 4:4:4 → the +//! two-plane 4:4:4 formats, with a device that cannot host the combination +//! refused BEFORE a session exists ([`CapsError::NoFormat`]). +//! - [`session_h265`]: the H.265 session and its THREE-array parameters ledger — +//! VPS included, with [`fallback_vps_from_sps`] standing in (and deduping +//! correctly) for streams joined after their VPS NALU. +//! - [`decoder_h265`]: [`VkH265Decoder`], mirroring [`VkH264Decoder`]'s public +//! surface method-for-method. Codec DISPATCH is the client wiring's job. +//! +//! M7 (AV1) — the CPU half, over [`pf_bitstream::av1`]'s planner: +//! +//! - [`params_av1`]: the sequence header into `StdVideoAV1SequenceHeader` behind an +//! owning wrapper ([`OwnedStdAv1SequenceHeader`]) — the ONE parameter set AV1 has. +//! - [`pic_av1`]: [`plan_to_vk_av1`], one [`pf_bitstream::av1::AuPlan`] into +//! `StdVideoDecodeAV1PictureInfo` and its eight per-frame sub-blocks, plus the +//! per-reference-NAME DPB SLOT table, the tile-group ranges and the slot bindings +//! — over the SAME [`SlotMap`] (AV1's ceiling is eight references + one setup). +//! +//! M7 (AV1) — the GPU half, sharing every codec-agnostic piece with the other two +//! (picture pool, bitstream ring, op ring, frame delivery, DPB settling) rather +//! than re-implementing them: +//! +//! - [`caps_av1`]: [`Av1ProfileKey`] — Std profile, sampling, bit depth AND the +//! sequence's film-grain flag, because `filmGrainSupport` is part of the Vulkan +//! decode PROFILE — and [`derive_caps_av1`]: 4:2:0 8-bit → NV12, 10-bit → P010, +//! 4:4:4 → the two-plane 4:4:4 pair, with a device that cannot host the +//! combination (film grain very much included) refused BEFORE a session exists. +//! - [`session_av1`]: the AV1 session and its ONE-set parameters ledger — no PPS, +//! no VPS, no update path at all, so a changed sequence header RECREATES. +//! - [`decoder_av1`]: [`VkAv1Decoder`], mirroring [`VkH265Decoder`]'s public +//! surface method-for-method, over temporal units that may carry several frames. +//! +//! M4 (status and telemetry) — three pure modules turning the signals above into +//! something a session, a user and a support engineer can act on: +//! +//! - [`recovery`]: [`RecoveryWatch`], the recovery point SEI folded into a +//! per-picture "the stream healed HERE" mark ([`RecoveryMark`], carried on +//! [`DecodedVkFrame::recovery`]). The only clean point an intra-refresh session +//! has — its wave emits no IDR — so without it a client freezes for its full +//! backstop and then forces the very IDR the wave exists to avoid. +//! - [`integrity`]: [`is_integrity_warning`] / [`is_integrity_warning_h265`] / +//! [`is_integrity_warning_av1`], the one list of warnings that mean the PICTURE +//! is damaged. Here rather than in the client so the fault harness asserts +//! against the predicate production conceals on. +//! - [`fault`]: [`AuFault`], deliberate decoder-input corruption +//! (`PUNKTFUNK_AU_FAULT`), inert unless armed. A detector nobody can fire is +//! exactly as trustworthy as no detector at all. +//! +//! Plus [`VkH264Decoder::status_queries`] / [`VkH265Decoder::status_queries`]: does +//! this device answer per-op `RESULT_STATUS` at all? Without that fact a clean +//! integrity report cannot be told apart from an unmeasured one — which is the +//! precise failure the program exists to end. +//! +//! Unsafe posture: unlike pf-bitstream (which forbids unsafe outright), this crate +//! cannot — the `ash::vk::native` bindgen structs are zero-initialized the way the +//! encode side does it (`pf-encode/src/enc/linux/vk_build.rs`), and the GPU half is +//! Vulkan FFI. Every unsafe block therefore carries a written `// SAFETY:` proof, +//! enforced (and unlike the encoder there is NO file-level +//! `unsafe_op_in_unsafe_fn` exemption — every operation is individually fenced): +#![deny(clippy::undocumented_unsafe_blocks)] + +pub mod caps; +pub mod caps_av1; +pub mod caps_h265; +pub mod decoder; +pub mod decoder_av1; +pub mod decoder_h265; +pub mod device; +pub mod fault; +pub mod images; +pub mod integrity; +pub mod params; +pub mod params_av1; +pub mod params_h265; +pub mod pic; +pub mod pic_av1; +pub mod pic_h265; +pub mod probe; +pub mod recovery; +pub mod ring; +pub mod session; +pub mod session_av1; +pub mod session_h265; +pub mod slots; + +/// Re-exported for the integration layer (WP-C): [`DecodedVkFrame`]'s handle fields are +/// ash types, and the consumer (pf-client-core, whose own `ash` is optional/feature-gated) +/// flattens them to raw `u64`s through `ash::vk::Handle` — via THIS instance of ash, so +/// the versions can never skew. +pub use ash; +// The pf-bitstream types a [`DecodedVkFrame`] consumer names, re-exported so it +// doesn't grow a pf-bitstream dependency of its own: +/// [`VkAv1Decoder::take_warnings`]'s warning type — the AV1 twin of +/// [`PlanWarning`], renamed for the same reason [`H265PlanWarning`] is: the three +/// enums are genuinely different (AV1 has `MissingShowExisting`, and its +/// `MissingReference` needs no interpretation because no AV1 process empties a +/// reference slot behind the stream's back) and a consumer dispatching per codec +/// must be able to name all three. +pub use pf_bitstream::av1::PlanWarning as Av1PlanWarning; +/// [`DecodedVkFrame::colour`]'s type. +pub use pf_bitstream::h264::ColourDescription; +/// [`DecodedVkFrame::crop`]'s type. +pub use pf_bitstream::h264::DisplayCrop; +/// [`VkH264Decoder::take_warnings`]'s warning type. +pub use pf_bitstream::h264::PlanWarning; +/// [`VkH265Decoder::take_warnings`]'s warning type — the H.265 twin of +/// [`PlanWarning`], renamed rather than shadowed because the two enums are +/// genuinely different (H.264 has `FrameNumGap`/`Mmco5Rebase`, H.265 has +/// `NonZeroReorder`) and a consumer dispatching per codec must be able to name +/// BOTH. Without it the client could only render warnings as strings — and it has +/// to BRANCH on them: `NonZeroReorder` and `Mmco5Rebase` are spec-legal facts the +/// planner planned in full, not concealment, and dropping their frames would cost +/// a visible hitch at every SPS activation. +pub use pf_bitstream::h265::PlanWarning as H265PlanWarning; + +pub use caps::derive_caps; +pub use caps::plane_formats; +pub use caps::CapsError; +pub use caps::DecodeCaps; +pub use caps::MaxLevelIdc; +pub use caps::RawH264Caps; +pub use caps::VideoFormat; +pub use caps::NV12; +pub use caps::OUTPUT_FORMATS; +pub use caps::P010; +pub use caps::YUV444_10; +pub use caps::YUV444_8; +pub use caps_av1::derive_caps_av1; +pub use caps_av1::Av1ProfileKey; +pub use caps_av1::RawAv1Caps; +pub use caps_h265::derive_caps_h265; +pub use caps_h265::output_format_for; +pub use caps_h265::H265ProfileKey; +pub use caps_h265::RawH265Caps; +pub use decoder::DecodeStatus; +pub use decoder::DecodedVkFrame; +pub use decoder::VkDecodeError; +pub use decoder::VkH264Decoder; +pub use decoder_av1::plan_bitstream; +pub use decoder_av1::Av1Bitstream; +pub use decoder_av1::Av1TileError; +pub use decoder_av1::VkAv1Decoder; +pub use decoder_h265::VkH265Decoder; +pub use device::DecodeDevice; +pub use device::DeviceHandles; +pub use device::NoopQueueLock; +pub use device::QueueLock; +pub use device::QueueSubmitGuard; +pub use fault::AuFault; +pub use fault::FaultAction; +pub use fault::FaultMode; +pub use fault::DEFAULT_FAULT_PERIOD; +pub use images::plan_pools; +pub use images::PoolPlan; +pub use images::HOLD_HEADROOM; +pub use integrity::is_integrity_warning; +pub use integrity::is_integrity_warning_av1; +pub use integrity::is_integrity_warning_h265; +pub use params::pps_to_std; +pub use params::sps_to_std; +pub use params::OwnedStdPps; +pub use params::OwnedStdSps; +pub use params::ParamsError; +pub use params_av1::sequence_to_std; +pub use params_av1::OwnedStdAv1SequenceHeader; +pub use params_av1::ParamsAv1Error; +pub use params_h265::fallback_vps_from_sps; +pub use params_h265::pps_to_std_h265; +pub use params_h265::sps_to_std_h265; +pub use params_h265::vps_to_std_h265; +pub use params_h265::H265ParamsError; +pub use params_h265::OwnedStdH265Pps; +pub use params_h265::OwnedStdH265Sps; +pub use params_h265::OwnedStdH265Vps; +pub use pic::plan_to_vk; +pub use pic::DecodePlanVk; +pub use pic::PlanToVkError; +pub use pic::VkRef; +pub use pic_av1::plan_to_vk_av1; +pub use pic_av1::DecodePlanVkAv1; +pub use pic_av1::OwnedStdAv1PictureInfo; +pub use pic_av1::PlanToVkAv1Error; +pub use pic_av1::VkRefAv1; +pub use pic_av1::REFERENCE_NAME_UNUSED; +pub use pic_h265::plan_to_vk_h265; +pub use pic_h265::DecodePlanVkH265; +pub use pic_h265::PlanToVkH265Error; +pub use pic_h265::VkRefH265; +pub use pic_h265::H265_RPS_LIST_SIZE; +pub use recovery::RecoveryMark; +pub use recovery::RecoveryWatch; +pub use ring::RingLayout; +pub use session::ParamsAction; +pub use session::SessionConfig; +pub use session_av1::ParamsActionAv1; +pub use session_av1::SessionConfigAv1; +pub use session_h265::ParamsActionH265; +pub use session_h265::SessionConfigH265; +pub use slots::SlotError; +pub use slots::SlotMap; diff --git a/crates/pf-vkdecode/src/params.rs b/crates/pf-vkdecode/src/params.rs new file mode 100644 index 00000000..522ab0ab --- /dev/null +++ b/crates/pf-vkdecode/src/params.rs @@ -0,0 +1,834 @@ +//! Parameter-set conversion: the vendored parser's [`Sps`]/[`Pps`] into the +//! `StdVideoH264*ParameterSet` structs a Vulkan Video session-parameters object is +//! created from (WP-B's `vkCreateVideoSessionParametersKHR`). +//! +//! The Std structs embed raw pointers (`pOffsetForRefFrame`, `pScalingLists`, +//! `pSequenceParameterSetVui`), so conversion returns OWNING wrappers instead of bare +//! structs — see [`OwnedStdSps`] for the aliasing/lifetime contract. +//! +//! VUI is deliberately not converted: a DECODE session consumes no VUI (it shapes +//! display, not reconstruction), so `vui_parameters_present_flag` stays 0 and +//! `pSequenceParameterSetVui` stays null. Colour handling rides +//! [`pf_bitstream::h264::PicturePlan`] into the presenter instead, exactly as the +//! FFmpeg-based path did. + +use ash::vk::native as hh; +use cros_codecs::codec::h264::parser::Level; +pub use cros_codecs::codec::h264::parser::Pps; +pub use cros_codecs::codec::h264::parser::Sps; + +/// A parameter set that cannot be represented as a StdVideo struct. All of these are +/// outside the punktfunk decode envelope (8-bit 4:2:0 streams from encoders we +/// control), so hitting one is a stream-integrity failure, not a feature gap — +/// reject-with-error rather than submit a half-truth to a driver. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParamsError { + /// `profile_idc` has no `StdVideoH264ProfileIdc` code point. Vulkan defines + /// Baseline (66), Main (77), High (100) and High 4:4:4 Predictive (244); + /// Extended/High10/High422 land here. + UnmappableProfileIdc(u8), + /// `chroma_format_idc` past 3 — not legal H.264 to begin with. + InvalidChromaFormatIdc(u8), + /// `pic_order_cnt_type` past 2 — not legal H.264 to begin with. + InvalidPocType(u8), + /// `weighted_bipred_idc` of 3: representable in the two-bit field, invalid per + /// 7.4.2.2, and no `StdVideoH264WeightedBipredIdc` code point exists for it. + InvalidWeightedBipredIdc(u8), + /// FMO (`num_slice_groups_minus1 > 0`): `StdVideoH264PictureParameterSet` has no + /// slice-group fields at all — Vulkan Video cannot express it. + SliceGroups(u32), +} + +impl std::fmt::Display for ParamsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParamsError::UnmappableProfileIdc(idc) => { + write!( + f, + "profile_idc {idc} has no StdVideoH264ProfileIdc code point" + ) + } + ParamsError::InvalidChromaFormatIdc(idc) => { + write!(f, "invalid chroma_format_idc {idc}") + } + ParamsError::InvalidPocType(t) => write!(f, "invalid pic_order_cnt_type {t}"), + ParamsError::InvalidWeightedBipredIdc(idc) => { + write!(f, "invalid weighted_bipred_idc {idc}") + } + ParamsError::SliceGroups(n) => { + write!( + f, + "FMO ({} slice groups) is not expressible in Vulkan Video", + n + 1 + ) + } + } + } +} + +impl std::error::Error for ParamsError {} + +/// The converted SPS plus the heap allocations its embedded pointers target. +/// +/// `StdVideoH264SequenceParameterSet` points at data it does not contain: the +/// POC-type-1 offset array (`pOffsetForRefFrame`) and the scaling lists +/// (`pScalingLists`). This wrapper owns that data, and the ownership design is the +/// contract WP-B builds on: +/// +/// - The backing is boxed, so the wrapper may be MOVED freely: moving it relocates +/// the `Box` handles (pointer values), never the heap blocks the Std struct's +/// pointers hold the addresses of. ⚠ The Std struct ITSELF is boxed for the same +/// reason and it is not decoration: `pStdSPSs` — the OUTER pointer the create/add +/// info carries — is [`Self::std`]'s address, and the session hands it over +/// BEFORE moving the wrapper into its stored parameters. Inline, that address +/// would be a moved-from slot; boxed, it is the one the object keeps +/// (`crate::session`'s `an_added_set_keeps_the_address_the_update_call_was_given`). +/// A driver retaining the outer pointer rather than an inner one would otherwise +/// reproduce the AV1 use-after-free exactly, with the same silent signature. +/// - [`Self::std`] hands the struct out by shared reference. The struct is `Copy`; a +/// copy taken out of the wrapper still points INTO the wrapper's backing and must +/// not outlive it. ⚠⚠ The obligation is NOT merely "keep the wrapper alive across +/// `vkCreateVideoSessionParametersKHR`", which is what this said and what the +/// spec reads like: NVIDIA 610.57.04 was measured retaining an embedded pointer +/// out of a Std set and dereferencing it at every `vkCmdDecodeVideoKHR` +/// ([`crate::session_av1`]). The wrapper must outlive the parameters OBJECT, and +/// [`crate::session`] is where that is enforced by construction. +/// - Nothing exposes mutation of the backing, so for the wrapper's lifetime the +/// pointed-to data is immutable and the `*const` aliasing rules hold trivially. +/// - Deliberately NOT `Clone`: a derived clone would duplicate the pointer VALUES but +/// not the backing, silently tying the clone's validity to the original's lifetime. +/// Re-convert from the `Sps` instead — conversion is cheap and pure. +#[derive(Debug)] +pub struct OwnedStdSps { + /// Boxed so [`Self::std`]'s ADDRESS — what `pStdSPSs` points at — survives every + /// move of the wrapper (type-level contract). + std: Box, + /// `pOffsetForRefFrame`'s target (POC type 1 only, else `None`/null). + _offset_backing: Option>, + /// `pScalingLists`' target (`seq_scaling_matrix_present_flag` only, else null). + _scaling_backing: Option>, +} + +impl OwnedStdSps { + /// The Std struct, valid for as long as `self` lives (see the type-level + /// contract; do not let a `Copy` of it outlive the wrapper). + pub fn std(&self) -> &hh::StdVideoH264SequenceParameterSet { + &self.std + } +} + +/// The converted PPS plus the scaling-list allocation its `pScalingLists` targets. +/// Same ownership contract as [`OwnedStdSps`], with the one pointer. +#[derive(Debug)] +pub struct OwnedStdPps { + /// Boxed for [`OwnedStdSps`]'s reason: `pStdPPSs` is this field's address. + std: Box, + _scaling_backing: Option>, +} + +impl OwnedStdPps { + /// The Std struct, valid for as long as `self` lives (see [`OwnedStdSps`]). + pub fn std(&self) -> &hh::StdVideoH264PictureParameterSet { + &self.std + } +} + +/// H.264 `level_idc` (value-coded: 10 ⇒ 1.0) to Vulkan's index-coded +/// `StdVideoH264LevelIdc`. The Std code points ascend with the level, so the +/// decoder's `maxLevelIdc` gate compares them numerically. +pub(crate) const fn level_to_std(level: Level) -> hh::StdVideoH264LevelIdc { + match level { + Level::L1 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_1_0, + // Vulkan has no 1b code point. 1b is signalled on the wire as level_idc 11 + // plus constraint_set3_flag — the flag is mapped, so 1.1 is the faithful cap. + Level::L1B => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_1_1, + Level::L1_1 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_1_1, + Level::L1_2 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_1_2, + Level::L1_3 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_1_3, + Level::L2_0 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_2_0, + Level::L2_1 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_2_1, + Level::L2_2 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_2_2, + Level::L3 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_3_0, + Level::L3_1 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_3_1, + Level::L3_2 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_3_2, + Level::L4 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_4_0, + Level::L4_1 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_4_1, + Level::L4_2 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_4_2, + Level::L5 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_5_0, + Level::L5_1 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_5_1, + Level::L5_2 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_5_2, + Level::L6 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_6_0, + Level::L6_1 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_6_1, + Level::L6_2 => hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_6_2, + } +} + +/// Pack the parser's scaling-list arrays into the Std layout. +/// +/// The vendored parser has already run 7.3.2.1.1.1 plus the Table 7-2 default and +/// fallback rules, so the arrays hold the fully RESOLVED lists. Every resolved list is +/// therefore declared present verbatim (`scaling_list_present_mask` set, +/// `use_default_scaling_matrix_mask` 0) and the driver applies no further inference — +/// simpler and equivalent to re-encoding which lists came from the bitstream. +/// +/// `num_8x8` is the count of 8x8 lists the parser actually resolved: 2 for 4:2:0/4:2:2 +/// (Y intra/inter), 6 for 4:4:4, and 0 for a PPS without `transform_8x8_mode_flag` +/// (whose 8x8 arrays are untouched zeros and must not be declared present). +fn scaling_lists_to_std( + lists_4x4: &[[u8; 16]; 6], + lists_8x8: &[[u8; 64]; 6], + num_8x8: u16, +) -> hh::StdVideoH264ScalingLists { + // SAFETY: StdVideoH264ScalingLists is a plain-C bindgen struct of two u16 masks + // and two byte arrays; the all-zero bit pattern is a valid value for every field. + let mut std: hh::StdVideoH264ScalingLists = unsafe { std::mem::zeroed() }; + std.scaling_list_present_mask = 0x3F | (((1u16 << num_8x8) - 1) << 6); + std.use_default_scaling_matrix_mask = 0; + std.ScalingList4x4 = *lists_4x4; + // All six 8x8 arrays are copied even when only two are declared present; the + // driver ignores entries whose mask bit is clear. + std.ScalingList8x8 = *lists_8x8; + std +} + +/// Convert one SPS into the Std struct (owning wrapper), mapping every field the +/// H.264 decode profile consumes. VUI is skipped by design (module docs). +pub fn sps_to_std(sps: &Sps) -> Result { + // StdVideoH264ProfileIdc code points equal the profile_idc values they name, so + // recognised ones pass through; everything else has no representation. + let profile_idc = match u32::from(sps.profile_idc) { + p @ (66 | 77 | 100 | 244) => p, + _ => return Err(ParamsError::UnmappableProfileIdc(sps.profile_idc)), + }; + if sps.chroma_format_idc > 3 { + return Err(ParamsError::InvalidChromaFormatIdc(sps.chroma_format_idc)); + } + if sps.pic_order_cnt_type > 2 { + return Err(ParamsError::InvalidPocType(sps.pic_order_cnt_type)); + } + + // SAFETY: StdVideoH264SequenceParameterSet is a plain-C bindgen struct of + // integers, a bitfield word and const pointers; all-zero is a valid value for + // every field (null for the pointers) and is the "everything absent" baseline the + // field writes below build on. Same idiom as pf-encode's vk_build.rs. + let mut std: hh::StdVideoH264SequenceParameterSet = unsafe { std::mem::zeroed() }; + + std.flags + .set_constraint_set0_flag(u32::from(sps.constraint_set0_flag)); + std.flags + .set_constraint_set1_flag(u32::from(sps.constraint_set1_flag)); + std.flags + .set_constraint_set2_flag(u32::from(sps.constraint_set2_flag)); + std.flags + .set_constraint_set3_flag(u32::from(sps.constraint_set3_flag)); + std.flags + .set_constraint_set4_flag(u32::from(sps.constraint_set4_flag)); + std.flags + .set_constraint_set5_flag(u32::from(sps.constraint_set5_flag)); + std.flags + .set_direct_8x8_inference_flag(u32::from(sps.direct_8x8_inference_flag)); + std.flags + .set_mb_adaptive_frame_field_flag(u32::from(sps.mb_adaptive_frame_field_flag)); + // 1 under the punktfunk envelope (pf-bitstream rejects interlaced SPSes), but the + // conversion itself is faithful, not envelope-coupled. + std.flags + .set_frame_mbs_only_flag(u32::from(sps.frame_mbs_only_flag)); + std.flags + .set_delta_pic_order_always_zero_flag(u32::from(sps.delta_pic_order_always_zero_flag)); + std.flags + .set_separate_colour_plane_flag(u32::from(sps.separate_colour_plane_flag)); + std.flags + .set_gaps_in_frame_num_value_allowed_flag(u32::from( + sps.gaps_in_frame_num_value_allowed_flag, + )); + std.flags + .set_qpprime_y_zero_transform_bypass_flag(u32::from( + sps.qpprime_y_zero_transform_bypass_flag, + )); + std.flags + .set_frame_cropping_flag(u32::from(sps.frame_cropping_flag)); + std.flags + .set_seq_scaling_matrix_present_flag(u32::from(sps.seq_scaling_matrix_present_flag)); + // vui_parameters_present_flag stays 0: decode sessions consume no VUI (module docs). + + std.profile_idc = profile_idc; + std.level_idc = level_to_std(sps.level_idc); + // Chroma format code points equal the chroma_format_idc values (0..3). + std.chroma_format_idc = u32::from(sps.chroma_format_idc); + std.seq_parameter_set_id = sps.seq_parameter_set_id; + std.bit_depth_luma_minus8 = sps.bit_depth_luma_minus8; + std.bit_depth_chroma_minus8 = sps.bit_depth_chroma_minus8; + std.log2_max_frame_num_minus4 = sps.log2_max_frame_num_minus4; + // POC type code points equal the pic_order_cnt_type values (0..2). + std.pic_order_cnt_type = u32::from(sps.pic_order_cnt_type); + std.offset_for_non_ref_pic = sps.offset_for_non_ref_pic; + std.offset_for_top_to_bottom_field = sps.offset_for_top_to_bottom_field; + std.log2_max_pic_order_cnt_lsb_minus4 = sps.log2_max_pic_order_cnt_lsb_minus4; + std.max_num_ref_frames = sps.max_num_ref_frames; + std.pic_width_in_mbs_minus1 = u32::from(sps.pic_width_in_mbs_minus1); + std.pic_height_in_map_units_minus1 = u32::from(sps.pic_height_in_map_units_minus1); + std.frame_crop_left_offset = sps.frame_crop_left_offset; + std.frame_crop_right_offset = sps.frame_crop_right_offset; + std.frame_crop_top_offset = sps.frame_crop_top_offset; + std.frame_crop_bottom_offset = sps.frame_crop_bottom_offset; + + // POC type 1's offset array: exactly num_ref_frames_in_pic_order_cnt_cycle + // entries, boxed so the pointer survives moves of the wrapper. The COUNT field + // and the pointer derive from this one condition so they can never disagree — a + // stale cycle count on a type-0/2 SPS must not become a nonzero count over a + // null array (both stay zeroed instead). + let offset_backing = + (sps.pic_order_cnt_type == 1 && sps.num_ref_frames_in_pic_order_cnt_cycle > 0).then(|| { + let cycle = usize::from(sps.num_ref_frames_in_pic_order_cnt_cycle); + Box::<[i32]>::from(&sps.offset_for_ref_frame[..cycle]) + }); + if let Some(backing) = &offset_backing { + std.num_ref_frames_in_pic_order_cnt_cycle = sps.num_ref_frames_in_pic_order_cnt_cycle; + std.pOffsetForRefFrame = backing.as_ptr(); + } + + let scaling_backing = sps.seq_scaling_matrix_present_flag.then(|| { + let num_8x8 = if sps.chroma_format_idc == 3 { 6 } else { 2 }; + Box::new(scaling_lists_to_std( + &sps.scaling_lists_4x4, + &sps.scaling_lists_8x8, + num_8x8, + )) + }); + if let Some(backing) = &scaling_backing { + std.pScalingLists = &**backing; + } + + Ok(OwnedStdSps { + std: Box::new(std), + _offset_backing: offset_backing, + _scaling_backing: scaling_backing, + }) +} + +/// Convert one PPS into the Std struct (owning wrapper), mapping every field the +/// H.264 decode profile consumes. +/// +/// `num_slice_groups_minus1` has no Std field at all; a PPS carrying FMO is rejected +/// rather than converted into a struct that silently claims there is none. +pub fn pps_to_std(pps: &Pps) -> Result { + if pps.num_slice_groups_minus1 != 0 { + return Err(ParamsError::SliceGroups(pps.num_slice_groups_minus1)); + } + if pps.weighted_bipred_idc > 2 { + return Err(ParamsError::InvalidWeightedBipredIdc( + pps.weighted_bipred_idc, + )); + } + + // SAFETY: StdVideoH264PictureParameterSet is a plain-C bindgen struct of + // integers, a bitfield word and one const pointer; all-zero is a valid value for + // every field (null for the pointer) and is the baseline the writes below fill. + let mut std: hh::StdVideoH264PictureParameterSet = unsafe { std::mem::zeroed() }; + + std.flags + .set_transform_8x8_mode_flag(u32::from(pps.transform_8x8_mode_flag)); + std.flags + .set_redundant_pic_cnt_present_flag(u32::from(pps.redundant_pic_cnt_present_flag)); + std.flags + .set_constrained_intra_pred_flag(u32::from(pps.constrained_intra_pred_flag)); + std.flags + .set_deblocking_filter_control_present_flag(u32::from( + pps.deblocking_filter_control_present_flag, + )); + std.flags + .set_weighted_pred_flag(u32::from(pps.weighted_pred_flag)); + std.flags + .set_bottom_field_pic_order_in_frame_present_flag(u32::from( + pps.bottom_field_pic_order_in_frame_present_flag, + )); + std.flags + .set_entropy_coding_mode_flag(u32::from(pps.entropy_coding_mode_flag)); + std.flags + .set_pic_scaling_matrix_present_flag(u32::from(pps.pic_scaling_matrix_present_flag)); + + std.seq_parameter_set_id = pps.seq_parameter_set_id; + std.pic_parameter_set_id = pps.pic_parameter_set_id; + std.num_ref_idx_l0_default_active_minus1 = pps.num_ref_idx_l0_default_active_minus1; + std.num_ref_idx_l1_default_active_minus1 = pps.num_ref_idx_l1_default_active_minus1; + // Code points equal the weighted_bipred_idc values (0..2), validated above. + std.weighted_bipred_idc = u32::from(pps.weighted_bipred_idc); + std.pic_init_qp_minus26 = pps.pic_init_qp_minus26; + std.pic_init_qs_minus26 = pps.pic_init_qs_minus26; + std.chroma_qp_index_offset = pps.chroma_qp_index_offset; + std.second_chroma_qp_index_offset = pps.second_chroma_qp_index_offset; + + let scaling_backing = pps.pic_scaling_matrix_present_flag.then(|| { + // The parser resolves a PPS's 8x8 lists only under transform_8x8_mode_flag + // (7.3.2.2 reads them only then); without it the arrays are untouched zeros + // and must not be declared present. + let num_8x8 = match (pps.transform_8x8_mode_flag, pps.sps.chroma_format_idc == 3) { + (false, _) => 0, + (true, false) => 2, + (true, true) => 6, + }; + Box::new(scaling_lists_to_std( + &pps.scaling_lists_4x4, + &pps.scaling_lists_8x8, + num_8x8, + )) + }); + if let Some(backing) = &scaling_backing { + std.pScalingLists = &**backing; + } + + Ok(OwnedStdPps { + std: Box::new(std), + _scaling_backing: scaling_backing, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// An SPS exercising every mapped field with distinct values. The sixteen flags + /// follow the Std bitfield order and strictly ALTERNATE false/true, so a swap of + /// any two adjacent flag mappings fails; `vui_parameters_present_flag` is true at + /// the SOURCE precisely because the conversion must NOT copy it (VUI skip). + fn full_sps() -> Sps { + Sps { + seq_parameter_set_id: 3, + profile_idc: 100, + // Flags, in Std bit order 0..15: F T F T F T F T F T F T F T F (T). + constraint_set1_flag: true, + constraint_set3_flag: true, + constraint_set5_flag: true, + mb_adaptive_frame_field_flag: true, + delta_pic_order_always_zero_flag: true, + gaps_in_frame_num_value_allowed_flag: true, + frame_cropping_flag: true, + vui_parameters_present_flag: true, + // constraint_set0/2/4, direct_8x8_inference, frame_mbs_only, + // separate_colour_plane, qpprime_y_zero_transform_bypass and + // seq_scaling_matrix_present stay false via ..Default. + level_idc: Level::L4_1, + chroma_format_idc: 1, + bit_depth_luma_minus8: 2, + bit_depth_chroma_minus8: 3, + log2_max_frame_num_minus4: 5, + pic_order_cnt_type: 0, + log2_max_pic_order_cnt_lsb_minus4: 6, + offset_for_non_ref_pic: -7, + offset_for_top_to_bottom_field: 3, + max_num_ref_frames: 4, + pic_width_in_mbs_minus1: 119, + pic_height_in_map_units_minus1: 67, + frame_crop_left_offset: 1, + frame_crop_right_offset: 2, + frame_crop_top_offset: 3, + frame_crop_bottom_offset: 4, + ..Default::default() + } + } + + /// A PPS over `sps` exercising every mapped field with distinct values. The + /// eight flags follow the Std bitfield order and strictly ALTERNATE true/false, + /// so a swap of any two adjacent flag mappings fails. + fn full_pps(sps: Sps) -> Pps { + Pps { + pic_parameter_set_id: 5, + seq_parameter_set_id: 3, + // Flags, in Std bit order 0..7: T F T F T F T F. + transform_8x8_mode_flag: true, + redundant_pic_cnt_present_flag: false, + constrained_intra_pred_flag: true, + deblocking_filter_control_present_flag: false, + weighted_pred_flag: true, + bottom_field_pic_order_in_frame_present_flag: false, + entropy_coding_mode_flag: true, + pic_scaling_matrix_present_flag: false, + num_slice_groups_minus1: 0, + num_ref_idx_l0_default_active_minus1: 2, + num_ref_idx_l1_default_active_minus1: 1, + weighted_bipred_idc: 2, + pic_init_qp_minus26: -3, + pic_init_qs_minus26: 4, + chroma_qp_index_offset: -2, + scaling_lists_4x4: [[0; 16]; 6], + scaling_lists_8x8: [[0; 64]; 6], + second_chroma_qp_index_offset: 6, + sps: std::rc::Rc::new(sps), + } + } + + #[test] + fn every_mapped_sps_field_and_flag_round_trips_exactly() { + let sps = full_sps(); + let owned = sps_to_std(&sps).unwrap(); + let std = owned.std(); + + // The strictly alternating pattern of the fixture, bit for bit. + assert_eq!(std.flags.constraint_set0_flag(), 0); + assert_eq!(std.flags.constraint_set1_flag(), 1); + assert_eq!(std.flags.constraint_set2_flag(), 0); + assert_eq!(std.flags.constraint_set3_flag(), 1); + assert_eq!(std.flags.constraint_set4_flag(), 0); + assert_eq!(std.flags.constraint_set5_flag(), 1); + assert_eq!(std.flags.direct_8x8_inference_flag(), 0); + assert_eq!(std.flags.mb_adaptive_frame_field_flag(), 1); + assert_eq!(std.flags.frame_mbs_only_flag(), 0); + assert_eq!(std.flags.delta_pic_order_always_zero_flag(), 1); + assert_eq!(std.flags.separate_colour_plane_flag(), 0); + assert_eq!(std.flags.gaps_in_frame_num_value_allowed_flag(), 1); + assert_eq!(std.flags.qpprime_y_zero_transform_bypass_flag(), 0); + assert_eq!(std.flags.frame_cropping_flag(), 1); + assert_eq!(std.flags.seq_scaling_matrix_present_flag(), 0); + assert_eq!( + std.flags.vui_parameters_present_flag(), + 0, + "true at the source, skipped by design" + ); + + assert_eq!( + std.profile_idc, + hh::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_HIGH + ); + assert_eq!( + std.level_idc, + hh::StdVideoH264LevelIdc_STD_VIDEO_H264_LEVEL_IDC_4_1 + ); + assert_eq!( + std.chroma_format_idc, + hh::StdVideoH264ChromaFormatIdc_STD_VIDEO_H264_CHROMA_FORMAT_IDC_420 + ); + assert_eq!(std.seq_parameter_set_id, 3); + assert_eq!(std.bit_depth_luma_minus8, 2); + assert_eq!(std.bit_depth_chroma_minus8, 3, "distinct from luma"); + assert_eq!(std.log2_max_frame_num_minus4, 5); + assert_eq!( + std.pic_order_cnt_type, + hh::StdVideoH264PocType_STD_VIDEO_H264_POC_TYPE_0 + ); + assert_eq!(std.offset_for_non_ref_pic, -7); + assert_eq!(std.offset_for_top_to_bottom_field, 3); + assert_eq!(std.log2_max_pic_order_cnt_lsb_minus4, 6); + assert_eq!(std.num_ref_frames_in_pic_order_cnt_cycle, 0); + assert_eq!(std.max_num_ref_frames, 4); + assert_eq!(std.pic_width_in_mbs_minus1, 119); + assert_eq!(std.pic_height_in_map_units_minus1, 67); + assert_eq!(std.frame_crop_left_offset, 1); + assert_eq!(std.frame_crop_right_offset, 2); + assert_eq!(std.frame_crop_top_offset, 3); + assert_eq!(std.frame_crop_bottom_offset, 4); + + assert!( + std.pOffsetForRefFrame.is_null(), + "POC type 0 carries no offset array" + ); + assert!(std.pScalingLists.is_null()); + assert!(std.pSequenceParameterSetVui.is_null()); + } + + #[test] + fn poc_type_1_offsets_are_owned_and_survive_moving_the_wrapper() { + let mut sps = full_sps(); + sps.pic_order_cnt_type = 1; + sps.num_ref_frames_in_pic_order_cnt_cycle = 3; + sps.offset_for_ref_frame[0] = 2; + sps.offset_for_ref_frame[1] = -1; + sps.offset_for_ref_frame[2] = 4; + + // Box the wrapper AFTER conversion: a move that relocates the wrapper itself + // must not invalidate the pointer, because the backing is heap-pinned. + let owned = Box::new(sps_to_std(&sps).unwrap()); + let std = owned.std(); + assert_eq!( + std.pic_order_cnt_type, + hh::StdVideoH264PocType_STD_VIDEO_H264_POC_TYPE_1 + ); + assert_eq!(std.num_ref_frames_in_pic_order_cnt_cycle, 3); + assert!(!std.pOffsetForRefFrame.is_null()); + // SAFETY: pOffsetForRefFrame points into `owned`'s boxed backing of exactly + // num_ref_frames_in_pic_order_cnt_cycle i32s, alive for this whole scope. + let offsets = unsafe { std::slice::from_raw_parts(std.pOffsetForRefFrame, 3) }; + assert_eq!(offsets, [2, -1, 4]); + } + + /// Scribble over the stack the conversion's frames just used. + /// + /// The discriminator in the move tests is READING a block back, and pointer + /// equality cannot stand in for it: the Std struct carries its pointers by + /// VALUE, so a stale one is copied along with the struct and still compares + /// equal. An inlined backing therefore shows up only as wrong CONTENT — and + /// only if the dead slot has actually been reused by then. This makes that + /// certain instead of lucky: after it runs, a pointer into a dead local reads + /// back `0xA5`s rather than, by chance, its old contents. + #[inline(never)] + fn clobber_the_dead_stack() { + let mut scratch = [0xA5u8; 16 * 1024]; + std::hint::black_box(&mut scratch); + } + + /// Both wrappers may be MOVED — into the session's stored parameters, out of a + /// `Result`, into a `Vec` that later reallocates — without disturbing the + /// addresses a driver has already been given. + /// + /// Not a Rust triviality worth skipping: it is the whole reason + /// [`crate::session`] can fix its use-after-free by STORING these values + /// alongside the parameters object rather than by boxing or pinning them. It + /// holds because every backing is `Box`ed; an "optimisation" that inlined any + /// one of them as a field would keep every other test in this crate green, keep + /// compiling, and hand the driver a pointer into a moved-from stack slot. The + /// H.264 parity leg would catch it on hardware — this catches it in ordinary CI. + /// (`params_av1::moving_the_wrapper_leaves_the_driver_s_pointers_put` is the + /// same test one codec over; `params_h265`'s is the third.) + #[test] + fn moving_the_wrapper_leaves_the_driver_s_pointers_put() { + // An SPS carrying BOTH of its embedded pointers: the POC-type-1 offset + // array and the scaling lists. (The vendored `Sps` is not `Clone`, so the + // fixture is a builder rather than a value.) + let pointer_bearing_sps = || { + let mut sps = full_sps(); + sps.pic_order_cnt_type = 1; + sps.num_ref_frames_in_pic_order_cnt_cycle = 3; + sps.offset_for_ref_frame[0] = 2; + sps.offset_for_ref_frame[1] = -1; + sps.offset_for_ref_frame[2] = 4; + sps.seq_scaling_matrix_present_flag = true; + sps.scaling_lists_4x4 = std::array::from_fn(|i| [10 + i as u8; 16]); + sps + }; + // And a PPS carrying its one. + let mut pps = full_pps(pointer_bearing_sps()); + pps.pic_scaling_matrix_present_flag = true; + pps.scaling_lists_4x4 = std::array::from_fn(|i| [60 + i as u8; 16]); + + let owned_sps = sps_to_std(&pointer_bearing_sps()).expect("a High-profile SPS converts"); + let owned_pps = pps_to_std(&pps).expect("its PPS converts"); + let (offsets, sps_lists) = ( + owned_sps.std().pOffsetForRefFrame, + owned_sps.std().pScalingLists, + ); + let pps_lists = owned_pps.std().pScalingLists; + assert!(!offsets.is_null(), "POC type 1 attaches the offset array"); + assert!(!sps_lists.is_null(), "the SPS declares scaling lists"); + assert!(!pps_lists.is_null(), "so does the PPS"); + + // Every move the session's stored parameters put them through: out of the + // conversion, into a `Vec`, through a reallocation of that `Vec` as later + // Adds push more sets in, and along with the whole `StoredParams` value as + // it is installed by `mem::replace`. + let stored_sps = vec![owned_sps]; + let mut stored_pps = vec![owned_pps]; + for id in 1..crate::session::MAX_STD_PPS as u8 { + let mut more = full_pps(pointer_bearing_sps()); + more.pic_parameter_set_id = id; + stored_pps.push(pps_to_std(&more).expect("converts")); + } + assert!( + stored_pps.capacity() > 1, + "the pushes reallocated, which is the case being pinned" + ); + let stored = (stored_sps, stored_pps, 0u8); + let (stored_sps, stored_pps, _) = stored; + + assert_eq!(stored_sps[0].std().pOffsetForRefFrame, offsets); + assert_eq!(stored_sps[0].std().pScalingLists, sps_lists); + assert_eq!(stored_pps[0].std().pScalingLists, pps_lists); + + // The assertions that actually bite. Pointer equality above cannot fail — + // the Std struct carries the value, so a stale pointer is copied along with + // it — but an inlined backing leaves those pointers addressing dead locals + // in `sps_to_std`/`pps_to_std`'s returned frames, which this has just + // overwritten. + clobber_the_dead_stack(); + // They still address live blocks holding the fixture's own values, not + // stale copies. + // SAFETY: `stored_sps`/`stored_pps` are alive here and own every block. + let (read_offsets, read_sps_lists, read_pps_lists) = unsafe { + ( + std::slice::from_raw_parts(offsets, 3), + &*sps_lists, + &*pps_lists, + ) + }; + assert_eq!(read_offsets, [2, -1, 4]); + assert_eq!(read_sps_lists.ScalingList4x4[5], [15; 16]); + assert_eq!(read_pps_lists.ScalingList4x4[5], [65; 16]); + } + + #[test] + fn sps_scaling_lists_convert_when_present_and_stay_absent_when_not() { + let mut sps = full_sps(); + assert!(sps_to_std(&sps).unwrap().std().pScalingLists.is_null()); + + sps.seq_scaling_matrix_present_flag = true; + // Each list gets a DISTINCT fill byte: a permutation of lists, or an + // intra/inter reinterleave, cannot pass. + sps.scaling_lists_4x4 = std::array::from_fn(|i| [10 + i as u8; 16]); + sps.scaling_lists_8x8 = std::array::from_fn(|i| [20 + i as u8; 64]); + let owned = sps_to_std(&sps).unwrap(); + let std = owned.std(); + assert_eq!(std.flags.seq_scaling_matrix_present_flag(), 1); + assert!(!std.pScalingLists.is_null()); + // SAFETY: pScalingLists points at `owned`'s boxed StdVideoH264ScalingLists, + // alive for this whole scope. + let lists = unsafe { &*std.pScalingLists }; + // 4:2:0: bits 0-5 (the six 4x4 lists) + bits 6-7 (the two resolved 8x8 + // lists), none deferred to driver-side defaults (the parser already + // resolved them). + assert_eq!(lists.scaling_list_present_mask, 0xFF); + assert_eq!(lists.use_default_scaling_matrix_mask, 0); + for i in 0..6 { + assert_eq!(lists.ScalingList4x4[i], [10 + i as u8; 16], "4x4 list {i}"); + assert_eq!(lists.ScalingList8x8[i], [20 + i as u8; 64], "8x8 list {i}"); + } + + // 4:4:4 resolves all six 8x8 lists. + sps.chroma_format_idc = 3; + let owned = sps_to_std(&sps).unwrap(); + // SAFETY: as above — the pointer targets `owned`'s boxed backing. + let lists = unsafe { &*owned.std().pScalingLists }; + assert_eq!(lists.scaling_list_present_mask, 0xFFF); + } + + #[test] + fn every_mapped_pps_field_and_flag_round_trips_exactly() { + let pps = full_pps(full_sps()); + let owned = pps_to_std(&pps).unwrap(); + let std = owned.std(); + + // The strictly alternating pattern of the fixture, bit for bit. + assert_eq!(std.flags.transform_8x8_mode_flag(), 1); + assert_eq!(std.flags.redundant_pic_cnt_present_flag(), 0); + assert_eq!(std.flags.constrained_intra_pred_flag(), 1); + assert_eq!(std.flags.deblocking_filter_control_present_flag(), 0); + assert_eq!(std.flags.weighted_pred_flag(), 1); + assert_eq!(std.flags.bottom_field_pic_order_in_frame_present_flag(), 0); + assert_eq!(std.flags.entropy_coding_mode_flag(), 1); + assert_eq!(std.flags.pic_scaling_matrix_present_flag(), 0); + + assert_eq!(std.seq_parameter_set_id, 3); + assert_eq!(std.pic_parameter_set_id, 5); + assert_eq!(std.num_ref_idx_l0_default_active_minus1, 2); + assert_eq!(std.num_ref_idx_l1_default_active_minus1, 1); + assert_eq!( + std.weighted_bipred_idc, + hh::StdVideoH264WeightedBipredIdc_STD_VIDEO_H264_WEIGHTED_BIPRED_IDC_IMPLICIT + ); + assert_eq!(std.pic_init_qp_minus26, -3); + assert_eq!(std.pic_init_qs_minus26, 4); + assert_eq!(std.chroma_qp_index_offset, -2); + assert_eq!(std.second_chroma_qp_index_offset, 6); + assert!(std.pScalingLists.is_null()); + } + + #[test] + fn pps_scaling_lists_declare_8x8_present_only_under_transform_8x8_mode() { + let mut pps = full_pps(full_sps()); + pps.pic_scaling_matrix_present_flag = true; + // Distinct fill bytes per list, as in the SPS test. + pps.scaling_lists_4x4 = std::array::from_fn(|i| [30 + i as u8; 16]); + pps.scaling_lists_8x8 = std::array::from_fn(|i| [40 + i as u8; 64]); + + let owned = pps_to_std(&pps).unwrap(); + // SAFETY: pScalingLists points at `owned`'s boxed backing, alive here. + let lists = unsafe { &*owned.std().pScalingLists }; + assert_eq!(lists.scaling_list_present_mask, 0xFF); + assert_eq!(lists.use_default_scaling_matrix_mask, 0); + for i in 0..6 { + assert_eq!(lists.ScalingList4x4[i], [30 + i as u8; 16], "4x4 list {i}"); + assert_eq!(lists.ScalingList8x8[i], [40 + i as u8; 64], "8x8 list {i}"); + } + + // Without transform_8x8_mode the parser never resolved the 8x8 arrays: only + // the six 4x4 lists may be declared present. + pps.transform_8x8_mode_flag = false; + let owned = pps_to_std(&pps).unwrap(); + // SAFETY: as above — the pointer targets `owned`'s boxed backing. + let lists = unsafe { &*owned.std().pScalingLists }; + assert_eq!(lists.scaling_list_present_mask, 0x3F); + assert_eq!(lists.use_default_scaling_matrix_mask, 0); + } + + #[test] + fn a_stale_cycle_count_on_a_type_0_sps_converts_to_zero_offsets() { + let mut sps = full_sps(); + sps.pic_order_cnt_type = 0; + // A stale/corrupt count with no POC-type-1 semantics behind it: the Std + // struct must not claim a cycle over a null array. + sps.num_ref_frames_in_pic_order_cnt_cycle = 5; + let owned = sps_to_std(&sps).unwrap(); + assert_eq!( + owned.std().num_ref_frames_in_pic_order_cnt_cycle, + 0, + "count and pointer derive from one condition" + ); + assert!(owned.std().pOffsetForRefFrame.is_null()); + } + + #[test] + fn the_25fps_vectors_own_parameter_sets_convert_cleanly() { + use std::io::Cursor; + + use cros_codecs::codec::h264::parser::Nalu; + use cros_codecs::codec::h264::parser::NaluType; + use cros_codecs::codec::h264::parser::Parser; + + // The same vendored vector pf-bitstream's tests plan, same relative path. + const TEST_25FPS: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264" + ); + + let mut cursor = Cursor::new(TEST_25FPS); + let mut parser = Parser::default(); + let (mut sps_seen, mut pps_seen) = (false, false); + while let Ok(nalu) = Nalu::next(&mut cursor) { + match nalu.header.type_ { + NaluType::Sps if !sps_seen => { + let sps = parser.parse_sps(&nalu).expect("the vector's SPS parses"); + let owned = sps_to_std(sps).expect("the vector's SPS converts"); + let std = owned.std(); + // The vector's own goldens: 320x240 progressive Main profile. + assert_eq!( + std.profile_idc, + hh::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_MAIN + ); + assert_eq!((std.pic_width_in_mbs_minus1 + 1) * 16, 320); + assert_eq!((std.pic_height_in_map_units_minus1 + 1) * 16, 240); + assert_eq!(std.flags.frame_mbs_only_flag(), 1); + assert_eq!(std.flags.vui_parameters_present_flag(), 0); + sps_seen = true; + } + NaluType::Pps if !pps_seen => { + let pps = parser.parse_pps(&nalu).expect("the vector's PPS parses"); + let owned = pps_to_std(pps).expect("the vector's PPS converts"); + assert_eq!(owned.std().pic_parameter_set_id, 0); + pps_seen = true; + } + _ => {} + } + if sps_seen && pps_seen { + break; + } + } + assert!(sps_seen && pps_seen, "the vector opens with SPS + PPS"); + } + + #[test] + fn unrepresentable_parameter_sets_are_rejected_not_approximated() { + let mut sps = full_sps(); + sps.profile_idc = 110; // High10: no StdVideoH264ProfileIdc code point. + assert_eq!( + sps_to_std(&sps).unwrap_err(), + ParamsError::UnmappableProfileIdc(110) + ); + + let mut pps = full_pps(full_sps()); + pps.num_slice_groups_minus1 = 1; + assert_eq!(pps_to_std(&pps).unwrap_err(), ParamsError::SliceGroups(1)); + + let mut pps = full_pps(full_sps()); + pps.weighted_bipred_idc = 3; + assert_eq!( + pps_to_std(&pps).unwrap_err(), + ParamsError::InvalidWeightedBipredIdc(3) + ); + } +} diff --git a/crates/pf-vkdecode/src/params_av1.rs b/crates/pf-vkdecode/src/params_av1.rs new file mode 100644 index 00000000..1257d54c --- /dev/null +++ b/crates/pf-vkdecode/src/params_av1.rs @@ -0,0 +1,295 @@ +//! AV1 session parameters: the sequence header, converted to `StdVideoAV1SequenceHeader`. +//! +//! AV1's parameter surface is far smaller than H.264's or H.265's — there is no PPS +//! and no VPS, and `VkVideoDecodeAV1SessionParametersCreateInfoKHR` carries exactly +//! ONE sequence header. Everything else a frame needs (tiles, quantisation, +//! segmentation, loop filter, CDEF, loop restoration, global motion, film grain) +//! rides on the PICTURE info, which is why [`crate::pic_av1`] is the large half of +//! this codec and this module is the small one. +//! +//! Ownership contract as [`crate::OwnedStdSps`]: boxed backing for the two embedded +//! pointers, movable wrapper, no mutation, deliberately not `Clone`. + +use ash::vk::native as hh; +use cros_codecs::codec::av1::parser::SequenceHeaderObu; + +/// `StdVideoAV1Profile` values (`vk_video/vulkan_video_codec_av1std.h`). +pub const STD_PROFILE_MAIN: hh::StdVideoAV1Profile = 0; +pub const STD_PROFILE_HIGH: hh::StdVideoAV1Profile = 1; +pub const STD_PROFILE_PROFESSIONAL: hh::StdVideoAV1Profile = 2; + +/// Why a sequence header cannot be expressed to Vulkan. +/// +/// The last two variants are the ENVELOPE gate rather than the conversion's: +/// [`crate::caps_av1::Av1ProfileKey::from_stream`] builds the Vulkan profile from +/// the same sequence header and has to refuse the sampling/depth combinations this +/// crate has no picture format for. They live here, with the other sequence-header +/// refusals, for the reason `H265ParamsError` carries its own pair — one error type +/// per codec's parameter surface, so a caller matches on one enum. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ParamsAv1Error { + /// A profile outside the Std enumeration. + UnsupportedProfile(u8), + /// A field wider than the Std struct's type for it. + FieldOverflow { field: &'static str, value: u32 }, + /// The sequence's sampling, in H.264's `chroma_format_idc` vocabulary (the + /// planner's translation): 0 = monochrome, 2 = 4:2:2, 4 = the 4:4:0 shape no + /// AV1 profile has. None of them has a picture format in this crate. + UnsupportedChromaFormat(u8), + /// 12-bit — legal in AV1 Professional, with no output format here. + UnsupportedBitDepth(u8), +} + +impl std::fmt::Display for ParamsAv1Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParamsAv1Error::UnsupportedProfile(p) => { + write!(f, "AV1 seq_profile {p} has no Std enumerator") + } + ParamsAv1Error::FieldOverflow { field, value } => { + write!(f, "{field} = {value} does not fit its Std field") + } + ParamsAv1Error::UnsupportedChromaFormat(c) => { + write!(f, "AV1 chroma format {c} has no picture format here") + } + ParamsAv1Error::UnsupportedBitDepth(d) => { + write!(f, "{d}-bit AV1 has no picture format here") + } + } + } +} + +impl std::error::Error for ParamsAv1Error {} + +/// The converted sequence header plus the heap allocations its pointers target. +/// +/// ⚠⚠ **This must outlive the `VkVideoSessionParametersKHR` it is handed to, not +/// merely the create call.** A driver in this fleet keeps `pColorConfig` and reads +/// it at every decode; `session_av1::StoredParamsAv1` is where that is enforced and +/// where the measurement lives. Boxed backing (rather than inline arrays) is what +/// makes storing the wrapper enough — moving it does not move the blocks, which +/// `moving_the_wrapper_leaves_the_driver_s_pointers_put` pins. +/// +/// ⚠⚠ The Std struct ITSELF is boxed for the same reason, one level out: +/// `pStdSequenceHeader` is [`Self::std`]'s address, and `ensure_parameters` hands +/// it to the create call BEFORE moving the wrapper into the stored parameters. +/// Inline, that address would be a moved-from stack slot the instant the function +/// returned — the original bug's exact shape, differing only in WHICH pointer a +/// driver chose to retain (`session_av1`'s +/// `the_sequence_header_address_the_create_call_is_given_survives_being_stored`). +#[derive(Debug)] +pub struct OwnedStdAv1SequenceHeader { + std: Box, + _color_backing: Box, + /// `pTimingInfo` is null unless the stream carries timing info: a decoder needs + /// none of it, and a zeroed block behind a non-null pointer would claim a frame + /// rate the stream never stated. + _timing_backing: Option>, +} + +impl OwnedStdAv1SequenceHeader { + /// The Std struct, valid for as long as `self` lives (see [`crate::OwnedStdSps`]). + pub fn std(&self) -> &hh::StdVideoAV1SequenceHeader { + &self.std + } +} + +/// Convert one parsed sequence header. +pub fn sequence_to_std( + seq: &SequenceHeaderObu, +) -> Result { + let seq_profile = match seq.seq_profile as u8 { + 0 => STD_PROFILE_MAIN, + 1 => STD_PROFILE_HIGH, + 2 => STD_PROFILE_PROFESSIONAL, + other => return Err(ParamsAv1Error::UnsupportedProfile(other)), + }; + + let narrow = |field: &'static str, value: i64| -> Result { + u8::try_from(value).map_err(|_| ParamsAv1Error::FieldOverflow { + field, + value: value as u32, + }) + }; + + let color = &seq.color_config; + // SAFETY: StdVideoAV1ColorConfig is a plain-C bindgen struct of a bitfield word, + // small integers and enum ints; all-zero is a valid value for every field, and + // every one that matters is assigned below. + let mut color_std: hh::StdVideoAV1ColorConfig = unsafe { std::mem::zeroed() }; + color_std.flags.set_mono_chrome(color.mono_chrome.into()); + color_std.flags.set_color_range(color.color_range.into()); + color_std + .flags + .set_separate_uv_delta_q(color.separate_uv_delta_q.into()); + color_std + .flags + .set_color_description_present_flag(color.color_description_present_flag.into()); + color_std.BitDepth = if color.high_bitdepth { + if color.twelve_bit { + 12 + } else { + 10 + } + } else { + 8 + }; + color_std.subsampling_x = u8::from(color.subsampling_x); + color_std.subsampling_y = u8::from(color.subsampling_y); + color_std.color_primaries = color.color_primaries as u32; + color_std.transfer_characteristics = color.transfer_characteristics as u32; + color_std.matrix_coefficients = color.matrix_coefficients as u32; + color_std.chroma_sample_position = color.chroma_sample_position as u32; + let color_backing = Box::new(color_std); + + let timing_backing = if seq.timing_info_present_flag { + // SAFETY: as above — a bitfield word and three integers. + let mut t: hh::StdVideoAV1TimingInfo = unsafe { std::mem::zeroed() }; + t.flags + .set_equal_picture_interval(seq.timing_info.equal_picture_interval.into()); + t.num_units_in_display_tick = seq.timing_info.num_units_in_display_tick; + t.time_scale = seq.timing_info.time_scale; + t.num_ticks_per_picture_minus_1 = seq.timing_info.num_ticks_per_picture_minus_1; + Some(Box::new(t)) + } else { + None + }; + + // SAFETY: as above — a bitfield word, integers and two const pointers, both of + // which are assigned below. + let mut std: hh::StdVideoAV1SequenceHeader = unsafe { std::mem::zeroed() }; + std.flags.set_still_picture(seq.still_picture.into()); + std.flags + .set_reduced_still_picture_header(seq.reduced_still_picture_header.into()); + std.flags + .set_use_128x128_superblock(seq.use_128x128_superblock.into()); + std.flags + .set_enable_filter_intra(seq.enable_filter_intra.into()); + std.flags + .set_enable_intra_edge_filter(seq.enable_intra_edge_filter.into()); + std.flags + .set_enable_interintra_compound(seq.enable_interintra_compound.into()); + std.flags + .set_enable_masked_compound(seq.enable_masked_compound.into()); + std.flags + .set_enable_warped_motion(seq.enable_warped_motion.into()); + std.flags + .set_enable_dual_filter(seq.enable_dual_filter.into()); + std.flags + .set_enable_order_hint(seq.enable_order_hint.into()); + std.flags.set_enable_jnt_comp(seq.enable_jnt_comp.into()); + std.flags + .set_enable_ref_frame_mvs(seq.enable_ref_frame_mvs.into()); + std.flags + .set_frame_id_numbers_present_flag(seq.frame_id_numbers_present_flag.into()); + std.flags.set_enable_superres(seq.enable_superres.into()); + std.flags.set_enable_cdef(seq.enable_cdef.into()); + std.flags + .set_enable_restoration(seq.enable_restoration.into()); + std.flags + .set_film_grain_params_present(seq.film_grain_params_present.into()); + std.flags + .set_timing_info_present_flag(seq.timing_info_present_flag.into()); + std.flags + .set_initial_display_delay_present_flag(seq.initial_display_delay_present_flag.into()); + + std.seq_profile = seq_profile; + std.frame_width_bits_minus_1 = seq.frame_width_bits_minus_1; + std.frame_height_bits_minus_1 = seq.frame_height_bits_minus_1; + std.max_frame_width_minus_1 = seq.max_frame_width_minus_1; + std.max_frame_height_minus_1 = seq.max_frame_height_minus_1; + std.delta_frame_id_length_minus_2 = narrow( + "delta_frame_id_length_minus_2", + i64::from(seq.delta_frame_id_length_minus_2), + )?; + std.additional_frame_id_length_minus_1 = narrow( + "additional_frame_id_length_minus_1", + i64::from(seq.additional_frame_id_length_minus_1), + )?; + std.order_hint_bits_minus_1 = narrow( + "order_hint_bits_minus_1", + i64::from(seq.order_hint_bits_minus_1), + )?; + std.seq_force_integer_mv = narrow("seq_force_integer_mv", i64::from(seq.seq_force_integer_mv))?; + std.seq_force_screen_content_tools = narrow( + "seq_force_screen_content_tools", + i64::from(seq.seq_force_screen_content_tools), + )?; + std.pColorConfig = &*color_backing; + std.pTimingInfo = timing_backing + .as_ref() + .map_or(std::ptr::null(), |t| &**t as *const _); + + Ok(OwnedStdAv1SequenceHeader { + std: Box::new(std), + _color_backing: color_backing, + _timing_backing: timing_backing, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The wrapper may be MOVED — into the session's stored parameters, out of a + /// `Result`, into a struct literal — without disturbing the addresses a driver + /// has already been given. + /// + /// Not a Rust triviality worth skipping: it is the whole reason + /// [`crate::session_av1`] can fix its use-after-free by storing this value + /// rather than by boxing it or pinning it. It holds because the two blocks are + /// `Box`ed; an "optimisation" that inlined either one as a field would keep + /// every other test in this crate green, keep compiling, and hand the driver a + /// pointer into a moved-from stack slot. The AV1 parity leg would catch it on + /// hardware — this catches it in ordinary CI. + #[test] + fn moving_the_wrapper_leaves_the_driver_s_pointers_put() { + let seq = SequenceHeaderObu { + max_frame_width_minus_1: 319, + max_frame_height_minus_1: 239, + timing_info_present_flag: true, + ..Default::default() + }; + let owned = sequence_to_std(&seq).expect("a plain 8-bit header converts"); + let (colour, timing) = (owned.std().pColorConfig, owned.std().pTimingInfo); + assert!(!colour.is_null(), "pColorConfig is always attached"); + assert!(!timing.is_null(), "this header states timing info"); + + // Every move a session parameters object's creation puts it through. + let moved = owned; + let boxed = Box::new(moved); + let stored = (*boxed, 0u8); + let owned = stored.0; + + assert_eq!(owned.std().pColorConfig, colour); + assert_eq!(owned.std().pTimingInfo, timing); + // And they still address the wrapper's own live blocks, not stale copies. + // SAFETY: `owned` is alive here and owns both blocks. + let subsampling = unsafe { ((*colour).subsampling_x, (*colour).subsampling_y) }; + assert_eq!( + subsampling, + (0, 0), + "the fixture's colour config, read back" + ); + } + + /// A stream without timing info gets a NULL `pTimingInfo`, and that is a + /// deliberate statement rather than an omission. + /// + /// Measured on NVIDIA 610.57.04: with the backing held for the parameters + /// object's life, all 250 frames of the vendored vector are bit-identical to + /// libavcodec with this pointer NULL. libavcodec always attaches a zeroed block + /// instead; both work. Attaching one here would claim a frame rate the stream + /// never stated, so the null stays — but if a future driver refuses it, this is + /// the line to change and the sentence to delete. + #[test] + fn a_stream_without_timing_info_sends_no_timing_block() { + let seq = SequenceHeaderObu { + timing_info_present_flag: false, + ..Default::default() + }; + let owned = sequence_to_std(&seq).expect("converts"); + assert!(owned.std().pTimingInfo.is_null()); + assert_eq!(owned.std().flags.timing_info_present_flag(), 0); + } +} diff --git a/crates/pf-vkdecode/src/params_h265.rs b/crates/pf-vkdecode/src/params_h265.rs new file mode 100644 index 00000000..d96ecc24 --- /dev/null +++ b/crates/pf-vkdecode/src/params_h265.rs @@ -0,0 +1,2003 @@ +//! H.265 parameter-set conversion: the vendored parser's [`Vps`]/[`Sps`]/[`Pps`] +//! into the `StdVideoH265*ParameterSet` structs a Vulkan Video session-parameters +//! object is created from — [`crate::params`] one codec over (M3's CPU half). +//! +//! The Std structs embed raw pointers (`pProfileTierLevel`, `pDecPicBufMgr`, +//! `pScalingLists`, `pShortTermRefPicSet`, `pLongTermRefPicsSps`, ...), so +//! conversion returns OWNING wrappers — the exact aliasing/lifetime contract of +//! [`crate::OwnedStdSps`], restated on [`OwnedStdH265Sps`]. +//! +//! Deliberate skips, mirroring the H.264 module's VUI decision (a DECODE session +//! consumes neither of these — they shape display and rate conformance, not +//! reconstruction): +//! +//! - VUI: `vui_parameters_present_flag` stays 0 and `pSequenceParameterSetVui` +//! stays null. Colour rides [`pf_bitstream::h265::PicturePlan::colour`] into the +//! presenter, per picture, exactly as H.264 does it. +//! - HRD/timing: `vps_timing_info_present_flag` stays 0 and `pHrdParameters` +//! stays null. HRD is buffer-conformance machinery; no decode operation reads it. +//! +//! Short-term RPS candidates are re-encoded in RESOLVED form: the vendored parser +//! has already run the 7.4.8 inter-RPS prediction (equations 7-59..7-66) and +//! stores every SPS candidate as absolute `DeltaPocS0`/`DeltaPocS1` arrays, so +//! each set is declared non-predicted with those derived values encoded back into +//! `delta_poc_sX_minus1` syntax. Equivalent by construction — 7-65/7-66 IS the +//! definition of a set's content, and both the direct and the predicted syntax +//! derive the same arrays — and it is the same "the parser already resolved it" +//! idiom the H.264 module applies to scaling lists. A slice-inline RPS that +//! predicts from an SPS candidate still derives identically in hardware: the +//! derivation consumes only the source set's DeltaPoc arrays and counts, all +//! preserved here (`NumDeltaPocsOfRefRpsIdx` rides the picture info, see +//! [`crate::pic_h265`]). + +use ash::vk::native as hh; +pub use cros_codecs::codec::h265::parser::Pps; +use cros_codecs::codec::h265::parser::ProfileTierLevel; +use cros_codecs::codec::h265::parser::ScalingLists; +use cros_codecs::codec::h265::parser::ShortTermRefPicSet; +pub use cros_codecs::codec::h265::parser::Sps; +pub use cros_codecs::codec::h265::parser::Vps; +use pf_bitstream::h265::Level; + +/// A parameter set that cannot be represented as a StdVideo struct, or that sits +/// outside the punktfunk H.265 decode envelope (4:2:0 and 4:4:4 at 8 or 10 bits, +/// from encoders we control). Hitting one is a stream-integrity failure, not a +/// feature gap — reject-with-error rather than submit a half-truth to a driver. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum H265ParamsError { + /// `general_profile_idc` has no `StdVideoH265ProfileIdc` code point. Vulkan + /// defines Main (1), Main 10 (2), Main Still Picture (3) and Format Range + /// Extensions (4); High Throughput/SCC/scalable profiles land here. + UnmappableProfileIdc(u8), + /// `chroma_format_idc` past 3 — not legal H.265 to begin with. + InvalidChromaFormatIdc(u8), + /// 4:2:2 or monochrome: legal H.265, but no punktfunk host emits it and the + /// client has no output-format plumbing for it — outside the envelope. + UnsupportedChromaFormat(u8), + /// 4:4:4 with `separate_colour_plane_flag`: ChromaArrayType 0 in disguise + /// (three monochrome-coded planes) — no output-format plumbing, and most + /// decode hardware refuses it. + SeparateColourPlanes, + /// Bit depth beyond 8/10, or luma and chroma depths that disagree: no + /// punktfunk output format (NV12/P010 and their 4:4:4 counterparts) can carry + /// it — outside the envelope. + UnsupportedBitDepth { luma_minus8: u8, chroma_minus8: u8 }, + /// SCC palette predictor initializers: `pPredictorPaletteEntries` is the one + /// Std pointer this conversion does not populate (punktfunk hosts emit no SCC + /// coding), and a present-flag over a null pointer would be a half-truth. + PalettePredictorInitializers, + /// `num_short_term_ref_pic_sets` past the spec's 64 (7.4.3.2.1). + TooManyShortTermRpsSets(u8), + /// The SPS declares more short-term RPS candidates than the parser resolved — + /// a corrupt table this conversion refuses to pad. + MissingShortTermRps { index: usize }, + /// An RPS candidate holds more entries on one side than the Std struct's + /// 16-element arrays (7.4.8 bounds both sides by the DPB size, itself <= 16). + RpsEntryOverflow { + set: usize, + negative: u8, + positive: u8, + }, + /// An RPS candidate's derived `DeltaPocS0`/`DeltaPocS1` array is not strictly + /// monotonic (or a step exceeds the 7.4.8 bound of 2^15), so it cannot be + /// re-encoded as `delta_poc_sX_minus1` syntax. Unreachable off the vendored + /// parser; guards directly-constructed inputs. + NonMonotonicRps { set: usize }, + /// `num_long_term_ref_pics_sps` past the spec's 32 (7.4.3.2.1). + TooManyLongTermSpsPics(u8), + /// A syntax value overflows the (narrower) Std field that carries it — e.g. a + /// tile column width past `u16`. The parser bounds everything it reads, so + /// this guards hostile or directly-constructed inputs. + FieldOverflow { field: &'static str, value: i64 }, +} + +impl std::fmt::Display for H265ParamsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + H265ParamsError::UnmappableProfileIdc(idc) => { + write!( + f, + "general_profile_idc {idc} has no StdVideoH265ProfileIdc code point" + ) + } + H265ParamsError::SeparateColourPlanes => { + write!( + f, + "4:4:4 with separate_colour_plane_flag (ChromaArrayType 0) is \ + outside the punktfunk decode envelope" + ) + } + H265ParamsError::InvalidChromaFormatIdc(idc) => { + write!(f, "invalid chroma_format_idc {idc}") + } + H265ParamsError::UnsupportedChromaFormat(idc) => { + write!( + f, + "chroma_format_idc {idc} is outside the punktfunk decode envelope \ + (4:2:0 and 4:4:4 only)" + ) + } + H265ParamsError::UnsupportedBitDepth { + luma_minus8, + chroma_minus8, + } => { + write!( + f, + "bit depth {}/{} is outside the punktfunk decode envelope \ + (8- and 10-bit, luma == chroma)", + luma_minus8 + 8, + chroma_minus8 + 8 + ) + } + H265ParamsError::PalettePredictorInitializers => { + write!( + f, + "SCC palette predictor initializers are not expressible by this conversion" + ) + } + H265ParamsError::TooManyShortTermRpsSets(n) => { + write!(f, "{n} short-term RPS candidates exceed the spec's 64") + } + H265ParamsError::MissingShortTermRps { index } => { + write!(f, "short-term RPS candidate {index} was never resolved") + } + H265ParamsError::RpsEntryOverflow { + set, + negative, + positive, + } => { + write!( + f, + "short-term RPS candidate {set} holds {negative} negative / {positive} \ + positive entries; the Std arrays hold 16 per side" + ) + } + H265ParamsError::NonMonotonicRps { set } => { + write!( + f, + "short-term RPS candidate {set} has a non-monotonic DeltaPoc array" + ) + } + H265ParamsError::TooManyLongTermSpsPics(n) => { + write!(f, "{n} long-term SPS candidates exceed the spec's 32") + } + H265ParamsError::FieldOverflow { field, value } => { + write!(f, "{field} value {value} overflows its Std field") + } + } + } +} + +impl std::error::Error for H265ParamsError {} + +/// Checked narrowing into a Std field: the parser bounds everything it reads, so +/// an overflow here means hostile or directly-constructed input — fail closed, +/// never truncate (a truncated tile width would decode as garbage, silently). +fn narrow(field: &'static str, value: S) -> Result +where + D: TryFrom, + S: Copy + Into, +{ + D::try_from(value).map_err(|_| H265ParamsError::FieldOverflow { + field, + value: value.into(), + }) +} + +/// The converted VPS plus the heap allocations its embedded pointers target. +/// Ownership contract as [`crate::OwnedStdSps`]: boxed backing, movable wrapper, +/// no mutation, deliberately not `Clone` (re-convert instead). +#[derive(Debug)] +pub struct OwnedStdH265Vps { + /// Boxed so [`Self::std`]'s ADDRESS — what `pStdVPSs` points at — survives every + /// move of the wrapper ([`crate::OwnedStdSps`]). + std: Box, + _ptl_backing: Box, + _dpb_backing: Box, +} + +impl OwnedStdH265Vps { + /// The Std struct, valid for as long as `self` lives (do not let a `Copy` of + /// it outlive the wrapper — see [`crate::OwnedStdSps`]). + pub fn std(&self) -> &hh::StdVideoH265VideoParameterSet { + &self.std + } +} + +/// The converted SPS plus the heap allocations its embedded pointers target. +/// +/// Same ownership contract as [`crate::OwnedStdSps`], with five potential +/// pointers: the profile/tier/level and DPB-manager blocks are always present, +/// scaling lists / short-term RPS candidates / long-term SPS candidates only when +/// the stream carries them. `pSequenceParameterSetVui` and +/// `pPredictorPaletteEntries` are null by design (module docs; palette data is +/// rejected, not dropped). +#[derive(Debug)] +pub struct OwnedStdH265Sps { + /// Boxed for [`OwnedStdH265Vps`]'s reason: `pStdSPSs` is this field's address. + std: Box, + _ptl_backing: Box, + _dpb_backing: Box, + _scaling_backing: Option>, + /// `pShortTermRefPicSet`'s target: `num_short_term_ref_pic_sets` entries. + _st_rps_backing: Option>, + _lt_backing: Option>, +} + +impl OwnedStdH265Sps { + /// The Std struct, valid for as long as `self` lives (see [`crate::OwnedStdSps`]). + pub fn std(&self) -> &hh::StdVideoH265SequenceParameterSet { + &self.std + } +} + +/// The converted PPS plus the scaling-list allocation its `pScalingLists` +/// targets. Same ownership contract as [`crate::OwnedStdSps`]. +#[derive(Debug)] +pub struct OwnedStdH265Pps { + /// Boxed for [`OwnedStdH265Vps`]'s reason: `pStdPPSs` is this field's address. + std: Box, + _scaling_backing: Option>, +} + +impl OwnedStdH265Pps { + /// The Std struct, valid for as long as `self` lives (see [`crate::OwnedStdSps`]). + pub fn std(&self) -> &hh::StdVideoH265PictureParameterSet { + &self.std + } +} + +/// H.265 `general_level_idc` (value-coded: 30 x the level number, Table A.8) to +/// Vulkan's index-coded `StdVideoH265LevelIdc`. The Std code points ascend with +/// the level, so a driver's `maxLevelIdc` gate compares them numerically. +pub(crate) const fn level_to_std(level: Level) -> hh::StdVideoH265LevelIdc { + match level { + Level::L1 => hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_1_0, + Level::L2 => hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_2_0, + Level::L2_1 => hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_2_1, + Level::L3 => hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_3_0, + Level::L3_1 => hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_3_1, + Level::L4 => hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_4_0, + Level::L4_1 => hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_4_1, + Level::L5 => hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_5_0, + Level::L5_1 => hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_5_1, + Level::L5_2 => hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_5_2, + Level::L6 => hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_6_0, + Level::L6_1 => hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_6_1, + Level::L6_2 => hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_6_2, + } +} + +/// `general_profile_idc` to `StdVideoH265ProfileIdc` — the code points equal the +/// profile_idc values they name, so recognised ones pass through. +/// +/// (Visible to the crate because the GPU half's profile key +/// [`crate::caps_h265::H265ProfileKey`] must be built from the PICTURE's profile +/// idc before any parameter-set conversion runs — the caps query needs it — and +/// both must agree on the mapping.) +pub(crate) fn profile_to_std(idc: u8) -> Result { + match u32::from(idc) { + p @ 1..=4 => Ok(p), + _ => Err(H265ParamsError::UnmappableProfileIdc(idc)), + } +} + +/// profile_tier_level() to the Std block (always pointer-backed in VPS and SPS). +fn ptl_to_std(ptl: &ProfileTierLevel) -> Result { + // SAFETY: StdVideoH265ProfileTierLevel is a plain-C bindgen struct of a + // bitfield word and two enum ints; all-zero is a valid value for every field. + let mut std: hh::StdVideoH265ProfileTierLevel = unsafe { std::mem::zeroed() }; + std.flags + .set_general_tier_flag(u32::from(ptl.general_tier_flag)); + std.flags + .set_general_progressive_source_flag(u32::from(ptl.general_progressive_source_flag)); + std.flags + .set_general_interlaced_source_flag(u32::from(ptl.general_interlaced_source_flag)); + std.flags + .set_general_non_packed_constraint_flag(u32::from(ptl.general_non_packed_constraint_flag)); + std.flags + .set_general_frame_only_constraint_flag(u32::from(ptl.general_frame_only_constraint_flag)); + std.general_profile_idc = profile_to_std(ptl.general_profile_idc)?; + std.general_level_idc = level_to_std(ptl.general_level_idc); + Ok(std) +} + +/// Pack the parser's scaling lists into the Std layout. +/// +/// The vendored parser has already run 7.4.5 in full — explicit coefficients, +/// matrix-id prediction (equation 7-42) and the Table 7-5/7-6 defaults — so the +/// arrays hold the fully RESOLVED lists (the H.264 module's idiom). Layout notes: +/// +/// - 32x32 (sizeId 3) has exactly TWO lists, at parser matrix ids 0 (intra) and +/// 3 (inter) — 7.4.5's loop steps matrixId by 3 there. The Std array holds them +/// compacted at indices 0 and 1. +/// - The Std DC fields carry the VALUE (`scaling_list_dc_coef_minus8 + 8`, +/// 1..255), not the minus8 syntax element. +fn scaling_lists_to_std( + lists: &ScalingLists, +) -> Result { + // SAFETY: StdVideoH265ScalingLists is a plain-C bindgen struct of byte + // arrays; all-zero is a valid value for every field. + let mut std: hh::StdVideoH265ScalingLists = unsafe { std::mem::zeroed() }; + std.ScalingList4x4 = lists.scaling_list_4x4; + std.ScalingList8x8 = lists.scaling_list_8x8; + std.ScalingList16x16 = lists.scaling_list_16x16; + std.ScalingList32x32 = [lists.scaling_list_32x32[0], lists.scaling_list_32x32[3]]; + for i in 0..6 { + std.ScalingListDCCoef16x16[i] = narrow( + "scaling_list_dc_coef_minus8_16x16 + 8", + i32::from(lists.scaling_list_dc_coef_minus8_16x16[i]) + 8, + )?; + } + for (dst, src) in [0usize, 3].into_iter().enumerate() { + std.ScalingListDCCoef32x32[dst] = narrow( + "scaling_list_dc_coef_minus8_32x32 + 8", + i32::from(lists.scaling_list_dc_coef_minus8_32x32[src]) + 8, + )?; + } + Ok(std) +} + +/// One resolved SPS short-term RPS candidate re-encoded as a non-predicted Std +/// set (module docs: the parser flattened 7.4.8's prediction, so the prediction +/// flags stay 0 and the derived `DeltaPocSX` arrays encode back into +/// `delta_poc_sX_minus1` syntax — `DeltaPocS0` is strictly decreasing negative, +/// `DeltaPocS1` strictly increasing positive, so both step differences are the +/// positive minus1+1 values). +fn st_rps_to_std( + index: usize, + set: &ShortTermRefPicSet, +) -> Result { + let negative = usize::from(set.num_negative_pics); + let positive = usize::from(set.num_positive_pics); + if negative > 16 || positive > 16 { + return Err(H265ParamsError::RpsEntryOverflow { + set: index, + negative: set.num_negative_pics, + positive: set.num_positive_pics, + }); + } + + // SAFETY: StdVideoH265ShortTermRefPicSet is a plain-C bindgen struct of a + // bitfield word, integers and integer arrays; all-zero is valid for every + // field and is exactly the non-predicted baseline (prediction flags 0). + let mut std: hh::StdVideoH265ShortTermRefPicSet = unsafe { std::mem::zeroed() }; + std.num_negative_pics = set.num_negative_pics; + std.num_positive_pics = set.num_positive_pics; + + // 7.4.8: the syntax steps are ue(v)-bounded at 2^15 - 1, so a legal step is + // 1..=2^15; anything else cannot be re-encoded (fn docs). + let mut prev: i64 = 0; + for i in 0..negative { + let cur = i64::from(set.delta_poc_s0[i]); + let step = prev - cur; + if !(1..=32768).contains(&step) { + return Err(H265ParamsError::NonMonotonicRps { set: index }); + } + std.delta_poc_s0_minus1[i] = (step - 1) as u16; + if set.used_by_curr_pic_s0[i] { + std.used_by_curr_pic_s0_flag |= 1 << i; + } + prev = cur; + } + let mut prev: i64 = 0; + for i in 0..positive { + let cur = i64::from(set.delta_poc_s1[i]); + let step = cur - prev; + if !(1..=32768).contains(&step) { + return Err(H265ParamsError::NonMonotonicRps { set: index }); + } + std.delta_poc_s1_minus1[i] = (step - 1) as u16; + if set.used_by_curr_pic_s1[i] { + std.used_by_curr_pic_s1_flag |= 1 << i; + } + prev = cur; + } + Ok(std) +} + +/// The envelope + representability gate every conversion path shares: profile, +/// chroma format and bit depth (struct docs on the error type for the WHY of +/// each). Runs before any allocation so a rejection is cheap and total. +fn check_envelope(sps: &Sps) -> Result<(), H265ParamsError> { + profile_to_std(sps.profile_tier_level.general_profile_idc)?; + if sps.chroma_format_idc > 3 { + return Err(H265ParamsError::InvalidChromaFormatIdc( + sps.chroma_format_idc, + )); + } + if sps.chroma_format_idc == 0 || sps.chroma_format_idc == 2 { + return Err(H265ParamsError::UnsupportedChromaFormat( + sps.chroma_format_idc, + )); + } + // 4:4:4 with separate colour planes is ChromaArrayType 0 in disguise — three + // monochrome-coded planes. No punktfunk output format can carry it and most + // decode hardware refuses it; letting it through would fail later at the + // driver (or worse, map planes wrongly) — the silent class this envelope + // exists to catch. + if sps.chroma_format_idc == 3 && sps.separate_colour_plane_flag { + return Err(H265ParamsError::SeparateColourPlanes); + } + if sps.bit_depth_luma_minus8 != sps.bit_depth_chroma_minus8 + || !matches!(sps.bit_depth_luma_minus8, 0 | 2) + { + return Err(H265ParamsError::UnsupportedBitDepth { + luma_minus8: sps.bit_depth_luma_minus8, + chroma_minus8: sps.bit_depth_chroma_minus8, + }); + } + if sps + .scc_extension + .palette_predictor_initializers_present_flag + { + return Err(H265ParamsError::PalettePredictorInitializers); + } + Ok(()) +} + +/// Convert one VPS into the Std struct (owning wrapper). HRD/timing is skipped by +/// design (module docs); the DPB manager and profile/tier/level blocks ride +/// behind owned pointers. +pub fn vps_to_std_h265(vps: &Vps) -> Result { + let ptl_backing = Box::new(ptl_to_std(&vps.profile_tier_level)?); + + // SAFETY: StdVideoH265DecPicBufMgr is a plain-C bindgen struct of integer + // arrays; all-zero is a valid value for every field. + let mut dpb: hh::StdVideoH265DecPicBufMgr = unsafe { std::mem::zeroed() }; + dpb.max_latency_increase_plus1 = vps.max_latency_increase_plus1; + for i in 0..7 { + // The VPS arrays are u32 in the parser; the spec bounds both syntax + // elements well inside u8 (MaxDpbSize <= 16), so an overflow is corrupt. + dpb.max_dec_pic_buffering_minus1[i] = narrow( + "vps_max_dec_pic_buffering_minus1", + vps.max_dec_pic_buffering_minus1[i], + )?; + dpb.max_num_reorder_pics[i] = + narrow("vps_max_num_reorder_pics", vps.max_num_reorder_pics[i])?; + } + let dpb_backing = Box::new(dpb); + + // SAFETY: StdVideoH265VideoParameterSet is a plain-C bindgen struct of a + // bitfield word, integers and const pointers; all-zero is valid for every + // field (null pointers) and is the baseline the writes below fill. + let mut std: hh::StdVideoH265VideoParameterSet = unsafe { std::mem::zeroed() }; + std.flags + .set_vps_temporal_id_nesting_flag(u32::from(vps.temporal_id_nesting_flag)); + std.flags + .set_vps_sub_layer_ordering_info_present_flag(u32::from( + vps.sub_layer_ordering_info_present_flag, + )); + // vps_timing_info_present_flag and vps_poc_proportional_to_timing_flag stay + // 0 with their fields: the HRD/timing skip (module docs) must be + // self-consistent — a present-flag over a null pHrdParameters would be the + // exact half-truth this module exists to avoid. + std.vps_video_parameter_set_id = vps.video_parameter_set_id; + std.vps_max_sub_layers_minus1 = vps.max_sub_layers_minus1; + std.pDecPicBufMgr = &*dpb_backing; + std.pProfileTierLevel = &*ptl_backing; + + Ok(OwnedStdH265Vps { + std: Box::new(std), + _ptl_backing: ptl_backing, + _dpb_backing: dpb_backing, + }) +} + +/// A minimal Std VPS synthesized from the SPS that references it, for streams +/// whose VPS NALU was lost upstream (the parser attaches the VPS to the SPS only +/// when it saw one — `sps.vps` is `None` otherwise). Every stream is REQUIRED to +/// carry a VPS (7.4.2.1), and Vulkan requires the parameters object to hold the +/// VPS the SPS names, so the session layer needs SOMETHING to add; this fallback +/// carries exactly the facts the SPS restates (ids, sub-layer count, +/// profile/tier/level, DPB sizing) — which is also everything a decode session +/// could consult, since the VPS's own additions (timing, layer sets) are all in +/// the deliberate-skip category (module docs). +pub fn fallback_vps_from_sps(sps: &Sps) -> Result { + let ptl_backing = Box::new(ptl_to_std(&sps.profile_tier_level)?); + let dpb_backing = Box::new(sps_dec_pic_buf_mgr(sps)); + + // SAFETY: as in vps_to_std_h265 — all-zero is a valid baseline. + let mut std: hh::StdVideoH265VideoParameterSet = unsafe { std::mem::zeroed() }; + std.vps_video_parameter_set_id = sps.video_parameter_set_id; + std.vps_max_sub_layers_minus1 = sps.max_sub_layers_minus1; + std.flags + .set_vps_temporal_id_nesting_flag(u32::from(sps.temporal_id_nesting_flag)); + std.flags + .set_vps_sub_layer_ordering_info_present_flag(u32::from( + sps.sub_layer_ordering_info_present_flag, + )); + std.pDecPicBufMgr = &*dpb_backing; + std.pProfileTierLevel = &*ptl_backing; + + Ok(OwnedStdH265Vps { + std: Box::new(std), + _ptl_backing: ptl_backing, + _dpb_backing: dpb_backing, + }) +} + +/// The SPS's sub-layer ordering arrays as the Std DPB-manager block. Infallible: +/// the parser's SPS arrays are already `u8` where the Std block wants `u8`. +fn sps_dec_pic_buf_mgr(sps: &Sps) -> hh::StdVideoH265DecPicBufMgr { + // SAFETY: plain-C bindgen struct of integer arrays; all-zero is valid. + let mut dpb: hh::StdVideoH265DecPicBufMgr = unsafe { std::mem::zeroed() }; + for i in 0..7 { + dpb.max_latency_increase_plus1[i] = u32::from(sps.max_latency_increase_plus1[i]); + } + dpb.max_dec_pic_buffering_minus1 = sps.max_dec_pic_buffering_minus1; + dpb.max_num_reorder_pics = sps.max_num_reorder_pics; + dpb +} + +/// Convert one SPS into the Std struct (owning wrapper), mapping every field the +/// H.265 decode profile consumes. VUI is skipped by design; SCC palette data is +/// rejected, never dropped (module docs). +pub fn sps_to_std_h265(sps: &Sps) -> Result { + check_envelope(sps)?; + if sps.num_short_term_ref_pic_sets > 64 { + return Err(H265ParamsError::TooManyShortTermRpsSets( + sps.num_short_term_ref_pic_sets, + )); + } + if sps.num_long_term_ref_pics_sps > 32 { + return Err(H265ParamsError::TooManyLongTermSpsPics( + sps.num_long_term_ref_pics_sps, + )); + } + + let ptl_backing = Box::new(ptl_to_std(&sps.profile_tier_level)?); + let dpb_backing = Box::new(sps_dec_pic_buf_mgr(sps)); + + let scaling_backing = sps + .scaling_list_data_present_flag + .then(|| scaling_lists_to_std(&sps.scaling_list).map(Box::new)) + .transpose()?; + + // The COUNT field and the pointer derive from the one condition (the H.264 + // module's stale-count rule): a declared candidate count only ever rides + // over a real array of exactly that many converted sets. + let st_rps_backing = (sps.num_short_term_ref_pic_sets > 0) + .then( + || -> Result, H265ParamsError> { + let count = usize::from(sps.num_short_term_ref_pic_sets); + let mut sets = Vec::with_capacity(count); + for index in 0..count { + let set = sps + .short_term_ref_pic_set + .get(index) + .ok_or(H265ParamsError::MissingShortTermRps { index })?; + sets.push(st_rps_to_std(index, set)?); + } + Ok(sets.into_boxed_slice()) + }, + ) + .transpose()?; + + // Backed whenever the FLAG is set, even with zero SPS candidates: the header + // annotates `pLongTermRefPicsSps` "must be a valid pointer if + // long_term_ref_pics_present_flag is set", and FFmpeg's vulkan_hevc passes it + // unconditionally — a set flag over a null pointer is untested territory in + // every driver. `flag=1, num=0` is not a corner case here: it is exactly the + // punktfunk LTR/RFI-recovery stream shape (slice-signalled long-term pics, + // no SPS candidates — pf-bitstream's own LTR synthesizer emits it), so the + // all-zero struct (the correct content for num=0) must be present. + let lt_backing = sps.long_term_ref_pics_present_flag.then(|| { + // SAFETY: plain-C bindgen struct of a mask and an integer array; + // all-zero is valid. + let mut lt: hh::StdVideoH265LongTermRefPicsSps = unsafe { std::mem::zeroed() }; + for i in 0..usize::from(sps.num_long_term_ref_pics_sps) { + if sps.used_by_curr_pic_lt_sps_flag[i] { + lt.used_by_curr_pic_lt_sps_flag |= 1 << i; + } + } + lt.lt_ref_pic_poc_lsb_sps = sps.lt_ref_pic_poc_lsb_sps; + Box::new(lt) + }); + + // SAFETY: StdVideoH265SequenceParameterSet is a plain-C bindgen struct of a + // bitfield word, integers and const pointers; all-zero is valid for every + // field (null pointers) and is the "everything absent" baseline the field + // writes below build on. Same idiom as the H.264 module. + let mut std: hh::StdVideoH265SequenceParameterSet = unsafe { std::mem::zeroed() }; + + std.flags + .set_sps_temporal_id_nesting_flag(u32::from(sps.temporal_id_nesting_flag)); + std.flags + .set_separate_colour_plane_flag(u32::from(sps.separate_colour_plane_flag)); + std.flags + .set_conformance_window_flag(u32::from(sps.conformance_window_flag)); + std.flags + .set_sps_sub_layer_ordering_info_present_flag(u32::from( + sps.sub_layer_ordering_info_present_flag, + )); + std.flags + .set_scaling_list_enabled_flag(u32::from(sps.scaling_list_enabled_flag)); + // When enabled-but-absent, the driver applies the Table 7-5/7-6 defaults + // itself (7.4.5's inference) — declaring data we did not convert would be + // wrong in exactly the way the null-pointer/flag pairing rules forbid. + std.flags + .set_sps_scaling_list_data_present_flag(u32::from(sps.scaling_list_data_present_flag)); + std.flags + .set_amp_enabled_flag(u32::from(sps.amp_enabled_flag)); + std.flags.set_sample_adaptive_offset_enabled_flag(u32::from( + sps.sample_adaptive_offset_enabled_flag, + )); + std.flags + .set_pcm_enabled_flag(u32::from(sps.pcm_enabled_flag)); + std.flags + .set_pcm_loop_filter_disabled_flag(u32::from(sps.pcm_loop_filter_disabled_flag)); + std.flags + .set_long_term_ref_pics_present_flag(u32::from(sps.long_term_ref_pics_present_flag)); + std.flags + .set_sps_temporal_mvp_enabled_flag(u32::from(sps.temporal_mvp_enabled_flag)); + std.flags.set_strong_intra_smoothing_enabled_flag(u32::from( + sps.strong_intra_smoothing_enabled_flag, + )); + // vui_parameters_present_flag stays 0: decode sessions consume no VUI + // (module docs); colour rides the PicturePlan. + std.flags + .set_sps_extension_present_flag(u32::from(sps.extension_present_flag)); + std.flags + .set_sps_range_extension_flag(u32::from(sps.range_extension_flag)); + let rext = &sps.range_extension; + std.flags + .set_transform_skip_rotation_enabled_flag(u32::from( + rext.transform_skip_rotation_enabled_flag, + )); + std.flags.set_transform_skip_context_enabled_flag(u32::from( + rext.transform_skip_context_enabled_flag, + )); + std.flags + .set_implicit_rdpcm_enabled_flag(u32::from(rext.implicit_rdpcm_enabled_flag)); + std.flags + .set_explicit_rdpcm_enabled_flag(u32::from(rext.explicit_rdpcm_enabled_flag)); + std.flags + .set_extended_precision_processing_flag(u32::from(rext.extended_precision_processing_flag)); + std.flags + .set_intra_smoothing_disabled_flag(u32::from(rext.intra_smoothing_disabled_flag)); + std.flags.set_high_precision_offsets_enabled_flag(u32::from( + rext.high_precision_offsets_enabled_flag, + )); + std.flags + .set_persistent_rice_adaptation_enabled_flag(u32::from( + rext.persistent_rice_adaptation_enabled_flag, + )); + std.flags.set_cabac_bypass_alignment_enabled_flag(u32::from( + rext.cabac_bypass_alignment_enabled_flag, + )); + let scc = &sps.scc_extension; + std.flags + .set_sps_scc_extension_flag(u32::from(sps.scc_extension_flag)); + // Faithful even though the planner's envelope gate rejects SCC + // self-referencing before a plan exists — conversion is not envelope-coupled + // beyond its own representability (the H.264 frame_mbs_only precedent). + std.flags + .set_sps_curr_pic_ref_enabled_flag(u32::from(scc.curr_pic_ref_enabled_flag)); + std.flags + .set_palette_mode_enabled_flag(u32::from(scc.palette_mode_enabled_flag)); + // sps_palette_predictor_initializers_present_flag stays 0: check_envelope + // rejected any SPS that sets it, so flag and (null) pPredictorPaletteEntries + // can never disagree. + std.flags + .set_intra_boundary_filtering_disabled_flag(u32::from( + scc.intra_boundary_filtering_disabled_flag, + )); + + // Chroma format code points equal the chroma_format_idc values (0..3). + std.chroma_format_idc = u32::from(sps.chroma_format_idc); + std.pic_width_in_luma_samples = u32::from(sps.pic_width_in_luma_samples); + std.pic_height_in_luma_samples = u32::from(sps.pic_height_in_luma_samples); + std.sps_video_parameter_set_id = sps.video_parameter_set_id; + std.sps_max_sub_layers_minus1 = sps.max_sub_layers_minus1; + std.sps_seq_parameter_set_id = sps.seq_parameter_set_id; + std.bit_depth_luma_minus8 = sps.bit_depth_luma_minus8; + std.bit_depth_chroma_minus8 = sps.bit_depth_chroma_minus8; + std.log2_max_pic_order_cnt_lsb_minus4 = sps.log2_max_pic_order_cnt_lsb_minus4; + std.log2_min_luma_coding_block_size_minus3 = sps.log2_min_luma_coding_block_size_minus3; + std.log2_diff_max_min_luma_coding_block_size = sps.log2_diff_max_min_luma_coding_block_size; + std.log2_min_luma_transform_block_size_minus2 = sps.log2_min_luma_transform_block_size_minus2; + std.log2_diff_max_min_luma_transform_block_size = + sps.log2_diff_max_min_luma_transform_block_size; + std.max_transform_hierarchy_depth_inter = sps.max_transform_hierarchy_depth_inter; + std.max_transform_hierarchy_depth_intra = sps.max_transform_hierarchy_depth_intra; + std.num_short_term_ref_pic_sets = sps.num_short_term_ref_pic_sets; + std.num_long_term_ref_pics_sps = sps.num_long_term_ref_pics_sps; + std.pcm_sample_bit_depth_luma_minus1 = sps.pcm_sample_bit_depth_luma_minus1; + std.pcm_sample_bit_depth_chroma_minus1 = sps.pcm_sample_bit_depth_chroma_minus1; + std.log2_min_pcm_luma_coding_block_size_minus3 = sps.log2_min_pcm_luma_coding_block_size_minus3; + std.log2_diff_max_min_pcm_luma_coding_block_size = + sps.log2_diff_max_min_pcm_luma_coding_block_size; + std.palette_max_size = scc.palette_max_size; + std.delta_palette_max_predictor_size = scc.delta_palette_max_predictor_size; + std.motion_vector_resolution_control_idc = scc.motion_vector_resolution_control_idc; + std.sps_num_palette_predictor_initializers_minus1 = + scc.num_palette_predictor_initializer_minus1; + std.conf_win_left_offset = sps.conf_win_left_offset; + std.conf_win_right_offset = sps.conf_win_right_offset; + std.conf_win_top_offset = sps.conf_win_top_offset; + std.conf_win_bottom_offset = sps.conf_win_bottom_offset; + + std.pProfileTierLevel = &*ptl_backing; + std.pDecPicBufMgr = &*dpb_backing; + if let Some(backing) = &scaling_backing { + std.pScalingLists = &**backing; + } + if let Some(backing) = &st_rps_backing { + std.pShortTermRefPicSet = backing.as_ptr(); + } + if let Some(backing) = <_backing { + std.pLongTermRefPicsSps = &**backing; + } + // pSequenceParameterSetVui and pPredictorPaletteEntries stay null (module + // docs / check_envelope). + + Ok(OwnedStdH265Sps { + std: Box::new(std), + _ptl_backing: ptl_backing, + _dpb_backing: dpb_backing, + _scaling_backing: scaling_backing, + _st_rps_backing: st_rps_backing, + _lt_backing: lt_backing, + }) +} + +/// Convert one PPS into the Std struct (owning wrapper), mapping every field the +/// H.265 decode profile consumes. SCC palette data is rejected, never dropped; +/// every narrower Std field is checked, never truncated. +pub fn pps_to_std_h265(pps: &Pps) -> Result { + if pps + .scc_extension + .palette_predictor_initializers_present_flag + { + return Err(H265ParamsError::PalettePredictorInitializers); + } + + let scaling_backing = pps + .scaling_list_data_present_flag + .then(|| scaling_lists_to_std(&pps.scaling_list).map(Box::new)) + .transpose()?; + + // SAFETY: StdVideoH265PictureParameterSet is a plain-C bindgen struct of a + // bitfield word, integers, integer arrays and const pointers; all-zero is + // valid for every field (null pointers) and is the baseline the writes fill. + let mut std: hh::StdVideoH265PictureParameterSet = unsafe { std::mem::zeroed() }; + + std.flags + .set_dependent_slice_segments_enabled_flag(u32::from( + pps.dependent_slice_segments_enabled_flag, + )); + std.flags + .set_output_flag_present_flag(u32::from(pps.output_flag_present_flag)); + std.flags + .set_sign_data_hiding_enabled_flag(u32::from(pps.sign_data_hiding_enabled_flag)); + std.flags + .set_cabac_init_present_flag(u32::from(pps.cabac_init_present_flag)); + std.flags + .set_constrained_intra_pred_flag(u32::from(pps.constrained_intra_pred_flag)); + std.flags + .set_transform_skip_enabled_flag(u32::from(pps.transform_skip_enabled_flag)); + std.flags + .set_cu_qp_delta_enabled_flag(u32::from(pps.cu_qp_delta_enabled_flag)); + std.flags + .set_pps_slice_chroma_qp_offsets_present_flag(u32::from( + pps.slice_chroma_qp_offsets_present_flag, + )); + std.flags + .set_weighted_pred_flag(u32::from(pps.weighted_pred_flag)); + std.flags + .set_weighted_bipred_flag(u32::from(pps.weighted_bipred_flag)); + std.flags + .set_transquant_bypass_enabled_flag(u32::from(pps.transquant_bypass_enabled_flag)); + std.flags + .set_tiles_enabled_flag(u32::from(pps.tiles_enabled_flag)); + std.flags + .set_entropy_coding_sync_enabled_flag(u32::from(pps.entropy_coding_sync_enabled_flag)); + std.flags + .set_uniform_spacing_flag(u32::from(pps.uniform_spacing_flag)); + std.flags + .set_loop_filter_across_tiles_enabled_flag(u32::from( + pps.loop_filter_across_tiles_enabled_flag, + )); + std.flags + .set_pps_loop_filter_across_slices_enabled_flag(u32::from( + pps.loop_filter_across_slices_enabled_flag, + )); + std.flags + .set_deblocking_filter_control_present_flag(u32::from( + pps.deblocking_filter_control_present_flag, + )); + std.flags + .set_deblocking_filter_override_enabled_flag(u32::from( + pps.deblocking_filter_override_enabled_flag, + )); + std.flags + .set_pps_deblocking_filter_disabled_flag(u32::from(pps.deblocking_filter_disabled_flag)); + std.flags + .set_pps_scaling_list_data_present_flag(u32::from(pps.scaling_list_data_present_flag)); + std.flags + .set_lists_modification_present_flag(u32::from(pps.lists_modification_present_flag)); + std.flags + .set_slice_segment_header_extension_present_flag(u32::from( + pps.slice_segment_header_extension_present_flag, + )); + std.flags + .set_pps_extension_present_flag(u32::from(pps.extension_present_flag)); + let rext = &pps.range_extension; + std.flags + .set_cross_component_prediction_enabled_flag(u32::from( + rext.cross_component_prediction_enabled_flag, + )); + std.flags + .set_chroma_qp_offset_list_enabled_flag(u32::from(rext.chroma_qp_offset_list_enabled_flag)); + let scc = &pps.scc_extension; + std.flags + .set_pps_curr_pic_ref_enabled_flag(u32::from(scc.curr_pic_ref_enabled_flag)); + std.flags + .set_residual_adaptive_colour_transform_enabled_flag(u32::from( + scc.residual_adaptive_colour_transform_enabled_flag, + )); + std.flags + .set_pps_slice_act_qp_offsets_present_flag(u32::from( + scc.slice_act_qp_offsets_present_flag, + )); + // pps_palette_predictor_initializers_present_flag stays 0 (rejected above). + std.flags + .set_monochrome_palette_flag(u32::from(scc.monochrome_palette_flag)); + std.flags + .set_pps_range_extension_flag(u32::from(pps.range_extension_flag)); + + std.pps_pic_parameter_set_id = pps.pic_parameter_set_id; + std.pps_seq_parameter_set_id = pps.seq_parameter_set_id; + // The VPS id the Std PPS names is the one its OWN SPS references — the + // parser resolved that chain at parse time. + std.sps_video_parameter_set_id = pps.sps.video_parameter_set_id; + std.num_extra_slice_header_bits = pps.num_extra_slice_header_bits; + std.num_ref_idx_l0_default_active_minus1 = pps.num_ref_idx_l0_default_active_minus1; + std.num_ref_idx_l1_default_active_minus1 = pps.num_ref_idx_l1_default_active_minus1; + std.init_qp_minus26 = pps.init_qp_minus26; + std.diff_cu_qp_delta_depth = pps.diff_cu_qp_delta_depth; + std.pps_cb_qp_offset = pps.cb_qp_offset; + std.pps_cr_qp_offset = pps.cr_qp_offset; + std.pps_beta_offset_div2 = pps.beta_offset_div2; + std.pps_tc_offset_div2 = pps.tc_offset_div2; + std.log2_parallel_merge_level_minus2 = pps.log2_parallel_merge_level_minus2; + std.log2_max_transform_skip_block_size_minus2 = narrow( + "log2_max_transform_skip_block_size_minus2", + rext.log2_max_transform_skip_block_size_minus2, + )?; + std.diff_cu_chroma_qp_offset_depth = narrow( + "diff_cu_chroma_qp_offset_depth", + rext.diff_cu_chroma_qp_offset_depth, + )?; + std.chroma_qp_offset_list_len_minus1 = narrow( + "chroma_qp_offset_list_len_minus1", + rext.chroma_qp_offset_list_len_minus1, + )?; + for i in 0..6 { + std.cb_qp_offset_list[i] = narrow("cb_qp_offset_list", rext.cb_qp_offset_list[i])?; + std.cr_qp_offset_list[i] = narrow("cr_qp_offset_list", rext.cr_qp_offset_list[i])?; + } + std.log2_sao_offset_scale_luma = narrow( + "log2_sao_offset_scale_luma", + rext.log2_sao_offset_scale_luma, + )?; + std.log2_sao_offset_scale_chroma = narrow( + "log2_sao_offset_scale_chroma", + rext.log2_sao_offset_scale_chroma, + )?; + std.pps_act_y_qp_offset_plus5 = scc.act_y_qp_offset_plus5; + std.pps_act_cb_qp_offset_plus5 = scc.act_cb_qp_offset_plus5; + std.pps_act_cr_qp_offset_plus3 = scc.act_cr_qp_offset_plus3; + std.pps_num_palette_predictor_initializers = scc.num_palette_predictor_initializers; + std.luma_bit_depth_entry_minus8 = scc.luma_bit_depth_entry_minus8; + std.chroma_bit_depth_entry_minus8 = scc.chroma_bit_depth_entry_minus8; + std.num_tile_columns_minus1 = pps.num_tile_columns_minus1; + std.num_tile_rows_minus1 = pps.num_tile_rows_minus1; + for i in 0..19 { + std.column_width_minus1[i] = narrow("column_width_minus1", pps.column_width_minus1[i])?; + } + for i in 0..21 { + std.row_height_minus1[i] = narrow("row_height_minus1", pps.row_height_minus1[i])?; + } + + if let Some(backing) = &scaling_backing { + std.pScalingLists = &**backing; + } + // pPredictorPaletteEntries stays null (rejected above). + + Ok(OwnedStdH265Pps { + std: Box::new(std), + _scaling_backing: scaling_backing, + }) +} + +#[cfg(test)] +mod tests { + use cros_codecs::codec::h265::parser::PpsRangeExtension; + use cros_codecs::codec::h265::parser::PpsSccExtension; + use cros_codecs::codec::h265::parser::ProfileTierLevel; + use cros_codecs::codec::h265::parser::SpsRangeExtension; + use std::rc::Rc; + + use super::*; + + /// An SPS exercising every mapped field with distinct values. Flags carry a + /// deliberate mixed pattern asserted bit-for-bit below; + /// `vui_parameters_present_flag` is true at the SOURCE precisely because the + /// conversion must NOT copy it (the VUI skip). + fn full_sps() -> Sps { + Sps { + video_parameter_set_id: 2, + max_sub_layers_minus1: 1, + temporal_id_nesting_flag: true, + profile_tier_level: ProfileTierLevel { + general_profile_idc: 2, // Main 10 + general_tier_flag: true, + general_progressive_source_flag: true, + general_interlaced_source_flag: false, + general_non_packed_constraint_flag: true, + general_frame_only_constraint_flag: false, + general_level_idc: Level::L4_1, + ..Default::default() + }, + seq_parameter_set_id: 5, + chroma_format_idc: 1, + pic_width_in_luma_samples: 1920, + pic_height_in_luma_samples: 1080, + conformance_window_flag: true, + conf_win_left_offset: 1, + conf_win_right_offset: 2, + conf_win_top_offset: 3, + conf_win_bottom_offset: 4, + bit_depth_luma_minus8: 2, + bit_depth_chroma_minus8: 2, + log2_max_pic_order_cnt_lsb_minus4: 6, + sub_layer_ordering_info_present_flag: true, + max_dec_pic_buffering_minus1: [5, 6, 0, 0, 0, 0, 0], + max_num_reorder_pics: [1, 2, 0, 0, 0, 0, 0], + max_latency_increase_plus1: [7, 8, 0, 0, 0, 0, 0], + log2_min_luma_coding_block_size_minus3: 1, + log2_diff_max_min_luma_coding_block_size: 2, + log2_min_luma_transform_block_size_minus2: 1, + log2_diff_max_min_luma_transform_block_size: 3, + max_transform_hierarchy_depth_inter: 2, + max_transform_hierarchy_depth_intra: 3, + scaling_list_enabled_flag: true, + scaling_list_data_present_flag: false, + amp_enabled_flag: true, + sample_adaptive_offset_enabled_flag: false, + pcm_enabled_flag: true, + pcm_sample_bit_depth_luma_minus1: 7, + pcm_sample_bit_depth_chroma_minus1: 9, + log2_min_pcm_luma_coding_block_size_minus3: 1, + log2_diff_max_min_pcm_luma_coding_block_size: 2, + pcm_loop_filter_disabled_flag: true, + num_short_term_ref_pic_sets: 0, + long_term_ref_pics_present_flag: false, + temporal_mvp_enabled_flag: true, + strong_intra_smoothing_enabled_flag: false, + vui_parameters_present_flag: true, + extension_present_flag: true, + range_extension_flag: true, + range_extension: SpsRangeExtension { + transform_skip_rotation_enabled_flag: true, + transform_skip_context_enabled_flag: false, + implicit_rdpcm_enabled_flag: true, + explicit_rdpcm_enabled_flag: false, + extended_precision_processing_flag: true, + intra_smoothing_disabled_flag: false, + high_precision_offsets_enabled_flag: true, + persistent_rice_adaptation_enabled_flag: false, + cabac_bypass_alignment_enabled_flag: true, + }, + ..Default::default() + } + } + + /// A PPS over `sps` exercising every mapped field with distinct values — + /// the vendored `Pps` derives no `Default`, so the literal is spelled out + /// once here (the h264 `full_pps` idiom). + fn full_pps(sps: Sps) -> Pps { + Pps { + pic_parameter_set_id: 3, + seq_parameter_set_id: 5, + dependent_slice_segments_enabled_flag: true, + output_flag_present_flag: false, + num_extra_slice_header_bits: 2, + sign_data_hiding_enabled_flag: true, + cabac_init_present_flag: false, + num_ref_idx_l0_default_active_minus1: 2, + num_ref_idx_l1_default_active_minus1: 1, + init_qp_minus26: -3, + constrained_intra_pred_flag: true, + transform_skip_enabled_flag: false, + cu_qp_delta_enabled_flag: true, + diff_cu_qp_delta_depth: 2, + cb_qp_offset: -4, + cr_qp_offset: 5, + slice_chroma_qp_offsets_present_flag: false, + weighted_pred_flag: true, + weighted_bipred_flag: false, + transquant_bypass_enabled_flag: true, + tiles_enabled_flag: true, + entropy_coding_sync_enabled_flag: false, + num_tile_columns_minus1: 1, + num_tile_rows_minus1: 2, + uniform_spacing_flag: false, + column_width_minus1: { + let mut w = [0u32; 19]; + w[0] = 17; + w[1] = 12; + w + }, + row_height_minus1: { + let mut h = [0u32; 21]; + h[0] = 9; + h[1] = 8; + h[2] = 16; + h + }, + loop_filter_across_tiles_enabled_flag: true, + loop_filter_across_slices_enabled_flag: false, + deblocking_filter_control_present_flag: true, + deblocking_filter_override_enabled_flag: false, + deblocking_filter_disabled_flag: true, + beta_offset_div2: -2, + tc_offset_div2: 3, + scaling_list_data_present_flag: false, + scaling_list: Default::default(), + lists_modification_present_flag: true, + log2_parallel_merge_level_minus2: 1, + slice_segment_header_extension_present_flag: false, + extension_present_flag: true, + range_extension_flag: true, + range_extension: PpsRangeExtension { + log2_max_transform_skip_block_size_minus2: 2, + cross_component_prediction_enabled_flag: true, + chroma_qp_offset_list_enabled_flag: true, + diff_cu_chroma_qp_offset_depth: 1, + chroma_qp_offset_list_len_minus1: 1, + cb_qp_offset_list: [1, -2, 0, 0, 0, 0], + cr_qp_offset_list: [-3, 4, 0, 0, 0, 0], + log2_sao_offset_scale_luma: 1, + log2_sao_offset_scale_chroma: 2, + }, + scc_extension_flag: false, + scc_extension: PpsSccExtension::default(), + qp_bd_offset_y: 0, + sps: Rc::new(sps), + } + } + + #[test] + fn every_mapped_sps_field_and_flag_round_trips_exactly() { + let sps = full_sps(); + let owned = sps_to_std_h265(&sps).unwrap(); + let std = owned.std(); + + // The fixture's mixed pattern, bit for bit. + assert_eq!(std.flags.sps_temporal_id_nesting_flag(), 1); + assert_eq!(std.flags.separate_colour_plane_flag(), 0); + assert_eq!(std.flags.conformance_window_flag(), 1); + assert_eq!(std.flags.sps_sub_layer_ordering_info_present_flag(), 1); + assert_eq!(std.flags.scaling_list_enabled_flag(), 1); + assert_eq!(std.flags.sps_scaling_list_data_present_flag(), 0); + assert_eq!(std.flags.amp_enabled_flag(), 1); + assert_eq!(std.flags.sample_adaptive_offset_enabled_flag(), 0); + assert_eq!(std.flags.pcm_enabled_flag(), 1); + assert_eq!(std.flags.pcm_loop_filter_disabled_flag(), 1); + assert_eq!(std.flags.long_term_ref_pics_present_flag(), 0); + assert_eq!(std.flags.sps_temporal_mvp_enabled_flag(), 1); + assert_eq!(std.flags.strong_intra_smoothing_enabled_flag(), 0); + assert_eq!( + std.flags.vui_parameters_present_flag(), + 0, + "true at the source, skipped by design" + ); + assert_eq!(std.flags.sps_extension_present_flag(), 1); + assert_eq!(std.flags.sps_range_extension_flag(), 1); + // The range-extension flags, alternating T/F per the fixture. + assert_eq!(std.flags.transform_skip_rotation_enabled_flag(), 1); + assert_eq!(std.flags.transform_skip_context_enabled_flag(), 0); + assert_eq!(std.flags.implicit_rdpcm_enabled_flag(), 1); + assert_eq!(std.flags.explicit_rdpcm_enabled_flag(), 0); + assert_eq!(std.flags.extended_precision_processing_flag(), 1); + assert_eq!(std.flags.intra_smoothing_disabled_flag(), 0); + assert_eq!(std.flags.high_precision_offsets_enabled_flag(), 1); + assert_eq!(std.flags.persistent_rice_adaptation_enabled_flag(), 0); + assert_eq!(std.flags.cabac_bypass_alignment_enabled_flag(), 1); + assert_eq!(std.flags.sps_scc_extension_flag(), 0); + assert_eq!(std.flags.sps_curr_pic_ref_enabled_flag(), 0); + assert_eq!(std.flags.palette_mode_enabled_flag(), 0); + assert_eq!( + std.flags.sps_palette_predictor_initializers_present_flag(), + 0 + ); + assert_eq!(std.flags.intra_boundary_filtering_disabled_flag(), 0); + + assert_eq!( + std.chroma_format_idc, + hh::StdVideoH265ChromaFormatIdc_STD_VIDEO_H265_CHROMA_FORMAT_IDC_420 + ); + assert_eq!(std.pic_width_in_luma_samples, 1920); + assert_eq!(std.pic_height_in_luma_samples, 1080); + assert_eq!(std.sps_video_parameter_set_id, 2); + assert_eq!(std.sps_max_sub_layers_minus1, 1); + assert_eq!(std.sps_seq_parameter_set_id, 5); + assert_eq!(std.bit_depth_luma_minus8, 2); + assert_eq!(std.bit_depth_chroma_minus8, 2); + assert_eq!(std.log2_max_pic_order_cnt_lsb_minus4, 6); + assert_eq!(std.log2_min_luma_coding_block_size_minus3, 1); + assert_eq!(std.log2_diff_max_min_luma_coding_block_size, 2); + assert_eq!(std.log2_min_luma_transform_block_size_minus2, 1); + assert_eq!(std.log2_diff_max_min_luma_transform_block_size, 3); + assert_eq!(std.max_transform_hierarchy_depth_inter, 2); + assert_eq!(std.max_transform_hierarchy_depth_intra, 3); + assert_eq!(std.num_short_term_ref_pic_sets, 0); + assert_eq!(std.num_long_term_ref_pics_sps, 0); + assert_eq!(std.pcm_sample_bit_depth_luma_minus1, 7); + assert_eq!( + std.pcm_sample_bit_depth_chroma_minus1, 9, + "distinct from luma" + ); + assert_eq!(std.log2_min_pcm_luma_coding_block_size_minus3, 1); + assert_eq!(std.log2_diff_max_min_pcm_luma_coding_block_size, 2); + assert_eq!(std.conf_win_left_offset, 1); + assert_eq!(std.conf_win_right_offset, 2); + assert_eq!(std.conf_win_top_offset, 3); + assert_eq!(std.conf_win_bottom_offset, 4); + + // The always-present pointer-backed blocks. + assert!(!std.pProfileTierLevel.is_null()); + // SAFETY: pProfileTierLevel targets `owned`'s boxed backing, alive here. + let ptl = unsafe { &*std.pProfileTierLevel }; + assert_eq!(ptl.flags.general_tier_flag(), 1); + assert_eq!(ptl.flags.general_progressive_source_flag(), 1); + assert_eq!(ptl.flags.general_interlaced_source_flag(), 0); + assert_eq!(ptl.flags.general_non_packed_constraint_flag(), 1); + assert_eq!(ptl.flags.general_frame_only_constraint_flag(), 0); + assert_eq!( + ptl.general_profile_idc, + hh::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_MAIN_10 + ); + assert_eq!( + ptl.general_level_idc, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_4_1 + ); + assert!(!std.pDecPicBufMgr.is_null()); + // SAFETY: pDecPicBufMgr targets `owned`'s boxed backing, alive here. + let dpb = unsafe { &*std.pDecPicBufMgr }; + assert_eq!(&dpb.max_dec_pic_buffering_minus1[..2], &[5, 6]); + assert_eq!(&dpb.max_num_reorder_pics[..2], &[1, 2]); + assert_eq!(&dpb.max_latency_increase_plus1[..2], &[7, 8]); + + // The absent-by-content and absent-by-design pointers. + assert!( + std.pScalingLists.is_null(), + "enabled but data-absent: driver defaults" + ); + assert!(std.pShortTermRefPicSet.is_null()); + assert!(std.pLongTermRefPicsSps.is_null()); + assert!(std.pSequenceParameterSetVui.is_null()); + assert!(std.pPredictorPaletteEntries.is_null()); + } + + #[test] + fn profile_and_level_code_points_map_or_reject() { + for (idc, expect) in [ + ( + 1u8, + hh::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_MAIN, + ), + ( + 2, + hh::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_MAIN_10, + ), + ( + 3, + hh::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_MAIN_STILL_PICTURE, + ), + ( + 4, + hh::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_FORMAT_RANGE_EXTENSIONS, + ), + ] { + assert_eq!(profile_to_std(idc).unwrap(), expect); + } + // High Throughput (5) and SCC (9) exist on the wire but not in Vulkan. + for idc in [0u8, 5, 9, 11] { + assert_eq!( + profile_to_std(idc).unwrap_err(), + H265ParamsError::UnmappableProfileIdc(idc) + ); + } + + // Every Table A.8 level maps to its ascending index-coded point. + let pairs = [ + ( + Level::L1, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_1_0, + ), + ( + Level::L2, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_2_0, + ), + ( + Level::L2_1, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_2_1, + ), + ( + Level::L3, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_3_0, + ), + ( + Level::L3_1, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_3_1, + ), + ( + Level::L4, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_4_0, + ), + ( + Level::L4_1, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_4_1, + ), + ( + Level::L5, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_5_0, + ), + ( + Level::L5_1, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_5_1, + ), + ( + Level::L5_2, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_5_2, + ), + ( + Level::L6, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_6_0, + ), + ( + Level::L6_1, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_6_1, + ), + ( + Level::L6_2, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_6_2, + ), + ]; + let mut prev = None; + for (level, expect) in pairs { + assert_eq!(level_to_std(level), expect); + if let Some(prev) = prev { + assert!(expect > prev, "code points must ascend for the caps gate"); + } + prev = Some(expect); + } + } + + #[test] + fn the_owned_backings_survive_moving_the_wrapper() { + // Box the wrapper AFTER conversion: a move relocating the wrapper itself + // must not invalidate its pointers, because the backing is heap-pinned + // (the h264 POC-offset test, one codec over). + let owned = Box::new(sps_to_std_h265(&full_sps()).unwrap()); + let std = owned.std(); + // SAFETY: both pointers target `owned`'s boxed backings, alive in scope. + let (ptl, dpb) = unsafe { (&*std.pProfileTierLevel, &*std.pDecPicBufMgr) }; + assert_eq!( + ptl.general_profile_idc, + hh::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_MAIN_10 + ); + assert_eq!(dpb.max_dec_pic_buffering_minus1[0], 5); + } + + /// Scribble over the stack the conversion's frames just used. + /// + /// The discriminator in the move tests is READING a block back, and pointer + /// equality cannot stand in for it: the Std struct carries its pointers by + /// VALUE, so a stale one is copied along with the struct and still compares + /// equal. An inlined backing therefore shows up only as wrong CONTENT — and + /// only if the dead slot has actually been reused by then. This makes that + /// certain instead of lucky: after it runs, a pointer into a dead local reads + /// back `0xA5`s rather than, by chance, its old contents. + #[inline(never)] + fn clobber_the_dead_stack() { + let mut scratch = [0xA5u8; 16 * 1024]; + std::hint::black_box(&mut scratch); + } + + /// All three wrappers may be MOVED — into the session's stored parameters, out + /// of a `Result`, into a `Vec` that later reallocates — without disturbing the + /// addresses a driver has already been given. + /// + /// Not a Rust triviality worth skipping: it is the whole reason + /// [`crate::session_h265`] can fix its use-after-free by STORING these values + /// alongside the parameters object rather than by boxing or pinning them. It + /// holds because every backing is `Box`ed; an "optimisation" that inlined any + /// one of them as a field would keep every other test in this crate green, keep + /// compiling, and hand the driver a pointer into a moved-from stack slot. The + /// H.265 and Main 10 parity legs would catch it on hardware — this catches it in + /// ordinary CI. H.265 has the most surface of the three codecs: seven pointers + /// across the SPS alone, and this exercises every one a stream can populate. + /// (`params_av1::moving_the_wrapper_leaves_the_driver_s_pointers_put` and + /// `params::`'s are the same test one codec over.) + #[test] + fn moving_the_wrapper_leaves_the_driver_s_pointers_put() { + // An SPS carrying every embedded pointer it can: profile/tier/level and + // DPB manager (always), plus scaling lists, short-term RPS candidates and + // long-term SPS candidates. + let mut sps = full_sps(); + sps.scaling_list_data_present_flag = true; + sps.scaling_list.scaling_list_4x4 = std::array::from_fn(|i| [10 + i as u8; 16]); + sps.num_short_term_ref_pic_sets = 1; + let mut st = ShortTermRefPicSet { + num_negative_pics: 1, + ..Default::default() + }; + st.delta_poc_s0[0] = -1; + st.used_by_curr_pic_s0[0] = true; + sps.short_term_ref_pic_set = vec![st]; + sps.long_term_ref_pics_present_flag = true; + sps.num_long_term_ref_pics_sps = 1; + sps.lt_ref_pic_poc_lsb_sps[0] = 11; + sps.used_by_curr_pic_lt_sps_flag[0] = true; + // A PPS carrying its one, and a VPS carrying its two. + let mut pps = full_pps(sps.clone()); + pps.scaling_list_data_present_flag = true; + pps.scaling_list.scaling_list_4x4 = std::array::from_fn(|i| [60 + i as u8; 16]); + let vps = Vps { + video_parameter_set_id: 2, + max_sub_layers_minus1: 1, + profile_tier_level: full_sps().profile_tier_level, + // Distinct from the SPS's [5, 6, …] so a read-back names which block + // it came from rather than merely that some block was readable. + max_dec_pic_buffering_minus1: [3, 4, 0, 0, 0, 0, 0], + ..Default::default() + }; + + let owned_vps = vps_to_std_h265(&vps).expect("the VPS converts"); + let owned_sps = sps_to_std_h265(&sps).expect("a Main 10 SPS converts"); + let owned_pps = pps_to_std_h265(&pps).expect("its PPS converts"); + let vps_ptrs = ( + owned_vps.std().pProfileTierLevel, + owned_vps.std().pDecPicBufMgr, + ); + let sps_ptrs = ( + owned_sps.std().pProfileTierLevel, + owned_sps.std().pDecPicBufMgr, + owned_sps.std().pScalingLists, + owned_sps.std().pShortTermRefPicSet, + owned_sps.std().pLongTermRefPicsSps, + ); + let pps_lists = owned_pps.std().pScalingLists; + for (what, ptr) in [ + ("vps pProfileTierLevel", vps_ptrs.0.cast::<()>()), + ("vps pDecPicBufMgr", vps_ptrs.1.cast()), + ("sps pProfileTierLevel", sps_ptrs.0.cast()), + ("sps pDecPicBufMgr", sps_ptrs.1.cast()), + ("sps pScalingLists", sps_ptrs.2.cast()), + ("sps pShortTermRefPicSet", sps_ptrs.3.cast()), + ("sps pLongTermRefPicsSps", sps_ptrs.4.cast()), + ("pps pScalingLists", pps_lists.cast()), + ] { + assert!(!ptr.is_null(), "{what} is attached by this fixture"); + } + + // Every move the session's stored parameters put them through: out of the + // conversion, into a `Vec`, through a reallocation of that `Vec` as later + // Adds push more sets in, and along with the whole `StoredParamsH265` value + // as it is installed by `mem::replace`. + let stored_vps = vec![owned_vps]; + let stored_sps = vec![owned_sps]; + let mut stored_pps = vec![owned_pps]; + for id in 1..crate::session_h265::MAX_STD_PPS as u8 { + let mut more = full_pps(sps.clone()); + more.pic_parameter_set_id = id; + stored_pps.push(pps_to_std_h265(&more).expect("converts")); + } + assert!( + stored_pps.capacity() > 1, + "the pushes reallocated, which is the case being pinned" + ); + let stored = (stored_vps, stored_sps, stored_pps, 0u8); + let (stored_vps, stored_sps, stored_pps, _) = stored; + + let moved_vps = stored_vps[0].std(); + assert_eq!( + (moved_vps.pProfileTierLevel, moved_vps.pDecPicBufMgr), + vps_ptrs + ); + let moved_sps = stored_sps[0].std(); + assert_eq!( + ( + moved_sps.pProfileTierLevel, + moved_sps.pDecPicBufMgr, + moved_sps.pScalingLists, + moved_sps.pShortTermRefPicSet, + moved_sps.pLongTermRefPicsSps, + ), + sps_ptrs + ); + assert_eq!(stored_pps[0].std().pScalingLists, pps_lists); + + // The assertions that actually bite. Pointer equality above cannot fail — + // the Std struct carries the value, so a stale pointer is copied along with + // it — but an inlined backing leaves those pointers addressing dead locals + // in the conversions' returned frames, which this has just overwritten. + clobber_the_dead_stack(); + // They still address live blocks holding the fixture's own values, not + // stale copies. + // Every one of the eight, so no single backing can be inlined without this + // failing — an earlier draft read only six and let exactly that through. + // SAFETY: `stored_*` are alive here and own every one of these blocks. + let (vps_ptl, vps_dpb) = unsafe { (&*vps_ptrs.0, &*vps_ptrs.1) }; + // SAFETY: as above. + let (sps_ptl, sps_dpb, sps_scaling, sps_st, sps_lt) = unsafe { + ( + &*sps_ptrs.0, + &*sps_ptrs.1, + &*sps_ptrs.2, + &*sps_ptrs.3, + &*sps_ptrs.4, + ) + }; + // SAFETY: as above. + let pps_scaling = unsafe { &*pps_lists }; + assert_eq!( + vps_ptl.general_level_idc, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_4_1, + "vps pProfileTierLevel" + ); + assert_eq!( + &vps_dpb.max_dec_pic_buffering_minus1[..2], + &[3, 4], + "vps pDecPicBufMgr" + ); + assert_eq!( + sps_ptl.general_level_idc, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_4_1, + "sps pProfileTierLevel" + ); + assert_eq!( + &sps_dpb.max_dec_pic_buffering_minus1[..2], + &[5, 6], + "sps pDecPicBufMgr" + ); + assert_eq!(sps_scaling.ScalingList4x4[5], [15; 16], "sps pScalingLists"); + assert_eq!(sps_st.num_negative_pics, 1, "sps pShortTermRefPicSet"); + assert_eq!( + sps_lt.lt_ref_pic_poc_lsb_sps[0], 11, + "sps pLongTermRefPicsSps" + ); + assert_eq!(pps_scaling.ScalingList4x4[5], [65; 16], "pps pScalingLists"); + } + + #[test] + fn sps_scaling_lists_convert_verbatim_including_the_32x32_pair_and_dc_values() { + let mut sps = full_sps(); + sps.scaling_list_data_present_flag = true; + // Distinct fill bytes per list; the 32x32 lists live at PARSER matrix + // ids 0 and 3 (7.4.5 steps matrixId by 3 at sizeId 3) and must land at + // Std indices 0 and 1. + sps.scaling_list.scaling_list_4x4 = std::array::from_fn(|i| [10 + i as u8; 16]); + sps.scaling_list.scaling_list_8x8 = std::array::from_fn(|i| [20 + i as u8; 64]); + sps.scaling_list.scaling_list_16x16 = std::array::from_fn(|i| [30 + i as u8; 64]); + sps.scaling_list.scaling_list_32x32 = std::array::from_fn(|i| [40 + i as u8; 64]); + sps.scaling_list.scaling_list_dc_coef_minus8_16x16 = [-7, 0, 8, 100, 200, 247]; + sps.scaling_list.scaling_list_dc_coef_minus8_32x32 = [42, 0, 0, 99, 0, 0]; + + let owned = sps_to_std_h265(&sps).unwrap(); + let std = owned.std(); + assert_eq!(std.flags.sps_scaling_list_data_present_flag(), 1); + assert!(!std.pScalingLists.is_null()); + // SAFETY: pScalingLists targets `owned`'s boxed backing, alive here. + let lists = unsafe { &*std.pScalingLists }; + for i in 0..6 { + assert_eq!(lists.ScalingList4x4[i], [10 + i as u8; 16], "4x4 list {i}"); + assert_eq!(lists.ScalingList8x8[i], [20 + i as u8; 64], "8x8 list {i}"); + assert_eq!( + lists.ScalingList16x16[i], + [30 + i as u8; 64], + "16x16 list {i}" + ); + } + assert_eq!( + lists.ScalingList32x32[0], [40; 64], + "intra = parser index 0" + ); + assert_eq!( + lists.ScalingList32x32[1], [43; 64], + "inter = parser index 3" + ); + // The Std DC fields carry the +8 VALUE, not the minus8 syntax element. + assert_eq!(lists.ScalingListDCCoef16x16, [1, 8, 16, 108, 208, 255]); + assert_eq!(lists.ScalingListDCCoef32x32, [50, 107]); + } + + #[test] + fn short_term_rps_candidates_reencode_to_the_std_syntax_exactly() { + let mut sps = full_sps(); + sps.num_short_term_ref_pic_sets = 2; + // Candidate 0: DeltaPocS0 = [-1, -3] (steps 1, 2), DeltaPocS1 = [2] + // (step 2), with mixed used_by flags. + let mut set0 = ShortTermRefPicSet { + num_negative_pics: 2, + num_positive_pics: 1, + ..Default::default() + }; + set0.delta_poc_s0[0] = -1; + set0.delta_poc_s0[1] = -3; + set0.used_by_curr_pic_s0[0] = true; + set0.used_by_curr_pic_s0[1] = false; + set0.delta_poc_s1[0] = 2; + set0.used_by_curr_pic_s1[0] = true; + // Candidate 1: as the parser leaves a PREDICTED set — resolved arrays + // with the prediction syntax still recorded. The conversion must emit + // the resolved non-predicted form, ignoring the prediction fields. + let mut set1 = ShortTermRefPicSet { + inter_ref_pic_set_prediction_flag: true, + delta_idx_minus1: 0, + abs_delta_rps_minus1: 0, + num_negative_pics: 1, + ..Default::default() + }; + set1.delta_poc_s0[0] = -2; + set1.used_by_curr_pic_s0[0] = true; + sps.short_term_ref_pic_set = vec![set0, set1]; + + let owned = sps_to_std_h265(&sps).unwrap(); + let std = owned.std(); + assert_eq!(std.num_short_term_ref_pic_sets, 2); + assert!(!std.pShortTermRefPicSet.is_null()); + // SAFETY: pShortTermRefPicSet targets `owned`'s boxed slice of exactly + // num_short_term_ref_pic_sets entries, alive for this whole scope. + let sets = unsafe { std::slice::from_raw_parts(std.pShortTermRefPicSet, 2) }; + + assert_eq!(sets[0].num_negative_pics, 2); + assert_eq!(sets[0].num_positive_pics, 1); + assert_eq!( + &sets[0].delta_poc_s0_minus1[..2], + &[0, 1], + "steps 1 and 2, minus 1" + ); + assert_eq!(sets[0].delta_poc_s1_minus1[0], 1, "step 2, minus 1"); + assert_eq!(sets[0].used_by_curr_pic_s0_flag, 0b01); + assert_eq!(sets[0].used_by_curr_pic_s1_flag, 0b1); + + assert_eq!( + sets[1].flags.inter_ref_pic_set_prediction_flag(), + 0, + "resolved form: the prediction is flattened, never re-declared" + ); + assert_eq!(sets[1].delta_idx_minus1, 0); + assert_eq!(sets[1].num_negative_pics, 1); + assert_eq!(sets[1].delta_poc_s0_minus1[0], 1); + } + + #[test] + fn long_term_sps_candidates_ride_with_mask_and_poc_lsbs() { + let mut sps = full_sps(); + sps.long_term_ref_pics_present_flag = true; + sps.num_long_term_ref_pics_sps = 3; + sps.lt_ref_pic_poc_lsb_sps[0] = 11; + sps.lt_ref_pic_poc_lsb_sps[1] = 22; + sps.lt_ref_pic_poc_lsb_sps[2] = 33; + sps.used_by_curr_pic_lt_sps_flag[0] = true; + sps.used_by_curr_pic_lt_sps_flag[2] = true; + + let owned = sps_to_std_h265(&sps).unwrap(); + let std = owned.std(); + assert_eq!(std.flags.long_term_ref_pics_present_flag(), 1); + assert_eq!(std.num_long_term_ref_pics_sps, 3); + assert!(!std.pLongTermRefPicsSps.is_null()); + // SAFETY: pLongTermRefPicsSps targets `owned`'s boxed backing, alive here. + let lt = unsafe { &*std.pLongTermRefPicsSps }; + assert_eq!(lt.used_by_curr_pic_lt_sps_flag, 0b101); + assert_eq!(<.lt_ref_pic_poc_lsb_sps[..3], &[11, 22, 33]); + } + + /// `flag=1, num=0` is not a corner case: it is the punktfunk LTR/RFI + /// recovery stream shape (slice-signalled long-term pics, zero SPS + /// candidates — pf-bitstream's LTR synthesizer emits exactly this). The + /// header requires a valid `pLongTermRefPicsSps` whenever the flag is set; + /// a set flag over a null pointer is untested territory in every driver. + #[test] + fn long_term_flag_without_sps_candidates_still_backs_the_pointer() { + let mut sps = full_sps(); + sps.long_term_ref_pics_present_flag = true; + sps.num_long_term_ref_pics_sps = 0; + + let owned = sps_to_std_h265(&sps).unwrap(); + let std = owned.std(); + assert_eq!(std.flags.long_term_ref_pics_present_flag(), 1); + assert_eq!(std.num_long_term_ref_pics_sps, 0); + assert!(!std.pLongTermRefPicsSps.is_null()); + // SAFETY: pLongTermRefPicsSps targets `owned`'s boxed backing, alive here. + let lt = unsafe { &*std.pLongTermRefPicsSps }; + assert_eq!(lt.used_by_curr_pic_lt_sps_flag, 0); + assert!(lt.lt_ref_pic_poc_lsb_sps.iter().all(|&lsb| lsb == 0)); + } + + #[test] + fn envelope_rejections_fail_closed_instead_of_approximating() { + // 4:2:2 — legal H.265, outside the punktfunk envelope. + let mut sps = full_sps(); + sps.chroma_format_idc = 2; + assert_eq!( + sps_to_std_h265(&sps).unwrap_err(), + H265ParamsError::UnsupportedChromaFormat(2) + ); + // Monochrome likewise. + sps.chroma_format_idc = 0; + assert_eq!( + sps_to_std_h265(&sps).unwrap_err(), + H265ParamsError::UnsupportedChromaFormat(0) + ); + // 4:4:4 with separate colour planes = ChromaArrayType 0 in disguise. + sps.chroma_format_idc = 3; + sps.separate_colour_plane_flag = true; + assert_eq!( + sps_to_std_h265(&sps).unwrap_err(), + H265ParamsError::SeparateColourPlanes + ); + sps.separate_colour_plane_flag = false; + // Past 3 is not legal H.265 at all. + sps.chroma_format_idc = 4; + assert_eq!( + sps_to_std_h265(&sps).unwrap_err(), + H265ParamsError::InvalidChromaFormatIdc(4) + ); + + // 12-bit and mismatched depths: no punktfunk output format carries them. + let mut sps = full_sps(); + sps.bit_depth_luma_minus8 = 4; + sps.bit_depth_chroma_minus8 = 4; + assert_eq!( + sps_to_std_h265(&sps).unwrap_err(), + H265ParamsError::UnsupportedBitDepth { + luma_minus8: 4, + chroma_minus8: 4 + } + ); + let mut sps = full_sps(); + sps.bit_depth_chroma_minus8 = 0; + assert!(matches!( + sps_to_std_h265(&sps).unwrap_err(), + H265ParamsError::UnsupportedBitDepth { .. } + )); + + // 4:4:4 at 8 and 10 bits IS in the envelope (RExt profile). + let mut sps = full_sps(); + sps.profile_tier_level.general_profile_idc = 4; + sps.chroma_format_idc = 3; + sps.bit_depth_luma_minus8 = 0; + sps.bit_depth_chroma_minus8 = 0; + let owned = sps_to_std_h265(&sps).unwrap(); + assert_eq!( + owned.std().chroma_format_idc, + hh::StdVideoH265ChromaFormatIdc_STD_VIDEO_H265_CHROMA_FORMAT_IDC_444 + ); + + // An unmappable profile. + let mut sps = full_sps(); + sps.profile_tier_level.general_profile_idc = 9; // SCC + assert_eq!( + sps_to_std_h265(&sps).unwrap_err(), + H265ParamsError::UnmappableProfileIdc(9) + ); + + // SCC palette predictor initializers: the one pointer we refuse to fake, + // on both parameter sets. + let mut sps = full_sps(); + sps.scc_extension + .palette_predictor_initializers_present_flag = true; + assert_eq!( + sps_to_std_h265(&sps).unwrap_err(), + H265ParamsError::PalettePredictorInitializers + ); + let mut pps = full_pps(full_sps()); + pps.scc_extension + .palette_predictor_initializers_present_flag = true; + assert_eq!( + pps_to_std_h265(&pps).unwrap_err(), + H265ParamsError::PalettePredictorInitializers + ); + } + + #[test] + fn oversized_or_corrupt_rps_tables_are_rejected_not_padded() { + // More candidates than the spec's 64. + let mut sps = full_sps(); + sps.num_short_term_ref_pic_sets = 65; + assert_eq!( + sps_to_std_h265(&sps).unwrap_err(), + H265ParamsError::TooManyShortTermRpsSets(65) + ); + + // A declared count the parser never resolved. + let mut sps = full_sps(); + sps.num_short_term_ref_pic_sets = 1; + sps.short_term_ref_pic_set = Vec::new(); + assert_eq!( + sps_to_std_h265(&sps).unwrap_err(), + H265ParamsError::MissingShortTermRps { index: 0 } + ); + + // A set with more entries on one side than the Std arrays hold. + let mut sps = full_sps(); + sps.num_short_term_ref_pic_sets = 1; + sps.short_term_ref_pic_set = vec![ShortTermRefPicSet { + num_negative_pics: 17, + ..Default::default() + }]; + assert!(matches!( + sps_to_std_h265(&sps).unwrap_err(), + H265ParamsError::RpsEntryOverflow { set: 0, .. } + )); + + // A non-monotonic DeltaPoc array cannot re-encode as minus1 syntax. + let mut sps = full_sps(); + sps.num_short_term_ref_pic_sets = 1; + let mut set = ShortTermRefPicSet { + num_negative_pics: 2, + ..Default::default() + }; + set.delta_poc_s0[0] = -3; + set.delta_poc_s0[1] = -1; // must be strictly decreasing + sps.short_term_ref_pic_set = vec![set]; + assert_eq!( + sps_to_std_h265(&sps).unwrap_err(), + H265ParamsError::NonMonotonicRps { set: 0 } + ); + + // Too many long-term SPS candidates. + let mut sps = full_sps(); + sps.long_term_ref_pics_present_flag = true; + sps.num_long_term_ref_pics_sps = 33; + assert_eq!( + sps_to_std_h265(&sps).unwrap_err(), + H265ParamsError::TooManyLongTermSpsPics(33) + ); + } + + #[test] + fn every_mapped_pps_field_and_flag_round_trips_exactly() { + let pps = full_pps(full_sps()); + let owned = pps_to_std_h265(&pps).unwrap(); + let std = owned.std(); + + // The fixture's mixed pattern, bit for bit. + assert_eq!(std.flags.dependent_slice_segments_enabled_flag(), 1); + assert_eq!(std.flags.output_flag_present_flag(), 0); + assert_eq!(std.flags.sign_data_hiding_enabled_flag(), 1); + assert_eq!(std.flags.cabac_init_present_flag(), 0); + assert_eq!(std.flags.constrained_intra_pred_flag(), 1); + assert_eq!(std.flags.transform_skip_enabled_flag(), 0); + assert_eq!(std.flags.cu_qp_delta_enabled_flag(), 1); + assert_eq!(std.flags.pps_slice_chroma_qp_offsets_present_flag(), 0); + assert_eq!(std.flags.weighted_pred_flag(), 1); + assert_eq!(std.flags.weighted_bipred_flag(), 0); + assert_eq!(std.flags.transquant_bypass_enabled_flag(), 1); + assert_eq!(std.flags.tiles_enabled_flag(), 1); + assert_eq!(std.flags.entropy_coding_sync_enabled_flag(), 0); + assert_eq!(std.flags.uniform_spacing_flag(), 0); + assert_eq!(std.flags.loop_filter_across_tiles_enabled_flag(), 1); + assert_eq!(std.flags.pps_loop_filter_across_slices_enabled_flag(), 0); + assert_eq!(std.flags.deblocking_filter_control_present_flag(), 1); + assert_eq!(std.flags.deblocking_filter_override_enabled_flag(), 0); + assert_eq!(std.flags.pps_deblocking_filter_disabled_flag(), 1); + assert_eq!(std.flags.pps_scaling_list_data_present_flag(), 0); + assert_eq!(std.flags.lists_modification_present_flag(), 1); + assert_eq!(std.flags.slice_segment_header_extension_present_flag(), 0); + assert_eq!(std.flags.pps_extension_present_flag(), 1); + assert_eq!(std.flags.cross_component_prediction_enabled_flag(), 1); + assert_eq!(std.flags.chroma_qp_offset_list_enabled_flag(), 1); + assert_eq!(std.flags.pps_curr_pic_ref_enabled_flag(), 0); + assert_eq!( + std.flags.residual_adaptive_colour_transform_enabled_flag(), + 0 + ); + assert_eq!(std.flags.pps_slice_act_qp_offsets_present_flag(), 0); + assert_eq!( + std.flags.pps_palette_predictor_initializers_present_flag(), + 0 + ); + assert_eq!(std.flags.monochrome_palette_flag(), 0); + assert_eq!(std.flags.pps_range_extension_flag(), 1); + + assert_eq!(std.pps_pic_parameter_set_id, 3); + assert_eq!(std.pps_seq_parameter_set_id, 5); + assert_eq!( + std.sps_video_parameter_set_id, 2, + "resolved through the PPS's own SPS" + ); + assert_eq!(std.num_extra_slice_header_bits, 2); + assert_eq!(std.num_ref_idx_l0_default_active_minus1, 2); + assert_eq!(std.num_ref_idx_l1_default_active_minus1, 1); + assert_eq!(std.init_qp_minus26, -3); + assert_eq!(std.diff_cu_qp_delta_depth, 2); + assert_eq!(std.pps_cb_qp_offset, -4); + assert_eq!(std.pps_cr_qp_offset, 5); + assert_eq!(std.pps_beta_offset_div2, -2); + assert_eq!(std.pps_tc_offset_div2, 3); + assert_eq!(std.log2_parallel_merge_level_minus2, 1); + assert_eq!(std.log2_max_transform_skip_block_size_minus2, 2); + assert_eq!(std.diff_cu_chroma_qp_offset_depth, 1); + assert_eq!(std.chroma_qp_offset_list_len_minus1, 1); + assert_eq!(&std.cb_qp_offset_list[..2], &[1, -2]); + assert_eq!(&std.cr_qp_offset_list[..2], &[-3, 4]); + assert_eq!(std.log2_sao_offset_scale_luma, 1); + assert_eq!(std.log2_sao_offset_scale_chroma, 2); + assert_eq!(std.num_tile_columns_minus1, 1); + assert_eq!(std.num_tile_rows_minus1, 2); + assert_eq!(&std.column_width_minus1[..2], &[17, 12]); + assert_eq!(&std.row_height_minus1[..3], &[9, 8, 16]); + assert!(std.pScalingLists.is_null()); + assert!(std.pPredictorPaletteEntries.is_null()); + } + + #[test] + fn a_pps_field_past_its_std_width_is_an_error_not_a_truncation() { + let mut pps = full_pps(full_sps()); + pps.column_width_minus1[0] = 70_000; // past u16 + assert!(matches!( + pps_to_std_h265(&pps).unwrap_err(), + H265ParamsError::FieldOverflow { + field: "column_width_minus1", + value: 70_000 + } + )); + + let mut pps = full_pps(full_sps()); + pps.range_extension.log2_sao_offset_scale_luma = 300; // past u8 + assert!(matches!( + pps_to_std_h265(&pps).unwrap_err(), + H265ParamsError::FieldOverflow { .. } + )); + } + + #[test] + fn pps_scaling_lists_ride_behind_the_owned_pointer() { + let mut pps = full_pps(full_sps()); + pps.scaling_list_data_present_flag = true; + pps.scaling_list.scaling_list_4x4 = std::array::from_fn(|i| [60 + i as u8; 16]); + let owned = Box::new(pps_to_std_h265(&pps).unwrap()); + assert_eq!(owned.std().flags.pps_scaling_list_data_present_flag(), 1); + // SAFETY: pScalingLists targets `owned`'s boxed backing, alive here. + let lists = unsafe { &*owned.std().pScalingLists }; + assert_eq!(lists.ScalingList4x4[5], [65; 16]); + } + + #[test] + fn a_vps_converts_with_hrd_and_timing_skipped_by_design() { + let vps = Vps { + video_parameter_set_id: 2, + max_sub_layers_minus1: 1, + temporal_id_nesting_flag: true, + sub_layer_ordering_info_present_flag: true, + profile_tier_level: full_sps().profile_tier_level, + max_dec_pic_buffering_minus1: [5, 6, 0, 0, 0, 0, 0], + max_num_reorder_pics: [1, 2, 0, 0, 0, 0, 0], + max_latency_increase_plus1: [7, 8, 0, 0, 0, 0, 0], + // Timing present at the SOURCE: the skip must not copy it. + timing_info_present_flag: true, + num_units_in_tick: 1000, + time_scale: 60_000, + ..Default::default() + }; + let owned = vps_to_std_h265(&vps).unwrap(); + let std = owned.std(); + assert_eq!(std.vps_video_parameter_set_id, 2); + assert_eq!(std.vps_max_sub_layers_minus1, 1); + assert_eq!(std.flags.vps_temporal_id_nesting_flag(), 1); + assert_eq!(std.flags.vps_sub_layer_ordering_info_present_flag(), 1); + assert_eq!( + std.flags.vps_timing_info_present_flag(), + 0, + "true at the source, skipped by design (HRD/timing)" + ); + assert_eq!(std.vps_num_units_in_tick, 0); + assert_eq!(std.vps_time_scale, 0); + assert!(std.pHrdParameters.is_null()); + // SAFETY: both pointers target `owned`'s boxed backings, alive here. + let (ptl, dpb) = unsafe { (&*std.pProfileTierLevel, &*std.pDecPicBufMgr) }; + assert_eq!( + ptl.general_profile_idc, + hh::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_MAIN_10 + ); + assert_eq!(&dpb.max_dec_pic_buffering_minus1[..2], &[5, 6]); + assert_eq!(&dpb.max_latency_increase_plus1[..2], &[7, 8]); + + // A VPS whose DPB sizing overflows the Std u8 fields is corrupt. + let mut hostile = vps; + hostile.max_dec_pic_buffering_minus1[0] = 300; + assert!(matches!( + vps_to_std_h265(&hostile).unwrap_err(), + H265ParamsError::FieldOverflow { .. } + )); + } + + #[test] + fn the_fallback_vps_restates_exactly_what_the_sps_knows() { + let sps = full_sps(); + let owned = fallback_vps_from_sps(&sps).unwrap(); + let std = owned.std(); + assert_eq!(std.vps_video_parameter_set_id, sps.video_parameter_set_id); + assert_eq!(std.vps_max_sub_layers_minus1, sps.max_sub_layers_minus1); + // SAFETY: both pointers target `owned`'s boxed backings, alive here. + let (ptl, dpb) = unsafe { (&*std.pProfileTierLevel, &*std.pDecPicBufMgr) }; + assert_eq!( + ptl.general_level_idc, + hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_4_1 + ); + assert_eq!( + dpb.max_dec_pic_buffering_minus1, + sps.max_dec_pic_buffering_minus1 + ); + assert_eq!(dpb.max_num_reorder_pics, sps.max_num_reorder_pics); + } + + #[test] + fn the_25fps_vectors_own_parameter_sets_convert_cleanly() { + use std::io::Cursor; + + use cros_codecs::codec::h265::parser::Nalu; + use cros_codecs::codec::h265::parser::NaluType; + use cros_codecs::codec::h265::parser::Parser; + + // The same vendored vector pf-bitstream's h265 tests plan, same path. + const TEST_25FPS: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265" + ); + + let mut cursor = Cursor::new(TEST_25FPS); + let mut parser = Parser::default(); + let (mut vps_seen, mut sps_seen, mut pps_seen) = (false, false, false); + while let Ok(nalu) = Nalu::next(&mut cursor) { + match nalu.header.type_ { + NaluType::VpsNut if !vps_seen => { + let vps = parser.parse_vps(&nalu).expect("the vector's VPS parses"); + vps_to_std_h265(vps).expect("the vector's VPS converts"); + vps_seen = true; + } + NaluType::SpsNut if !sps_seen => { + let sps = parser.parse_sps(&nalu).expect("the vector's SPS parses"); + let owned = sps_to_std_h265(sps).expect("the vector's SPS converts"); + let std = owned.std(); + // The vector's own goldens: 320x240 8-bit 4:2:0 Main. + assert_eq!(std.pic_width_in_luma_samples, 320, "the vector is 320x240"); + assert_eq!(std.pic_height_in_luma_samples, 240); + assert_eq!(std.bit_depth_luma_minus8, 0); + assert_eq!( + std.chroma_format_idc, + hh::StdVideoH265ChromaFormatIdc_STD_VIDEO_H265_CHROMA_FORMAT_IDC_420 + ); + // SAFETY: pProfileTierLevel targets `owned`'s boxed backing. + let ptl = unsafe { &*std.pProfileTierLevel }; + assert_eq!( + ptl.general_profile_idc, + hh::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_MAIN + ); + sps_seen = true; + } + NaluType::PpsNut if !pps_seen => { + let pps = parser.parse_pps(&nalu).expect("the vector's PPS parses"); + let owned = pps_to_std_h265(pps).expect("the vector's PPS converts"); + assert_eq!(owned.std().pps_pic_parameter_set_id, 0); + pps_seen = true; + } + _ => {} + } + if vps_seen && sps_seen && pps_seen { + break; + } + } + assert!( + vps_seen && sps_seen && pps_seen, + "the vector opens with VPS + SPS + PPS" + ); + } +} diff --git a/crates/pf-vkdecode/src/pic.rs b/crates/pf-vkdecode/src/pic.rs new file mode 100644 index 00000000..2e7c0eda --- /dev/null +++ b/crates/pf-vkdecode/src/pic.rs @@ -0,0 +1,954 @@ +//! Per-AU conversion: one [`AuPlan`] into the `StdVideoDecodeH264*` structs, slice +//! offsets and DPB slot bindings a `vkCmdDecodeVideoKHR` call is built from (WP-B). +//! +//! Progressive envelope: pf-bitstream's planner rejects interlaced streams before a +//! plan exists, so every field/bottom FLAG here is written 0. The top/bottom +//! PicOrderCnt pairs are still real pairs — a progressive frame's bottom count +//! differs from its top whenever the PPS carries +//! `bottom_field_pic_order_in_frame_present_flag` — and ride through from +//! pf-bitstream verbatim. + +use ash::vk::native as hh; +use pf_bitstream::h264::AuPlan; +use pf_bitstream::h264::PicId; +use pf_bitstream::h264::RefPic; +use tracing::trace; + +use crate::slots::SlotError; +use crate::slots::SlotMap; + +/// One active reference of the AU: its DPB slot, its Std reference info, and the +/// planner id it resolves (kept so the backend can map the slot to its image). +#[derive(Debug, Clone)] +pub struct VkRef { + pub slot: u8, + pub std: hh::StdVideoDecodeH264ReferenceInfo, + pub id: PicId, +} + +/// Everything CPU-derivable of one AU's decode submission. WP-B adds the live +/// objects: bitstream buffer, DPB images, session and command recording. +#[derive(Debug, Clone)] +pub struct DecodePlanVk { + pub std_pic: hh::StdVideoDecodeH264PictureInfo, + /// Byte offset of each slice NALU in the AU as planned, START CODE INCLUDED. + /// AU-relative, NOT submission-final: the recording layer packs the SLICE + /// NALUs alone into the bitstream buffer and rebases these offsets while + /// doing so (non-VCL NALUs inside the decode range hang VCN firmware — see + /// the slices-only packing in `decoder.rs`); Vulkan's `pSliceOffsets` + /// receives the rebased offsets, each pointing at a start code within the + /// packed buffer. + pub slice_offsets: Vec, + /// The slot the decoded picture activates (`pSetupReferenceSlot`). + pub setup_slot: u8, + /// Reference info for the setup slot: the picture's own FrameNum/POC, with the + /// long-term flag already set when this very AU marks itself long-term (IDR + /// `long_term_reference_flag` or MMCO 6). + pub setup_ref: hh::StdVideoDecodeH264ReferenceInfo, + /// The planner id of the decoded picture (`AuPlan.dpb.stored`) — the backend + /// keys its image bookkeeping by it. + pub setup_id: PicId, + /// Whether the decoded picture is a reference. When `false` the setup slot + /// exists for the decode itself (plus any remaining DPB residency the planner + /// grants the picture) and must never be bound as a reference for later AUs — + /// it may even have been released already, via this very AU's `removed`, when + /// the picture bypassed the DPB. + pub setup_is_reference: bool, + /// The unique referenced pictures across all slices, in first-appearance order. + pub refs: Vec, +} + +/// Conversion failures. Stream damage never lands here — pf-bitstream degrades it to +/// [`pf_bitstream::h264::PlanWarning`]s upstream; these are caller/session bugs. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanToVkError { + /// The plan holds no slices; there is nothing to submit. + NoSlices, + /// The plan's `DpbUpdate.stored` is `None`. `plan_au` always stores; only + /// `flush()` produces such updates, and those go to [`SlotMap::apply`] directly. + NoStoredId, + /// A reference list entry's id holds no slot: an earlier plan of this stream + /// never went through this [`SlotMap`]. + UnresolvedReference(PicId), + Slot(SlotError), + /// A slice offset exceeds `u32` (Vulkan submits offsets as `u32`). + OffsetOverflow(usize), + /// The map was built for a different DPB depth than this plan's + /// `max_dpb_frames` — an SPS renegotiation resized the DPB. The session (WP-C) + /// must rebuild the video session and its [`SlotMap`]; converting against the + /// stale map would hand out slot indices the session's image pool does not have. + CapacityMismatch { + required: usize, + capacity: usize, + }, +} + +impl std::fmt::Display for PlanToVkError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlanToVkError::NoSlices => write!(f, "the plan holds no slices"), + PlanToVkError::NoStoredId => { + write!( + f, + "the plan stores no picture (flush updates go to SlotMap::apply)" + ) + } + PlanToVkError::UnresolvedReference(id) => { + write!(f, "referenced picture {id} holds no DPB slot in this map") + } + PlanToVkError::Slot(err) => write!(f, "slot assignment failed: {err}"), + PlanToVkError::OffsetOverflow(offset) => { + write!(f, "slice offset {offset} exceeds u32") + } + PlanToVkError::CapacityMismatch { required, capacity } => { + write!( + f, + "the plan needs {required} slots but the map holds {capacity} — \ + an SPS renegotiation resized the DPB; rebuild session and map" + ) + } + } + } +} + +impl std::error::Error for PlanToVkError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + PlanToVkError::Slot(err) => Some(err), + _ => None, + } + } +} + +impl From for PlanToVkError { + fn from(err: SlotError) -> Self { + PlanToVkError::Slot(err) + } +} + +/// One [`RefPic`] as Std reference info. +fn ref_info(rp: &RefPic) -> hh::StdVideoDecodeH264ReferenceInfo { + // SAFETY: StdVideoDecodeH264ReferenceInfo is a plain-C bindgen struct of a + // bitfield word and integers; all-zero is a valid value for every field. + let mut std: hh::StdVideoDecodeH264ReferenceInfo = unsafe { std::mem::zeroed() }; + std.flags + .set_used_for_long_term_reference(u32::from(rp.is_long_term)); + // top/bottom_field_flag stay 0 and is_non_existing stays 0: progressive envelope, + // and pf-bitstream never emits a gap placeholder as an id (it substitutes and + // warns instead). + // + // FrameNum carries exactly the pair-key the Std struct wants: frame_num for + // short-term references, LongTermFrameIdx for long-term ones. + std.FrameNum = rp.frame_num_or_lt_idx; + // The stored picture's real 8.2.1 pair: top != bottom whenever the PPS carried + // bottom_field_pic_order_in_frame_present_flag and the slice a nonzero + // delta_pic_order_cnt_bottom — even for progressive frames. + std.PicOrderCnt = [rp.top_field_order_cnt, rp.bottom_field_order_cnt]; + std +} + +/// Convert one planned AU, driving `slots` through the AU's slot lifecycle. +/// +/// `sps_id` is the id of the active SPS: an [`AuPlan`] names the active PPS (each +/// slice header carries `pic_parameter_set_id`) but not the SPS that PPS references — +/// the caller resolves it through its parameter-set table (in WP-B, +/// [`crate::OwnedStdPps`]'s `seq_parameter_set_id` keyed by the first slice's PPS id). +/// +/// Atomicity contract: every fallible step runs before any mutation of `slots`, so +/// an error leaves the map exactly as it was. In order: +/// 1. capacity is validated against the plan's `max_dpb_frames` (read-only); +/// 2. references resolve against the PRE-removal state (read-only) — this AU's own +/// end-of-picture marking (8.2.5) can evict a picture its slices legitimately +/// reference, e.g. the sliding window dropping the oldest short-term reference, +/// so `removed` must not be applied before the lists are mapped; +/// 3. slice offsets are validated (read-only); +/// 4. `removed` is applied — removals were real regardless of this AU's fate — and +/// the setup slot is assigned last (its failures are caller bugs; nothing is ever +/// half-applied). Released slots become assignable to later pictures; keeping the +/// underlying images alive until in-flight decodes complete is WP-B's +/// synchronization, not this map's. +pub fn plan_to_vk( + plan: &AuPlan, + slots: &mut SlotMap, + sps_id: u8, +) -> Result { + let first_slice = plan.slices.first().ok_or(PlanToVkError::NoSlices)?; + let setup_id = plan.dpb.stored.ok_or(PlanToVkError::NoStoredId)?; + + // The map must match THIS plan's DPB depth; a mismatch means an SPS + // renegotiation resized the DPB and the session must be rebuilt (WP-C). + let required = plan.picture.max_dpb_frames + 1; + if slots.capacity() != required { + return Err(PlanToVkError::CapacityMismatch { + required, + capacity: slots.capacity(), + }); + } + + // Unique referenced pictures across every slice's two lists, first appearance + // first. Per-slice list ORDER (ref_idx mapping) lives in the SlicePlans; this Vec + // is the AU-level slot binding set Vulkan wants (each slot listed once). + let mut refs: Vec = Vec::new(); + for slice in &plan.slices { + for rp in slice.ref_list0.iter().chain(&slice.ref_list1) { + if refs.iter().any(|existing| existing.id == rp.id) { + continue; + } + let slot = slots + .slot_of(rp.id) + .ok_or(PlanToVkError::UnresolvedReference(rp.id))?; + refs.push(VkRef { + slot, + std: ref_info(rp), + id: rp.id, + }); + } + } + + let pic = &plan.picture; + + // SAFETY: StdVideoDecodeH264PictureInfo is a plain-C bindgen struct of a bitfield + // word and integers; all-zero is a valid value for every field. + let mut std_pic: hh::StdVideoDecodeH264PictureInfo = unsafe { std::mem::zeroed() }; + let is_intra = plan + .slices + .iter() + .all(|slice| slice.header.slice_type.is_i() || slice.header.slice_type.is_si()); + std_pic.flags.set_is_intra(u32::from(is_intra)); + std_pic.flags.set_is_reference(u32::from(pic.is_reference)); + std_pic.flags.set_IdrPicFlag(u32::from(pic.is_idr)); + // field_pic_flag / bottom_field_flag / complementary_field_pair stay 0 by the + // progressive envelope (module docs). + std_pic.seq_parameter_set_id = sps_id; + std_pic.pic_parameter_set_id = first_slice.header.pic_parameter_set_id; + std_pic.frame_num = pic.frame_num; + std_pic.idr_pic_id = if pic.is_idr { + first_slice.header.idr_pic_id + } else { + 0 + }; + std_pic.PicOrderCnt = [pic.top_field_order_cnt, pic.bottom_field_order_cnt]; + + // The setup slot's reference info: this picture's own identity. When the AU marks + // ITSELF long-term — an IDR's long_term_reference_flag, or an MMCO 6 assigning an + // index (8.2.5) — the slot activates as a long-term reference keyed by + // LongTermFrameIdx, mirroring how ref_info keys long-term entries. + // + // ACTIVATION-vs-REFERENCE asymmetry under MMCO 5 (spec-legal, deliberately NOT + // rejected): these are the picture's 8.2.1 values as decoded, but an MMCO 5 in + // this same AU rebases the STORED frame_num/POC to zero after decoding + // (8.2.5.4.5), so later AUs reference this slot by the rebased pair (RefPic + // carries the stored values). punktfunk hosts never emit MMCO 5; + // pf_bitstream::h264::PlanWarning::Mmco5Rebase flags any occurrence so field + // logs tell us if that assumption ever breaks. + // SAFETY: as above — all-zero is a valid StdVideoDecodeH264ReferenceInfo. + let mut setup_ref: hh::StdVideoDecodeH264ReferenceInfo = unsafe { std::mem::zeroed() }; + setup_ref.PicOrderCnt = [pic.top_field_order_cnt, pic.bottom_field_order_cnt]; + let marking = &first_slice.header.dec_ref_pic_marking; + let self_lt_idx = if pic.is_idr { + marking.long_term_reference_flag.then_some(0u32) + } else if marking.adaptive_ref_pic_marking_mode_flag { + marking + .inner + .iter() + .find(|op| op.memory_management_control_operation == 6) + .map(|op| op.long_term_frame_idx) + } else { + None + }; + match self_lt_idx { + Some(idx) => { + setup_ref.flags.set_used_for_long_term_reference(1); + // Same saturation as pf-bitstream's frame_num_or_lt_idx: the spec bounds + // the ue(v)-coded index at 15, the parser does not. + setup_ref.FrameNum = u16::try_from(idx).unwrap_or(u16::MAX); + } + None => setup_ref.FrameNum = pic.frame_num, + } + + let mut slice_offsets = Vec::with_capacity(plan.slices.len()); + for slice in &plan.slices { + // SlicePlan.data starts at the slice NALU's start code — exactly the offset + // Vulkan wants (struct docs). + slice_offsets.push( + u32::try_from(slice.data.start) + .map_err(|_| PlanToVkError::OffsetOverflow(slice.data.start))?, + ); + } + + // Mutations LAST, after every fallible step above (fn docs). Removals first — + // they were real regardless of this AU's fate — then the setup assignment. + // + // The AU's own picture can itself appear in `removed`: a non-reference picture + // with no free frame buffer bypasses the DPB and is stored-and-evicted within + // one plan. Its slot must still exist for the decode itself, so it is assigned + // here and released right after — see `DecodePlanVk::setup_is_reference`. + let setup_evicted = plan.dpb.removed.contains(&setup_id); + for &id in &plan.dpb.removed { + if id == setup_id { + continue; + } + if !slots.release(id) { + // Tolerated but never silent: reachable only when the caller skipped + // feeding an AU's plan through this map. + trace!(id, "DpbUpdate removed an id this SlotMap never assigned"); + } + } + let setup_slot = slots.assign(setup_id)?; + if setup_evicted { + slots.release(setup_id); + } + + Ok(DecodePlanVk { + std_pic, + slice_offsets, + setup_slot, + setup_ref, + setup_id, + setup_is_reference: pic.is_reference, + refs, + }) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use std::io::Cursor; + use std::rc::Rc; + + use cros_codecs::codec::h264::nalu_writer::NaluWriter; + use cros_codecs::codec::h264::parser::Nalu; + use cros_codecs::codec::h264::parser::NaluType; + use cros_codecs::codec::h264::parser::Pps; + use cros_codecs::codec::h264::parser::PpsBuilder; + use cros_codecs::codec::h264::parser::Profile; + use cros_codecs::codec::h264::parser::Sps; + use cros_codecs::codec::h264::parser::SpsBuilder; + use cros_codecs::codec::h264::synthesizer::Synthesizer; + use pf_bitstream::h264::H264Planner; + use pf_bitstream::h264::Level; + + use super::*; + + // The same vendored vector pf-bitstream's tests plan (its goldens: 250 AUs, 500 + // slices), included from the same path rather than copied. + const TEST_25FPS: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264" + ); + + /// Test-only AU splitter, mirroring pf-bitstream's `split_into_aus` helper (which + /// is `#[cfg(test)]`-private there): a new AU starts at a non-slice NALU following + /// slices, or at a slice with `first_mb_in_slice == 0` (whose ue(v) encoding makes + /// the first RBSP bit 1) when the current AU already has slices. + fn split_into_aus(stream: &[u8]) -> Vec<&[u8]> { + let mut aus = Vec::new(); + let mut cursor = Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + + while let Ok(nalu) = Nalu::next(&mut cursor) { + let nalu_offset = cursor.position() as usize; + let start = nalu_offset - nalu.offset; + let is_slice = matches!(nalu.header.type_, NaluType::Slice | NaluType::SliceIdr); + let first_mb_zero = + is_slice && stream.get(nalu_offset + 1).is_some_and(|b| b & 0x80 != 0); + + if au_has_slice && (!is_slice || first_mb_zero) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus + } + + /// The decoder's picture-pool occupancy (`bound`/`pending`/`held`, exactly + /// `decode_inner`'s bookkeeping minus the GPU) over the WHOLE vendored + /// vector, with a consumer that HOLDS `hold` delivered frames before + /// releasing the oldest — the real client's shape (~4-7 held across its + /// channels, preroll and in-flight present). Returns the first starved AU + /// index, if any. + fn simulate_pool_occupancy(pool_size: usize, hold: usize) -> Option { + use std::collections::VecDeque; + + #[derive(Clone, Default)] + struct SimPicture { + bound: bool, + pending: bool, + held: u32, + } + + let aus = split_into_aus(TEST_25FPS); + let mut planner = H264Planner::new(); + let mut slots: Option = None; + let mut pictures = vec![SimPicture::default(); pool_size]; + let mut slot_image: Vec> = Vec::new(); + // id -> pool image of the decoded picture awaiting its output verdict. + let mut pending: BTreeMap = BTreeMap::new(); + // Delivered frames the consumer holds, oldest first. + let mut consumer: VecDeque = VecDeque::new(); + + for (index, au) in aus.iter().enumerate() { + let plan = planner.plan_au(au).expect("the clean vector plans"); + let slots = slots.get_or_insert_with(|| { + slot_image = vec![None; plan.picture.max_dpb_frames + 1]; + SlotMap::new(plan.picture.max_dpb_frames) + }); + let vk = plan_to_vk(&plan, slots, 0).expect("the clean vector converts"); + + // Binding sync: released slots unbind; the setup slot rebinds fresh. + let setup = usize::from(vk.setup_slot); + let mut held_slots = vec![false; slot_image.len()]; + for (slot, _id) in slots.held() { + held_slots[usize::from(slot)] = true; + } + for (slot, binding) in slot_image.iter_mut().enumerate() { + if let Some(picture) = *binding { + if !held_slots[slot] || slot == setup { + pictures[picture].bound = false; + *binding = None; + } + } + } + + // The decode target: a free pool image. + let Some(dst) = pictures + .iter() + .position(|p| !p.bound && !p.pending && p.held == 0) + else { + return Some(index); + }; + pictures[dst].pending = true; + pictures[dst].bound = true; + slot_image[setup] = Some(dst); + pending.insert(vk.setup_id, dst); + + // Settle: outputs deliver to the consumer; removed-never-output free. + for id in &plan.dpb.outputs { + if let Some(picture) = pending.remove(id) { + pictures[picture].pending = false; + pictures[picture].held += 1; + consumer.push_back(picture); + } + } + for id in &plan.dpb.removed { + if let Some(picture) = pending.remove(id) { + pictures[picture].pending = false; + } + } + // The hold-N consumer: releases only once it holds MORE than `hold`. + while consumer.len() > hold { + let released = consumer.pop_front().expect("nonempty"); + pictures[released].held -= 1; + } + } + None + } + + /// The .25 field-failure regression, pool-model edition: the vendored vector + /// keeps up to `max_dpb_frames + 1 = 8` pictures resident AND the real + /// client holds ~4 delivered frames — the pool must absorb BOTH at once. + /// `required_slots + HOLD_HEADROOM` never starves; the counterfactual shows + /// an under-headroomed pool starving on the same clean stream, which is the + /// exact class the fixed-size ring shipped in the first WP-B round. + #[test] + fn the_full_vector_with_a_hold_four_consumer_never_starves_the_picture_pool() { + // This vector: max_dpb_frames = 7 → required_slots = 8 (measured; + // asserted inside via SlotMap sizing). + let required_slots = 8; + let headroom = crate::images::HOLD_HEADROOM as usize; + assert_eq!( + simulate_pool_occupancy(required_slots + headroom, 4), + None, + "the shipped sizing must survive the whole vector with 4 held frames" + ); + // Counterfactual: holds beyond the headroom starve — the documented + // NoFreeSlot condition, now meaning exactly what it says. + assert!( + simulate_pool_occupancy(required_slots + 2, 4).is_some(), + "an under-headroomed pool must starve (else this regression proves nothing)" + ); + } + + #[test] + fn the_full_25fps_vector_converts_with_stable_slots_and_start_code_offsets() { + let aus = split_into_aus(TEST_25FPS); + let mut planner = H264Planner::new(); + let mut slots: Option = None; + // PicId -> the slot it was assigned; entries leave only on `removed`. + let mut held: BTreeMap = BTreeMap::new(); + let mut converted = 0usize; + + for au in &aus { + let plan = planner.plan_au(au).expect("the clean vector plans"); + let slots = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames)); + let vk = plan_to_vk(&plan, slots, 0).expect("the clean vector converts"); + converted += 1; + + // Slot stability: every reference resolves to the slot its picture was + // assigned when IT was decoded, and no ref shares the setup slot. + for r in &vk.refs { + assert_eq!( + held.get(&r.id), + Some(&r.slot), + "a referenced picture's slot changed while it was referenced" + ); + assert_ne!(r.slot, vk.setup_slot, "a reference aliases the setup slot"); + } + + // Slice offsets: one per slice, each at a start-code boundary of the AU, + // and exactly where the plan said the slice begins. + assert_eq!(vk.slice_offsets.len(), plan.slices.len()); + for (offset, slice) in vk.slice_offsets.iter().zip(&plan.slices) { + let offset = *offset as usize; + assert_eq!(offset, slice.data.start); + let at = &au[offset..]; + assert!( + at.starts_with(&[0, 0, 1]) || at.starts_with(&[0, 0, 0, 1]), + "slice offset {offset} does not sit on a start code" + ); + } + + assert_eq!(vk.std_pic.frame_num, plan.picture.frame_num); + assert_eq!( + u32::from(plan.picture.is_idr), + vk.std_pic.flags.IdrPicFlag() + ); + assert_eq!( + vk.setup_ref.PicOrderCnt[0], + plan.picture.top_field_order_cnt + ); + + // Mirror the map's bookkeeping: record the new picture, drop the removed. + let stored = plan.dpb.stored.unwrap(); + assert_eq!(vk.setup_id, stored); + assert_eq!(vk.setup_is_reference, plan.picture.is_reference); + held.insert(stored, vk.setup_slot); + for id in &plan.dpb.removed { + held.remove(id); + } + + // held() must mirror the plan-driven bookkeeping exactly, every AU. + let ledger: BTreeMap = slots.held().map(|(slot, id)| (id, slot)).collect(); + assert_eq!(ledger, held); + } + + assert_eq!(converted, 250, "the vector's own golden"); + + // Teardown: the flush update releases every remaining slot. + let mut slots = slots.unwrap(); + slots.apply(&planner.flush()); + assert_eq!(slots.active(), 0); + } + + /// Byte-level authoring, mirroring pf-bitstream's MMCO/LTR test (its helpers are + /// `#[cfg(test)]`-private): parameter sets via the vendored builders + + /// synthesizer, slice headers hand-written with the vendored `NaluWriter`. The + /// planner only reads headers, so no slice data follows the rbsp stop bit. + fn authored_sps_pps() -> (Rc, Rc) { + let sps = SpsBuilder::new() + .seq_parameter_set_id(0) + .profile_idc(Profile::Main) + .level_idc(Level::L4) + .frame_mbs_only_flag(true) + .direct_8x8_inference_flag(true) + .max_num_ref_frames(4) + .log2_max_frame_num_minus4(0) + .pic_order_cnt_type(0) + .log2_max_pic_order_cnt_lsb_minus4(0) + .resolution(64, 64) + .build(); + let pps = PpsBuilder::new(Rc::clone(&sps)) + .pic_parameter_set_id(0) + .pic_init_qp(26) + .build(); + (sps, pps) + } + + /// `bottom_delta` writes `delta_pic_order_cnt_bottom` — only legal when the PPS + /// the slice references sets `bottom_field_pic_order_in_frame_present_flag` + /// (the parser reads the field iff the flag is set, so writer and PPS must + /// agree). + fn write_idr_slice(bottom_delta: Option) -> Vec { + let mut buf = Vec::new(); + { + let mut w = NaluWriter::new(&mut buf, true); + w.write_header(3, NaluType::SliceIdr as u8).unwrap(); + w.write_ue(0u32).unwrap(); // first_mb_in_slice + w.write_ue(2u32).unwrap(); // slice_type: I + w.write_ue(0u32).unwrap(); // pic_parameter_set_id + w.write_f(4, 0u32).unwrap(); // frame_num, u(4): log2_max_frame_num_minus4 = 0 + w.write_ue(7u32).unwrap(); // idr_pic_id + w.write_f(4, 0u32).unwrap(); // pic_order_cnt_lsb, u(4) + if let Some(delta) = bottom_delta { + w.write_se(delta).unwrap(); // delta_pic_order_cnt_bottom + } + w.write_f(1, 0u32).unwrap(); // no_output_of_prior_pics_flag + w.write_f(1, 0u32).unwrap(); // long_term_reference_flag + w.write_se(0i32).unwrap(); // slice_qp_delta + w.write_f(1, 1u32).unwrap(); // rbsp stop bit + while !w.aligned() { + w.write_f(1, 0u32).unwrap(); + } + } + buf + } + + /// One P slice NALU. `mmco_ops` = `None` for sliding-window marking, `Some(ops)` + /// for adaptive marking with `(operation, single-argument)` pairs (ops 2/4/6 all + /// take exactly one) — the writer appends the terminating op 0. `bottom_delta` + /// as in [`write_idr_slice`]. + fn write_p_slice( + frame_num: u32, + poc_lsb: u32, + bottom_delta: Option, + num_ref_idx_l0_active: u32, + mmco_ops: Option<&[(u32, u32)]>, + ) -> Vec { + let mut buf = Vec::new(); + { + let mut w = NaluWriter::new(&mut buf, true); + w.write_header(1, NaluType::Slice as u8).unwrap(); + w.write_ue(0u32).unwrap(); // first_mb_in_slice + w.write_ue(0u32).unwrap(); // slice_type: P + w.write_ue(0u32).unwrap(); // pic_parameter_set_id + w.write_f(4, frame_num).unwrap(); // frame_num, u(4) + w.write_f(4, poc_lsb).unwrap(); // pic_order_cnt_lsb, u(4) + if let Some(delta) = bottom_delta { + w.write_se(delta).unwrap(); // delta_pic_order_cnt_bottom + } + w.write_f(1, 1u32).unwrap(); // num_ref_idx_active_override_flag + w.write_ue(num_ref_idx_l0_active - 1).unwrap(); + w.write_f(1, 0u32).unwrap(); // ref_pic_list_modification_flag_l0 + match mmco_ops { + None => w.write_f(1, 0u32).map(|_| ()).unwrap(), + Some(ops) => { + w.write_f(1, 1u32).unwrap(); // adaptive_ref_pic_marking_mode_flag + for (op, arg) in ops { + w.write_ue(*op).unwrap(); + w.write_ue(*arg).unwrap(); + } + w.write_ue(0u32).unwrap(); // memory_management_control_operation end + } + } + w.write_se(0i32).unwrap(); // slice_qp_delta + w.write_f(1, 1u32).unwrap(); // rbsp stop bit + while !w.aligned() { + w.write_f(1, 0u32).unwrap(); + } + } + buf + } + + #[test] + fn an_mmco_self_marking_sets_the_setup_lt_flag_and_later_refs_carry_it() { + let (sps, pps) = authored_sps_pps(); + let mut au0 = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au0, true).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(3, &pps, &mut au0, true).unwrap(); + au0.extend(write_idr_slice(None)); + // AU1 marks itself long-term: MMCO 4 admits long-term index 0, MMCO 6 + // assigns it to the current picture. + let au1 = write_p_slice(1, 2, None, 1, Some(&[(4, 1), (6, 0)])); + let au2 = write_p_slice(2, 4, None, 2, None); + + let mut planner = H264Planner::new(); + let p0 = planner.plan_au(&au0).unwrap(); + let mut slots = SlotMap::new(p0.picture.max_dpb_frames); + + let idr_id = p0.dpb.stored.unwrap(); + let vk0 = plan_to_vk(&p0, &mut slots, 0).unwrap(); + assert_eq!(vk0.std_pic.flags.IdrPicFlag(), 1); + assert_eq!(vk0.std_pic.flags.is_intra(), 1); + assert_eq!(vk0.std_pic.idr_pic_id, 7, "from the authored slice header"); + assert_eq!(vk0.std_pic.PicOrderCnt, [0, 0]); + assert_eq!(vk0.setup_ref.flags.used_for_long_term_reference(), 0); + assert_eq!(vk0.setup_id, idr_id); + assert!(vk0.setup_is_reference, "an IDR is a reference"); + assert!(vk0.refs.is_empty()); + + // AU1: the setup slot activates LONG-TERM (its own MMCO 6), keyed by + // LongTermFrameIdx 0, not by frame_num 1. + let p1 = planner.plan_au(&au1).unwrap(); + let lt_id = p1.dpb.stored.unwrap(); + let vk1 = plan_to_vk(&p1, &mut slots, 0).unwrap(); + assert_eq!(vk1.std_pic.flags.is_intra(), 0); + assert_eq!(vk1.std_pic.PicOrderCnt, [2, 2]); + assert_eq!(vk1.setup_ref.flags.used_for_long_term_reference(), 1); + assert_eq!(vk1.setup_ref.FrameNum, 0, "LongTermFrameIdx, not frame_num"); + assert_eq!(vk1.refs.len(), 1); + assert_eq!(vk1.refs[0].id, idr_id); + assert_eq!(vk1.refs[0].std.flags.used_for_long_term_reference(), 0); + + // AU2 references both: the IDR short-term (keyed by frame_num) and AU1 + // long-term (keyed by LongTermFrameIdx), each on its stable slot. + let p2 = planner.plan_au(&au2).unwrap(); + let vk2 = plan_to_vk(&p2, &mut slots, 0).unwrap(); + let by_id: BTreeMap = vk2.refs.iter().map(|r| (r.id, r)).collect(); + let idr_ref = by_id[&idr_id]; + assert_eq!(idr_ref.std.flags.used_for_long_term_reference(), 0); + assert_eq!(idr_ref.std.FrameNum, 0); + assert_eq!(idr_ref.slot, vk0.setup_slot); + let lt_ref = by_id[<_id]; + assert_eq!(lt_ref.std.flags.used_for_long_term_reference(), 1); + assert_eq!(lt_ref.std.FrameNum, 0, "LongTermFrameIdx, not frame_num"); + assert_eq!( + lt_ref.std.PicOrderCnt, + [2, 2], + "the stored top/bottom pair (equal here: no delta_pic_order_cnt_bottom)" + ); + assert_eq!(lt_ref.slot, vk1.setup_slot); + assert_ne!(vk2.setup_slot, idr_ref.slot); + assert_ne!(vk2.setup_slot, lt_ref.slot); + } + + #[test] + fn a_failed_conversion_leaves_the_slot_map_untouched_and_the_session_recovers() { + let (sps, pps) = authored_sps_pps(); + let mut au0 = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au0, true).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(3, &pps, &mut au0, true).unwrap(); + au0.extend(write_idr_slice(None)); + let au1 = write_p_slice(1, 2, None, 1, None); + + let mut planner = H264Planner::new(); + let p0 = planner.plan_au(&au0).unwrap(); + let idr_id = p0.dpb.stored.unwrap(); + let p1 = planner.plan_au(&au1).unwrap(); + + // A right-sized map that never saw AU0, holding one unrelated slot: the + // reference must fail loudly, not resolve to a fabricated slot. + let mut fresh = SlotMap::new(p1.picture.max_dpb_frames); + fresh.assign(999).unwrap(); + assert_eq!( + plan_to_vk(&p1, &mut fresh, 0).unwrap_err(), + PlanToVkError::UnresolvedReference(idr_id) + ); + + // Atomicity: the failed conversion mutated nothing. + assert_eq!(fresh.active(), 1); + assert_eq!(fresh.held().collect::>(), vec![(0, 999)]); + + // And the session recovers: the next valid AU (an IDR restart) still + // converts on the same map. Its `removed` names ids this map never assigned + // (planned before the map existed) — tolerated by design. + let p2 = planner.plan_au(&write_idr_slice(None)).unwrap(); + let vk2 = plan_to_vk(&p2, &mut fresh, 0).unwrap(); + assert_eq!(vk2.setup_slot, 1, "the lowest free slot after the held one"); + assert_eq!(fresh.active(), 2); + } + + #[test] + fn an_sps_switch_that_resizes_the_dpb_is_a_capacity_mismatch_not_a_guess() { + // Two SPSes whose only planning-relevant difference is DPB depth: Level 1 at + // 320x240 gives MaxDpbMbs 396 / 300 = 1 frame; Level 4 gives 16. + let authored = |level: Level, max_refs: u8| -> (Rc, Rc) { + let sps = SpsBuilder::new() + .seq_parameter_set_id(0) + .profile_idc(Profile::Main) + .level_idc(level) + .frame_mbs_only_flag(true) + .direct_8x8_inference_flag(true) + .max_num_ref_frames(max_refs) + .log2_max_frame_num_minus4(0) + .pic_order_cnt_type(0) + .log2_max_pic_order_cnt_lsb_minus4(0) + .resolution(320, 240) + .build(); + let pps = PpsBuilder::new(Rc::clone(&sps)) + .pic_parameter_set_id(0) + .pic_init_qp(26) + .build(); + (sps, pps) + }; + let (sps_a, pps_a) = authored(Level::L1, 1); + let (sps_b, pps_b) = authored(Level::L4, 4); + + let mut au0 = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps_a, &mut au0, true).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(3, &pps_a, &mut au0, true).unwrap(); + au0.extend(write_idr_slice(None)); + let mut au1 = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps_b, &mut au1, true).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(3, &pps_b, &mut au1, true).unwrap(); + au1.extend(write_idr_slice(None)); + + let mut planner = H264Planner::new(); + let p0 = planner.plan_au(&au0).unwrap(); + assert_eq!(p0.picture.max_dpb_frames, 1); + let mut slots = SlotMap::new(p0.picture.max_dpb_frames); + plan_to_vk(&p0, &mut slots, 0).unwrap(); + + // The renegotiated stream needs a deeper DPB than this map was built for: + // refuse, so the session (WP-C) rebuilds session + map instead of handing + // out slots the image pool does not have. + let p1 = planner.plan_au(&au1).unwrap(); + assert_eq!(p1.picture.max_dpb_frames, 16); + assert_eq!( + plan_to_vk(&p1, &mut slots, 0).unwrap_err(), + PlanToVkError::CapacityMismatch { + required: 17, + capacity: 2 + } + ); + } + + #[test] + fn a_full_dpb_bump_reuses_the_slot_but_the_pool_model_binds_a_fresh_image() { + // Depth-1 DPB (Level 1 at 320x240 ⇒ max_dpb_frames 1, capacity 2): every + // stored P evicts the previous picture, and that picture's id lands in + // BOTH `outputs` and `removed` of the SAME plan — so `plan_to_vk` frees + // the evicted slot and immediately re-assigns it as this AU's setup. + // That SLOT reuse is fine and expected; the picture-pool model's whole + // point is that the re-activated slot binds a DIFFERENT free image, so + // the delivered picture's image is never the new decode target while the + // consumer holds it (the HIGH overwrite bug of the adversarial round, + // and the .25 field failure's class). + let sps = SpsBuilder::new() + .seq_parameter_set_id(0) + .profile_idc(Profile::Main) + .level_idc(Level::L1) + .frame_mbs_only_flag(true) + .direct_8x8_inference_flag(true) + .max_num_ref_frames(1) + .log2_max_frame_num_minus4(0) + .pic_order_cnt_type(0) + .log2_max_pic_order_cnt_lsb_minus4(0) + .resolution(320, 240) + .build(); + let pps = PpsBuilder::new(Rc::clone(&sps)) + .pic_parameter_set_id(0) + .pic_init_qp(26) + .build(); + let mut au0 = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au0, true).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(3, &pps, &mut au0, true).unwrap(); + au0.extend(write_idr_slice(None)); + + let mut planner = H264Planner::new(); + let p0 = planner.plan_au(&au0).unwrap(); + let mut slots = SlotMap::new(p0.picture.max_dpb_frames); + let vk0 = plan_to_vk(&p0, &mut slots, 0).unwrap(); + + // Decoder-side pool bookkeeping (mirrors decode_inner): image 0 hosts + // picture 0; the consumer receives and HOLDS its frame. + let pool = 2 + 1; // required_slots + 1 of headroom is enough here + let mut bound = vec![false; pool]; + let mut held = vec![0u32; pool]; + let mut slot_image: Vec> = vec![None; slots.capacity()]; + let free = |bound: &[bool], held: &[u32]| (0..pool).find(|&i| !bound[i] && held[i] == 0); + + let img0 = free(&bound, &held).unwrap(); + bound[img0] = true; + slot_image[usize::from(vk0.setup_slot)] = Some(img0); + + // AU1 bumps AU0's picture: outputs+removed carry id0, and plan_to_vk + // hands the SAME slot back as the setup. + let p1 = planner + .plan_au(&write_p_slice(1, 2, None, 1, None)) + .unwrap(); + assert!(p1.dpb.outputs.contains(&vk0.setup_id) && p1.dpb.removed.contains(&vk0.setup_id)); + let vk1 = plan_to_vk(&p1, &mut slots, 0).unwrap(); + assert_eq!( + vk1.setup_slot, vk0.setup_slot, + "slot reuse across the bump is the planner's normal behaviour" + ); + + // Binding sync: the re-activated slot drops its old binding; picture 0's + // image is now delivered to the consumer (held), NOT freed. + bound[img0] = false; + held[img0] += 1; // outputs → delivered, consumer holds it + slot_image[usize::from(vk1.setup_slot)] = None; + + // The pool hands the re-activated slot a FRESH image — never image 0. + let img1 = free(&bound, &held).expect("headroom guarantees a free image"); + assert_ne!( + img1, img0, + "the held (delivered, unreleased) image must never be re-bound as a \ + decode target — the pool decoupling IS the overwrite fix" + ); + bound[img1] = true; + slot_image[usize::from(vk1.setup_slot)] = Some(img1); + + // Once the consumer releases frame 0, image 0 returns to the free list. + held[img0] -= 1; + assert_eq!(free(&bound, &held), Some(img0)); + } + + #[test] + fn a_nonzero_delta_bottom_reaches_setup_and_reference_poc_pairs_distinctly() { + // A PPS with bottom_field_pic_order_in_frame_present_flag: progressive + // frames then carry delta_pic_order_cnt_bottom and bottom != top. The + // builder has no setter for the flag, so the Pps is constructed directly + // (its fields are public) and synthesized from there. + let sps = SpsBuilder::new() + .seq_parameter_set_id(0) + .profile_idc(Profile::Main) + .level_idc(Level::L4) + .frame_mbs_only_flag(true) + .direct_8x8_inference_flag(true) + .max_num_ref_frames(4) + .log2_max_frame_num_minus4(0) + .pic_order_cnt_type(0) + .log2_max_pic_order_cnt_lsb_minus4(0) + .resolution(64, 64) + .build(); + let pps = Pps { + pic_parameter_set_id: 0, + seq_parameter_set_id: 0, + entropy_coding_mode_flag: false, + bottom_field_pic_order_in_frame_present_flag: true, + num_slice_groups_minus1: 0, + num_ref_idx_l0_default_active_minus1: 0, + num_ref_idx_l1_default_active_minus1: 0, + weighted_pred_flag: false, + weighted_bipred_idc: 0, + pic_init_qp_minus26: 0, + pic_init_qs_minus26: 0, + chroma_qp_index_offset: 0, + deblocking_filter_control_present_flag: false, + constrained_intra_pred_flag: false, + redundant_pic_cnt_present_flag: false, + transform_8x8_mode_flag: false, + pic_scaling_matrix_present_flag: false, + scaling_lists_4x4: [[0; 16]; 6], + scaling_lists_8x8: [[0; 64]; 6], + second_chroma_qp_index_offset: 0, + sps: Rc::clone(&sps), + }; + + let mut au0 = Vec::new(); + Synthesizer::<'_, Sps, _>::synthesize(3, &sps, &mut au0, true).unwrap(); + Synthesizer::<'_, Pps, _>::synthesize(3, &pps, &mut au0, true).unwrap(); + au0.extend(write_idr_slice(Some(2))); // top 0, bottom 0 + 2 + let au1 = write_p_slice(1, 4, Some(1), 1, None); // top 4, bottom 4 + 1 + + let mut planner = H264Planner::new(); + let p0 = planner.plan_au(&au0).unwrap(); + assert_eq!( + ( + p0.picture.top_field_order_cnt, + p0.picture.bottom_field_order_cnt + ), + (0, 2) + ); + let mut slots = SlotMap::new(p0.picture.max_dpb_frames); + let vk0 = plan_to_vk(&p0, &mut slots, 0).unwrap(); + // BOTH orders, both structs: a top/bottom swap or a collapse to one value + // must fail here. + assert_eq!(vk0.std_pic.PicOrderCnt, [0, 2]); + assert_eq!(vk0.setup_ref.PicOrderCnt, [0, 2]); + + let p1 = planner.plan_au(&au1).unwrap(); + let vk1 = plan_to_vk(&p1, &mut slots, 0).unwrap(); + assert_eq!(vk1.std_pic.PicOrderCnt, [4, 5]); + assert_eq!(vk1.setup_ref.PicOrderCnt, [4, 5]); + // The reference carries the STORED pair of the IDR — through RefPic, not a + // fabricated bottom. + assert_eq!(vk1.refs.len(), 1); + assert_eq!(vk1.refs[0].id, p0.dpb.stored.unwrap()); + assert_eq!(vk1.refs[0].std.PicOrderCnt, [0, 2]); + } +} diff --git a/crates/pf-vkdecode/src/pic_av1.rs b/crates/pf-vkdecode/src/pic_av1.rs new file mode 100644 index 00000000..28da7f11 --- /dev/null +++ b/crates/pf-vkdecode/src/pic_av1.rs @@ -0,0 +1,1391 @@ +//! One AV1 [`AuPlan`] into the Vulkan decode structures — the CPU half of M7's +//! Vulkan rung. +//! +//! AV1 puts almost the whole frame header in the PICTURE info rather than in session +//! parameters, so `StdVideoDecodeAV1PictureInfo` carries eight pointers to blocks +//! that are per-frame: tile info, quantisation, segmentation, loop filter, CDEF, loop +//! restoration, global motion and film grain. Each is owned here, boxed, beside the +//! Std struct that points at it — the ownership contract [`crate::OwnedStdSps`] +//! documents. +//! +//! # The reference numbering, which is a THIRD convention again +//! +//! `VkVideoDecodeAV1PictureInfoKHR::referenceNameSlotIndices` is indexed by AV1 +//! REFERENCE NAME — `LAST_FRAME` through `ALTREF_FRAME`, seven of them, matching +//! `ref_frame_idx[0..7]` — and each entry holds the **DPB SLOT INDEX** that name +//! resolves to, or `-1` for a name this frame does not use. +//! +//! That is not the same as a position in `pReferenceSlots`, and it is the same class +//! of mistake that made HEVC unplayable on every driver in this program: there, the +//! RPS arrays were filled with positions where the spec wanted slots, and the two +//! coincide right up until they do not. Here the trap is narrower but identical in +//! shape, so the plan carries slot indices and says so, and the backend lays +//! `pReferenceSlots` out in [`DecodePlanVkAv1::refs`] order independently. +//! +//! The NAME itself comes from the planner, not from counting: `AuPlan::refs` is +//! indexed by reference name and a lost reference leaves a hole there, so the loop +//! below reads its index off the iterator and skips the holes. Compacting the list +//! first — which is what it used to receive — renamed every reference after the +//! first loss. + +use ash::vk::native as hh; +use pf_bitstream::av1::coded_cdef_sec_strength; +use pf_bitstream::av1::AuPlan; +use pf_bitstream::av1::PicId; +use pf_bitstream::av1::REFS_PER_FRAME; +use pf_bitstream::av1::{TilePlan, NUM_REF_SLOTS}; + +use crate::slots::SlotError; +use crate::slots::SlotMap; + +/// `StdVideoAV1FrameType`. +const STD_FRAME_TYPE_KEY: hh::StdVideoAV1FrameType = 0; +const STD_FRAME_TYPE_INTER: hh::StdVideoAV1FrameType = 1; +const STD_FRAME_TYPE_INTRA_ONLY: hh::StdVideoAV1FrameType = 2; +const STD_FRAME_TYPE_SWITCH: hh::StdVideoAV1FrameType = 3; + +/// `referenceNameSlotIndices` entry for a reference name this frame does not use. +pub const REFERENCE_NAME_UNUSED: i32 = -1; + +/// `SUPERRES_DENOM_MIN` (AV1 spec) — `coded_denom` is the denominator less this. +const SUPERRES_DENOM_MIN: u32 = 9; + +/// One active reference: its DPB slot, its Std reference info, and the planner id it +/// resolves — the same shape the H.264 and H.265 conversions carry. +#[derive(Debug, Clone)] +pub struct VkRefAv1 { + pub slot: u8, + pub std: hh::StdVideoDecodeAV1ReferenceInfo, + pub id: PicId, +} + +/// Everything CPU-derivable of one AV1 frame's decode submission. +#[derive(Debug)] +pub struct DecodePlanVkAv1 { + /// The Std picture info and everything its eight pointers target. + pub pic: OwnedStdAv1PictureInfo, + /// Per reference NAME (`LAST_FRAME`..`ALTREF_FRAME`), the DPB SLOT it resolves + /// to, or [`REFERENCE_NAME_UNUSED`] — see the module docs. Not positions in + /// [`Self::refs`]. + pub reference_name_slot_indices: [i32; REFS_PER_FRAME], + /// Each tile group's byte range in the access unit as planned — whole OBUs. + /// + /// ⚠ NOT what is uploaded. The bitstream buffer holds the raw tile PAYLOADS + /// found inside these OBUs and nothing else, and the recording layer walks + /// them itself (`decoder_av1`'s `plan_bitstream`) because that walk needs the + /// access-unit bytes, which a conversion never sees. Carried here so a caller + /// can see what the frame was made of without re-parsing. + pub tiles: Vec, + /// The slot the decoded picture activates (`pSetupReferenceSlot`). + pub setup_slot: u8, + pub setup_ref: hh::StdVideoDecodeAV1ReferenceInfo, + pub setup_id: PicId, + /// The unique referenced pictures of this frame, first appearance first. The + /// backend lays `pReferenceSlots` out in THIS order. + pub refs: Vec, + /// Pictures this frame's own `refresh_frame_flags` displaces from the store + /// while THIS frame still reads them — their slots are released only once the + /// decode op has been recorded, and the caller owes exactly that. + /// + /// AV1 applies `refresh_frame_flags` AFTER the frame is decoded (7.20), so a + /// frame reading a slot and overwriting it is ordinary rather than exotic: + /// `ref_frame_idx` resolves against the pre-decode store and the refresh lands + /// behind it. The picture is therefore a LIVE reference for exactly this decode + /// op and its DPB slot may not be recycled until the op is submitted. Releasing + /// it inside this conversion — which is what the H.264 and H.265 siblings do + /// with their whole `removed` list — hands its slot straight back to + /// [`Self::setup_slot`], because the lowest free slot is the one just vacated: + /// [`Self::refs`] then names the very slot the decode target activates, and the + /// decoder's binding sync clears its image on the way past. Measured on the + /// vendored vector at frame 6 of 274 (`slot_recycling_waits_for_the_decode_op`). + /// + /// Empty for the overwhelming majority of frames; the ids are always a subset + /// of the plan's `dpb.removed`, so applying them completes that plan's + /// bookkeeping and never invents a removal. + pub release_after_decode: Vec, +} + +/// The Std picture info plus the heap allocations its eight pointers target. +/// +/// Ownership contract as [`crate::OwnedStdSps`]: boxed backing, movable wrapper, no +/// mutation, deliberately not `Clone`. +#[derive(Debug)] +pub struct OwnedStdAv1PictureInfo { + std: hh::StdVideoDecodeAV1PictureInfo, + _tile_info: Box, + /// `StdVideoAV1TileInfo`'s own four arrays, behind ITS pointers — a second level + /// of backing, and the reason this wrapper exists rather than a plain struct. + _tile_arrays: TileArrays, + _quantization: Box, + _segmentation: Box, + _loop_filter: Box, + _cdef: Box, + _loop_restoration: Box, + _global_motion: Box, + /// Only present where the stream codes film grain; null otherwise, because a + /// zeroed block behind a live pointer would ask the decoder to synthesise grain + /// the stream never described. + _film_grain: Option>, +} + +impl OwnedStdAv1PictureInfo { + /// The Std struct, valid for as long as `self` lives. + pub fn std(&self) -> &hh::StdVideoDecodeAV1PictureInfo { + &self.std + } +} + +/// The tile-info arrays, boxed so `StdVideoAV1TileInfo`'s pointers stay valid. +#[derive(Debug)] +struct TileArrays { + _mi_col_starts: Box<[u16]>, + _mi_row_starts: Box<[u16]>, + _width_in_sbs_minus_1: Box<[u16]>, + _height_in_sbs_minus_1: Box<[u16]>, +} + +/// Why a plan cannot be expressed as Vulkan AV1 structures. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanToVkAv1Error { + /// A `show_existing_frame` plan: it decodes nothing, so it has no submission. + /// The backend displays the named picture instead of calling this. + NoDecode, + /// A reference the slot map does not hold. + UnresolvedReference(PicId), + /// More distinct references than the DPB can bind. + TooManyReferences(usize), + /// A field wider than its Std type. + FieldOverflow { + field: &'static str, + value: u32, + }, + Slot(SlotError), +} + +impl From for PlanToVkAv1Error { + fn from(e: SlotError) -> Self { + PlanToVkAv1Error::Slot(e) + } +} + +impl std::fmt::Display for PlanToVkAv1Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlanToVkAv1Error::NoDecode => { + write!(f, "a show_existing_frame plan has no decode submission") + } + PlanToVkAv1Error::UnresolvedReference(id) => { + write!(f, "reference picture {id} holds no DPB slot") + } + PlanToVkAv1Error::TooManyReferences(n) => { + write!(f, "{n} distinct references exceed the DPB") + } + PlanToVkAv1Error::FieldOverflow { field, value } => { + write!(f, "{field} = {value} does not fit its Std field") + } + PlanToVkAv1Error::Slot(e) => write!(f, "DPB slot map: {e:?}"), + } + } +} + +impl std::error::Error for PlanToVkAv1Error {} + +fn narrow(field: &'static str, value: u32) -> Result { + u8::try_from(value).map_err(|_| PlanToVkAv1Error::FieldOverflow { field, value }) +} + +/// The parser's frame type as `StdVideoAV1FrameType`. +/// +/// Written out rather than cast even though the four discriminants happen to +/// coincide: the coincidence is between a vendored crate's enum and a Vulkan +/// header, and neither is ours to keep in step. Both the picture info and every +/// reference info go through here, so the two can never disagree either. +fn std_frame_type(frame_type: pf_bitstream::av1::FrameType) -> hh::StdVideoAV1FrameType { + match frame_type { + pf_bitstream::av1::FrameType::KeyFrame => STD_FRAME_TYPE_KEY, + pf_bitstream::av1::FrameType::InterFrame => STD_FRAME_TYPE_INTER, + pf_bitstream::av1::FrameType::IntraOnlyFrame => STD_FRAME_TYPE_INTRA_ONLY, + pf_bitstream::av1::FrameType::SwitchFrame => STD_FRAME_TYPE_SWITCH, + } +} + +/// Convert one planned AV1 frame. +/// +/// Nothing mutates `slots` until every fallible step has passed — the same +/// transaction discipline the other two conversions keep, for the same reason: a +/// half-applied DPB update is the shape of a corrupt reference. +pub fn plan_to_vk_av1( + plan: &AuPlan, + slots: &mut SlotMap, +) -> Result { + let setup_id = plan.dpb.stored.ok_or(PlanToVkAv1Error::NoDecode)?; + let header = &*plan.header; + + // --- resolve, before any mutation ------------------------------------ + // The unique references, first appearance first, plus the per-NAME slot table. + // `plan.refs` is indexed BY NAME and holes are real (a lost reference), so the + // index is taken from the iterator and empty names are skipped rather than + // shifting everything after them up one. + let mut refs: Vec = Vec::new(); + let mut reference_name_slot_indices = [REFERENCE_NAME_UNUSED; REFS_PER_FRAME]; + for (name, r) in plan.refs.iter().enumerate() { + let Some(r) = r else { continue }; + let slot = slots + .slot_of(r.id) + .ok_or(PlanToVkAv1Error::UnresolvedReference(r.id))?; + reference_name_slot_indices[name] = i32::from(slot); + if !refs.iter().any(|existing| existing.id == r.id) { + refs.push(VkRefAv1 { + slot, + // The REFERENCE's own state, never this frame's — see + // `pf_bitstream::av1::RefState`. + std: reference_info(&r.state)?, + id: r.id, + }); + } + } + if refs.len() > NUM_REF_SLOTS { + return Err(PlanToVkAv1Error::TooManyReferences(refs.len())); + } + + let pic = picture_info(header, &plan.sequence)?; + // The picture being decoded activates a slot, so it needs the same answers a + // reference does — and it is cached as that slot's reference info for later + // frames (`decoder_av1`'s `slot_refs`), so it is built through the SAME + // function the reference path uses. libavcodec leaves `SavedOrderHints` zero + // here because it rebuilds a reference's info from scratch every frame and + // never re-reads the setup entry; this rung caches, so filling them keeps the + // cached copy equal to the one the reference path would build. + let setup_ref = reference_info(&pf_bitstream::av1::RefState::of(header))?; + + // --- mutations, after every fallible step ----------------------------- + // A picture this frame READS may be displaced by this same frame's refresh — + // see `DecodePlanVkAv1::release_after_decode` for why that is ordinary AV1 and + // what releasing it here would cost. Its slot survives the assignment below and + // is handed to the caller to release once the decode op is recorded. + let release_after_decode: Vec = plan + .dpb + .removed + .iter() + .copied() + .filter(|id| *id != setup_id && refs.iter().any(|r| r.id == *id)) + .collect(); + for &id in &plan.dpb.removed { + if id == setup_id || release_after_decode.contains(&id) { + continue; + } + let _ = slots.release(id); + } + let setup_slot = match slots.slot_of(setup_id) { + // A frame may refresh a slot it already occupies; re-planning must not + // double-assign. + Some(existing) => existing, + None => slots.assign(setup_id)?, + }; + + Ok(DecodePlanVkAv1 { + pic, + reference_name_slot_indices, + tiles: plan.tiles.clone(), + setup_slot, + setup_ref, + setup_id, + refs, + release_after_decode, + }) +} + +/// One picture's `StdVideoDecodeAV1ReferenceInfo`, from THAT picture's own header +/// state. +/// +/// Every field here is about the reference, and answering any of them from the +/// frame currently being decoded is a silent mispredict rather than an error. The +/// set matches libavcodec's `vulkan_av1.c` field for field (`vk_av1_fill_pict`); +/// `RefFrameSignBias` and `SavedOrderHints` are the two RADV reads +/// (`radv_video.c`, `av1->ref_frames[i].ref_frame_sign_bias`). +fn reference_info( + state: &pf_bitstream::av1::RefState, +) -> Result { + // SAFETY: StdVideoDecodeAV1ReferenceInfo is a plain-C bindgen struct of a + // bitfield word, three small integers and a byte array; all-zero is valid for + // every field. + let mut std: hh::StdVideoDecodeAV1ReferenceInfo = unsafe { std::mem::zeroed() }; + std.flags + .set_disable_frame_end_update_cdf(state.disable_frame_end_update_cdf.into()); + std.flags + .set_segmentation_enabled(state.segmentation_enabled.into()); + std.frame_type = narrow("frame_type", std_frame_type(state.frame_type))?; + std.RefFrameSignBias = state.ref_frame_sign_bias; + std.OrderHint = narrow("OrderHint", state.order_hint)?; + for (dst, hint) in std + .SavedOrderHints + .iter_mut() + .zip(state.saved_order_hints.iter()) + { + // Order hints are `order_hint_bits` wide and that is at most 8, so the + // truncation is unreachable — and it is the same cast `OrderHints` in the + // picture info takes, kept identical on purpose. + *dst = *hint as u8; + } + Ok(std) +} + +/// One frame header (plus the sequence header, for the film-grain gate) into the +/// Std picture info and everything its eight pointers target. +/// +/// Takes the two headers rather than the whole [`AuPlan`] so a hand-built header — +/// film grain, say, which no vendored vector codes — can be converted in a unit +/// test without inventing a plan around it. +fn picture_info( + p: &pf_bitstream::av1::ParsedFrameHeader, + sequence: &pf_bitstream::av1::ParsedSequenceHeader, +) -> Result { + // Tile info, and its four arrays. + let tile = &p.tile_info; + let mi_col_starts: Box<[u16]> = tile.mi_col_starts.iter().map(|v| *v as u16).collect(); + let mi_row_starts: Box<[u16]> = tile.mi_row_starts.iter().map(|v| *v as u16).collect(); + let width_in_sbs: Box<[u16]> = tile + .width_in_sbs_minus_1 + .iter() + .map(|v| *v as u16) + .collect(); + let height_in_sbs: Box<[u16]> = tile + .height_in_sbs_minus_1 + .iter() + .map(|v| *v as u16) + .collect(); + // SAFETY: plain-C bindgen structs throughout this function — a bitfield word, + // integers, fixed arrays and const pointers. All-zero is valid for every field, + // and every pointer is assigned before use. + let mut tile_std: hh::StdVideoAV1TileInfo = unsafe { std::mem::zeroed() }; + tile_std + .flags + .set_uniform_tile_spacing_flag(tile.uniform_tile_spacing_flag.into()); + tile_std.TileCols = narrow("TileCols", tile.tile_cols)?; + tile_std.TileRows = narrow("TileRows", tile.tile_rows)?; + tile_std.context_update_tile_id = tile.context_update_tile_id as u16; + tile_std.tile_size_bytes_minus_1 = narrow( + "tile_size_bytes_minus_1", + tile.tile_size_bytes.saturating_sub(1), + )?; + tile_std.pMiColStarts = mi_col_starts.as_ptr(); + tile_std.pMiRowStarts = mi_row_starts.as_ptr(); + tile_std.pWidthInSbsMinus1 = width_in_sbs.as_ptr(); + tile_std.pHeightInSbsMinus1 = height_in_sbs.as_ptr(); + let tile_info = Box::new(tile_std); + let tile_arrays = TileArrays { + _mi_col_starts: mi_col_starts, + _mi_row_starts: mi_row_starts, + _width_in_sbs_minus_1: width_in_sbs, + _height_in_sbs_minus_1: height_in_sbs, + }; + + // Quantisation. + let q = &p.quantization_params; + // SAFETY: see above. + let mut q_std: hh::StdVideoAV1Quantization = unsafe { std::mem::zeroed() }; + q_std.flags.set_using_qmatrix(q.using_qmatrix.into()); + q_std.flags.set_diff_uv_delta(q.diff_uv_delta.into()); + q_std.base_q_idx = narrow("base_q_idx", q.base_q_idx)?; + q_std.DeltaQYDc = q.delta_q_y_dc as i8; + q_std.DeltaQUDc = q.delta_q_u_dc as i8; + q_std.DeltaQUAc = q.delta_q_u_ac as i8; + q_std.DeltaQVDc = q.delta_q_v_dc as i8; + q_std.DeltaQVAc = q.delta_q_v_ac as i8; + q_std.qm_y = narrow("qm_y", q.qm_y)?; + q_std.qm_u = narrow("qm_u", q.qm_u)?; + q_std.qm_v = narrow("qm_v", q.qm_v)?; + let quantization = Box::new(q_std); + + // Segmentation: an 8x8 enable matrix and its data. + let s = &p.segmentation_params; + // SAFETY: see above. + let mut s_std: hh::StdVideoAV1Segmentation = unsafe { std::mem::zeroed() }; + for (seg, enabled) in s.feature_enabled.iter().enumerate() { + let mut bits = 0u8; + for (feature, on) in enabled.iter().enumerate() { + if *on { + bits |= 1 << feature; + } + } + s_std.FeatureEnabled[seg] = bits; + s_std.FeatureData[seg] = s.feature_data[seg]; + } + let segmentation = Box::new(s_std); + + // Loop filter. + let lf = &p.loop_filter_params; + // SAFETY: see above. + let mut lf_std: hh::StdVideoAV1LoopFilter = unsafe { std::mem::zeroed() }; + lf_std + .flags + .set_loop_filter_delta_enabled(lf.loop_filter_delta_enabled.into()); + lf_std + .flags + .set_loop_filter_delta_update(lf.loop_filter_delta_update.into()); + lf_std.loop_filter_level = lf.loop_filter_level; + lf_std.loop_filter_sharpness = lf.loop_filter_sharpness; + lf_std.loop_filter_ref_deltas = lf.loop_filter_ref_deltas; + lf_std.loop_filter_mode_deltas = lf.loop_filter_mode_deltas; + let loop_filter = Box::new(lf_std); + + // CDEF. + // + // ⚠ The SECONDARY strengths are the CODED two-bit values, and the parser does + // not hold them: AV1 5.9.19 mutates the syntax element in place (`== 3` becomes + // 4) and cros-codecs follows the spec, while libavcodec sends CBS's unmodified + // two-bit read and every driver was validated against that. Sending 4 overflows + // the two bits VA-API, NVDEC and DXVA all give the field, so the strongest + // secondary filter reads back as NO filter. `coded_cdef_sec_strength` is the + // inverse, and its docs carry the four-API evidence. + let c = &p.cdef_params; + // SAFETY: see above. + let mut c_std: hh::StdVideoAV1CDEF = unsafe { std::mem::zeroed() }; + c_std.cdef_damping_minus_3 = narrow("cdef_damping_minus_3", c.cdef_damping.saturating_sub(3))?; + c_std.cdef_bits = narrow("cdef_bits", c.cdef_bits)?; + for i in 0..8 { + c_std.cdef_y_pri_strength[i] = c.cdef_y_pri_strength[i] as u8; + c_std.cdef_y_sec_strength[i] = coded_cdef_sec_strength(c.cdef_y_sec_strength[i]); + c_std.cdef_uv_pri_strength[i] = c.cdef_uv_pri_strength[i] as u8; + c_std.cdef_uv_sec_strength[i] = coded_cdef_sec_strength(c.cdef_uv_sec_strength[i]); + } + let cdef = Box::new(c_std); + + // Loop restoration. + // + // ⚠ `LoopRestorationSize` is NOT the size in pixels. The Vulkan field carries + // the CODED value — RADV names its destination `log2_restoration_size_minus5` + // (`radv_video.c`) and libavcodec sends `1 + lr_unit_shift` (luma) and + // `1 + lr_unit_shift - lr_uv_shift` (chroma) — while the vendored parser + // records the pixel size, 64/128/256. Sending 64 where a driver expects 1 asks + // for a restoration unit of 2^69 pixels. + let lr = &p.loop_restoration_params; + // SAFETY: see above. + let mut lr_std: hh::StdVideoAV1LoopRestoration = unsafe { std::mem::zeroed() }; + let luma_size = 1 + u16::from(lr.lr_unit_shift); + // `lr_uv_shift` is one coded bit (0 or 1) and `luma_size` is at least 1, so the + // saturation is unreachable; it is here so a malformed parse cannot wrap to + // 65535, which a driver would read as log2(size) − 5. + let chroma_size = luma_size.saturating_sub(u16::from(lr.lr_uv_shift)); + for i in 0..3 { + lr_std.FrameRestorationType[i] = lr.frame_restoration_type[i] as u32; + lr_std.LoopRestorationSize[i] = if i == 0 { luma_size } else { chroma_size }; + } + let loop_restoration = Box::new(lr_std); + + // Global motion. + let gm = &p.global_motion_params; + // SAFETY: see above. + let mut gm_std: hh::StdVideoAV1GlobalMotion = unsafe { std::mem::zeroed() }; + for i in 0..NUM_REF_SLOTS { + gm_std.GmType[i] = gm.gm_type[i] as u8; + gm_std.gm_params[i] = gm.gm_params[i]; + } + let global_motion = Box::new(gm_std); + + // Film grain: only where the SEQUENCE enables it and this frame applies it. + // The gate is deliberately both — a zeroed block behind a live pointer would ask + // the decoder to synthesise grain the stream never described. + let film_grain = if sequence.film_grain_params_present && p.film_grain_params.apply_grain { + let fg = &p.film_grain_params; + // SAFETY: see above. + let mut fg_std: hh::StdVideoAV1FilmGrain = unsafe { std::mem::zeroed() }; + fg_std + .flags + .set_chroma_scaling_from_luma(fg.chroma_scaling_from_luma.into()); + fg_std.flags.set_overlap_flag(fg.overlap_flag.into()); + fg_std + .flags + .set_clip_to_restricted_range(fg.clip_to_restricted_range.into()); + fg_std.flags.set_update_grain(fg.update_grain.into()); + fg_std.grain_scaling_minus_8 = fg.grain_scaling_minus_8; + fg_std.ar_coeff_lag = narrow("ar_coeff_lag", fg.ar_coeff_lag)?; + fg_std.ar_coeff_shift_minus_6 = fg.ar_coeff_shift_minus_6; + fg_std.grain_scale_shift = fg.grain_scale_shift; + fg_std.grain_seed = fg.grain_seed; + fg_std.film_grain_params_ref_idx = fg.film_grain_params_ref_idx; + // The chroma scaling function's six coefficients (7.18.3.5 `scaling_lut` + // for the chroma planes). Nothing else describes how luma feeds chroma + // grain, so leaving them zero synthesises grey-drifting chroma noise on + // any stream that codes grain — libavcodec sets all six, and so does this + // program's DXVA conversion. + fg_std.cb_mult = fg.cb_mult; + fg_std.cb_luma_mult = fg.cb_luma_mult; + fg_std.cb_offset = fg.cb_offset; + fg_std.cr_mult = fg.cr_mult; + fg_std.cr_luma_mult = fg.cr_luma_mult; + fg_std.cr_offset = fg.cr_offset; + + // ⚠ The PARSER's point arrays are 16 entries; the Std ones are 14 (luma) and + // 10 (chroma), which are the spec's own maxima. So the counts are checked + // against the STD capacity and the copy is bounded by them — a blind + // array-to-array assignment does not compile here, and a blind + // `copy_from_slice` of 16 into 14 would panic at runtime on a malformed + // stream. Refused rather than truncated: a decoder given fewer scaling + // points than the stream declared synthesises different grain. + let points = + |name: &'static str, count: u8, cap: usize| -> Result { + if usize::from(count) > cap { + return Err(PlanToVkAv1Error::FieldOverflow { + field: name, + value: u32::from(count), + }); + } + Ok(usize::from(count)) + }; + let ny = points("num_y_points", fg.num_y_points, fg_std.point_y_value.len())?; + let ncb = points( + "num_cb_points", + fg.num_cb_points, + fg_std.point_cb_value.len(), + )?; + let ncr = points( + "num_cr_points", + fg.num_cr_points, + fg_std.point_cr_value.len(), + )?; + fg_std.num_y_points = fg.num_y_points; + fg_std.num_cb_points = fg.num_cb_points; + fg_std.num_cr_points = fg.num_cr_points; + fg_std.point_y_value[..ny].copy_from_slice(&fg.point_y_value[..ny]); + fg_std.point_y_scaling[..ny].copy_from_slice(&fg.point_y_scaling[..ny]); + fg_std.point_cb_value[..ncb].copy_from_slice(&fg.point_cb_value[..ncb]); + fg_std.point_cb_scaling[..ncb].copy_from_slice(&fg.point_cb_scaling[..ncb]); + fg_std.point_cr_value[..ncr].copy_from_slice(&fg.point_cr_value[..ncr]); + fg_std.point_cr_scaling[..ncr].copy_from_slice(&fg.point_cr_scaling[..ncr]); + for (dst, src) in fg_std + .ar_coeffs_y_plus_128 + .iter_mut() + .zip(fg.ar_coeffs_y_plus_128.iter()) + { + *dst = *src as i8; + } + for (dst, src) in fg_std + .ar_coeffs_cb_plus_128 + .iter_mut() + .zip(fg.ar_coeffs_cb_plus_128.iter()) + { + *dst = *src as i8; + } + for (dst, src) in fg_std + .ar_coeffs_cr_plus_128 + .iter_mut() + .zip(fg.ar_coeffs_cr_plus_128.iter()) + { + *dst = *src as i8; + } + Some(Box::new(fg_std)) + } else { + None + }; + + // The picture info itself. + // SAFETY: see above. + let mut std: hh::StdVideoDecodeAV1PictureInfo = unsafe { std::mem::zeroed() }; + std.flags + .set_error_resilient_mode(p.error_resilient_mode.into()); + std.flags + .set_disable_cdf_update(p.disable_cdf_update.into()); + std.flags.set_use_superres(p.use_superres.into()); + // The four that CHANGE RECONSTRUCTION and were missing until the M7 review. + // Measured incidence on the vendored 274-frame vector: + // + // * `allow_screen_content_tools` — 274/274 frames, and RADV reads it + // (`av1->pic_flags.allow_screen_content_tools`). It also has to be set for + // `allow_intrabc` below to be coherent: intra block copy is only codeable + // when screen-content tools are on, so the two disagreeing is a contradiction + // a driver is free to resolve either way; + // * `allow_warped_motion` — 273/274; + // * `is_filter_switchable` — 172/274; + // * `force_integer_mv` — 1/274 (the key frame: the parser applies the spec's + // `frame_is_intra ⇒ 1` rule, as libavcodec does for `cur_frame`). + std.flags + .set_allow_screen_content_tools(u32::from(p.allow_screen_content_tools != 0)); + std.flags + .set_allow_warped_motion(p.allow_warped_motion.into()); + std.flags + .set_is_filter_switchable(p.is_filter_switchable.into()); + std.flags + .set_force_integer_mv(u32::from(p.force_integer_mv != 0)); + // The four informational ones libavcodec also sends. No driver in this fleet is + // known to act on them, but they are coded facts about the frame and a decoder + // is entitled to check them against its own parse. + std.flags + .set_render_and_frame_size_different(p.render_and_frame_size_different.into()); + std.flags + .set_frame_size_override_flag(p.frame_size_override_flag.into()); + std.flags + .set_buffer_removal_time_present_flag(p.buffer_removal_time_present_flag.into()); + std.flags + .set_frame_refs_short_signaling(p.frame_refs_short_signaling.into()); + std.flags.set_allow_intrabc(p.allow_intrabc.into()); + std.flags + .set_allow_high_precision_mv(p.allow_high_precision_mv.into()); + std.flags + .set_is_motion_mode_switchable(p.is_motion_mode_switchable.into()); + std.flags.set_use_ref_frame_mvs(p.use_ref_frame_mvs.into()); + std.flags + .set_disable_frame_end_update_cdf(p.disable_frame_end_update_cdf.into()); + std.flags.set_reduced_tx_set(p.reduced_tx_set.into()); + std.flags.set_reference_select(p.reference_select.into()); + std.flags.set_skip_mode_present(p.skip_mode_present.into()); + std.flags + .set_delta_q_present(p.quantization_params.delta_q_present.into()); + std.flags + .set_delta_lf_present(p.loop_filter_params.delta_lf_present.into()); + std.flags + .set_delta_lf_multi(p.loop_filter_params.delta_lf_multi.into()); + std.flags + .set_segmentation_enabled(p.segmentation_params.segmentation_enabled.into()); + std.flags + .set_segmentation_update_map(p.segmentation_params.segmentation_update_map.into()); + std.flags.set_segmentation_temporal_update( + p.segmentation_params.segmentation_temporal_update.into(), + ); + std.flags + .set_segmentation_update_data(p.segmentation_params.segmentation_update_data.into()); + // `UsesLr` is derived, not coded: loop restoration is in use when any plane's + // restoration type is something other than NONE (0). + std.flags.set_UsesLr(u32::from( + lr.frame_restoration_type.iter().any(|t| *t as u32 != 0), + )); + // `usesChromaLr` is deliberately LEFT ZERO, and this is not an oversight. + // + // The AV1 spec's `UsesChromaLr` is `FrameRestorationType[1] != NONE || + // FrameRestorationType[2] != NONE` — the vendored parser even computes it + // (`LoopRestorationParams::uses_chroma_lr`). libavcodec's `vulkan_av1.c` sets + // neither, and libavcodec is the implementation every driver in this fleet was + // validated against: a driver that reads the field at all reads it as zero + // today, and sending a truthful 1 would be the FIRST implementation to do so. + // That is not a bet to take blind on a rung with no on-glass mileage. Revisit + // with a driver-by-driver measurement, not by "fixing" it. + // Kept in step with the `pFilmGrain` gate above by construction: the flag says + // grain is applied exactly when a block describing it is attached. + std.flags.set_apply_grain(u32::from(film_grain.is_some())); + + std.frame_type = std_frame_type(p.frame_type); + std.current_frame_id = p.current_frame_id; + std.OrderHint = narrow("OrderHint", p.order_hint)?; + std.primary_ref_frame = narrow("primary_ref_frame", p.primary_ref_frame)?; + std.refresh_frame_flags = narrow("refresh_frame_flags", p.refresh_frame_flags)?; + std.interpolation_filter = p.interpolation_filter as u32; + std.TxMode = p.tx_mode as u32; + std.delta_q_res = narrow("delta_q_res", p.quantization_params.delta_q_res)?; + std.delta_lf_res = p.loop_filter_params.delta_lf_res; + std.SkipModeFrame = [ + narrow("SkipModeFrame[0]", p.skip_mode_frame[0])?, + narrow("SkipModeFrame[1]", p.skip_mode_frame[1])?, + ]; + // `coded_denom` is the superres denominator as CODED — the spec writes it + // `SUPERRES_DENOM_MIN` less than the real one, and it is only meaningful where + // superres is actually in use. + std.coded_denom = if p.use_superres { + narrow( + "coded_denom", + p.superres_denom.saturating_sub(SUPERRES_DENOM_MIN), + )? + } else { + 0 + }; + for (i, hint) in p.order_hints.iter().enumerate().take(NUM_REF_SLOTS) { + std.OrderHints[i] = *hint as u8; + } + std.pTileInfo = &*tile_info; + std.pQuantization = &*quantization; + std.pSegmentation = &*segmentation; + std.pLoopFilter = &*loop_filter; + std.pCDEF = &*cdef; + std.pLoopRestoration = &*loop_restoration; + std.pGlobalMotion = &*global_motion; + std.pFilmGrain = film_grain + .as_ref() + .map_or(std::ptr::null(), |g| &**g as *const _); + + Ok(OwnedStdAv1PictureInfo { + std, + _tile_info: tile_info, + _tile_arrays: tile_arrays, + _quantization: quantization, + _segmentation: segmentation, + _loop_filter: loop_filter, + _cdef: cdef, + _loop_restoration: loop_restoration, + _global_motion: global_motion, + _film_grain: film_grain, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use cros_codecs::bitstream_utils::IvfIterator; + use pf_bitstream::av1::Av1Planner; + + const AV1_25FPS: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1" + ); + + /// Convert one plan the way a BACKEND must: the conversion, then the releases + /// it defers past the decode op ([`DecodePlanVkAv1::release_after_decode`]). + /// + /// Not a convenience — it is the caller's half of the contract. A loop that + /// converts without it leaks a slot on nearly every frame of this vector (268 + /// of 274) and runs the nine-slot ledger dry inside ten frames, so any test + /// walking the vector has to speak it. + fn convert(plan: &AuPlan, slots: &mut SlotMap) -> DecodePlanVkAv1 { + let vk = plan_to_vk_av1(plan, slots).expect("the clean vector converts"); + for &id in &vk.release_after_decode { + assert!( + slots.release(id), + "a deferred release named picture {id}, which holds no slot" + ); + } + vk + } + + /// Convert every frame of the vendored vector and check the parts a driver + /// reads against each other. + /// + /// The load-bearing assertion is the last one. `referenceNameSlotIndices` holds + /// DPB SLOT indices, not positions in `refs`, and the two coincide for as long + /// as references happen to land in slots `0..refs.len()` in `refs` order — which + /// on a freshly keyed stream they do. That is exactly how the HEVC RPS defect + /// shipped: correct for the first few access units, silently wrong afterwards. + /// So this measures how often the two numberings actually DISAGREE on a real + /// stream, and fails if the answer is never — because then the test is proving + /// nothing and the distinction would be free to rot. + #[test] + fn every_frame_converts_and_slot_indices_are_not_positions() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let (mut frames, mut with_refs) = (0u32, 0u32); + let mut disagreements = 0u32; + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("the clean vector plans") { + if plan.dpb.stored.is_none() { + continue; // show_existing_frame: no submission + } + let vk = convert(&plan, &mut slots); + frames += 1; + + // Every named slot must be one the decode op will bind. + for name in vk.reference_name_slot_indices { + if name == REFERENCE_NAME_UNUSED { + continue; + } + let slot = u8::try_from(name).expect("a slot index is small and positive"); + assert!( + vk.refs.iter().any(|r| r.slot == slot), + "reference name resolves to slot {slot}, which this frame's \ + reference list does not bind" + ); + } + if !vk.refs.is_empty() { + with_refs += 1; + // Would reading these as POSITIONS have given the same answer? + for (name, entry) in vk.reference_name_slot_indices.iter().enumerate() { + if *entry == REFERENCE_NAME_UNUSED { + continue; + } + let as_position = vk.refs.get(name).map(|r| i32::from(r.slot)); + if as_position != Some(*entry) { + disagreements += 1; + } + } + } + // No reference may share the decode target's slot — the assertion + // the H.264 and H.265 conversion tests have carried since M2, and + // the one this file was missing. A frame whose own refresh + // displaces a picture it READS had its slot recycled straight into + // `setup_slot`, so `refs` named the slot the decode target + // activates: the hardware would predict from the picture it is in + // the middle of writing. Frame 6 of this vector does it. + for r in &vk.refs { + assert_ne!( + r.slot, vk.setup_slot, + "frame {frames}: reference (picture {}) aliases the setup slot", + r.id + ); + } + // The setup picture must hold the slot the plan says it does. + assert_eq!(slots.slot_of(vk.setup_id), Some(vk.setup_slot)); + } + } + + assert_eq!(frames, 274, "every frame of the vector must convert"); + assert!(with_refs > 0, "a 274-frame vector must reference something"); + assert!( + disagreements > 0, + "slot indices and reference-list positions never disagreed on this \ + vector, so this test cannot tell the two conventions apart — the same \ + blind spot that let the HEVC RPS defect ship" + ); + eprintln!( + "frames {frames} · with refs {with_refs} · slot-vs-position disagreements \ + {disagreements}" + ); + } + + /// A frame that READS a slot its own refresh overwrites keeps that slot until + /// the decode op is recorded — and the incidence is pinned, because it is the + /// ordinary case rather than the exotic one. + /// + /// AV1 applies `refresh_frame_flags` after decoding (7.20), so `ref_frame_idx` + /// resolves against the store as it stood BEFORE the frame. Cycling eight slots + /// in a low-delay stream therefore means almost every frame displaces something + /// it is reading: **268 of this vector's 274 frames**, first at frame 6. The + /// H.264 and H.265 planners can produce the same shape — `plan_to_vk`'s own + /// docs name the sliding window evicting a picture the slices reference — but + /// neither vendored vector ever does it (measured: zero on the 250-AU H.264 + /// clip), which is why the hole survived two hardware-proven codecs and opened + /// on the first AV1 frame that was not a key frame's neighbour. + #[test] + fn a_reference_this_frame_displaces_keeps_its_slot_until_after_the_decode() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let (mut frames, mut deferring, mut deferred_ids) = (0u32, 0u32, 0u32); + let mut peak_active = 0usize; + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("the clean vector plans") { + if plan.dpb.stored.is_none() { + continue; + } + let vk = plan_to_vk_av1(&plan, &mut slots).expect("converts"); + frames += 1; + peak_active = peak_active.max(slots.active()); + + // Every deferred id is one this plan really removed AND this frame + // really reads — never an invented removal, never a live picture. + for &id in &vk.release_after_decode { + assert!( + plan.dpb.removed.contains(&id), + "frame {frames}: deferred picture {id} is not in this plan's \ + removed list" + ); + assert!( + vk.refs.iter().any(|r| r.id == id), + "frame {frames}: picture {id} is deferred without being read — \ + only a reference of THIS frame earns the reprieve" + ); + assert!( + slots.slot_of(id).is_some(), + "frame {frames}: a deferred picture must still hold its slot" + ); + } + // And the whole point: the slot it still holds is not the one the + // decode target just took. + for r in &vk.refs { + assert_ne!(r.slot, vk.setup_slot); + } + if !vk.release_after_decode.is_empty() { + deferring += 1; + deferred_ids += vk.release_after_decode.len() as u32; + } + for &id in &vk.release_after_decode { + assert!(slots.release(id)); + } + } + } + + assert_eq!(frames, 274); + assert_eq!( + deferring, 268, + "268 of 274 frames of this vector displace a picture they are reading; \ + at zero this test compares an empty list against itself and the \ + deferral could be deleted without a single assertion noticing" + ); + assert_eq!(deferred_ids, 268, "one displaced reference per frame here"); + assert!( + peak_active <= slots.capacity(), + "deferring a release must not overrun the ledger" + ); + // The nine-slot ledger is `NUM_REF_SLOTS + 1` and holding a displaced + // reference one frame longer is exactly what that spare slot is for. If + // this ever reaches capacity the sizing argument needs re-reading, not a + // bigger number. + eprintln!("frames {frames} · deferring {deferring} · peak slots held {peak_active}"); + } + + /// Every `StdVideoDecodeAV1PictureInfoFlags` bit this conversion is responsible + /// for, checked against the parsed header on all 274 frames — with the + /// INCIDENCE of each pinned, so a bit that silently stopped being written + /// fails here. + /// + /// Nine of these were unset when M7 first landed, and four of them change + /// reconstruction. A test that only asserted "flag == header field" would have + /// passed just as happily against a conversion that wrote neither, which is why + /// the counts below are assertions and not `eprintln!`s. + #[test] + fn every_picture_info_flag_matches_the_header_and_the_incidence_is_pinned() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let mut frames = 0u32; + let (mut screen, mut warped, mut switchable, mut integer_mv) = (0u32, 0u32, 0u32, 0u32); + let (mut informational, mut intrabc) = (0u32, 0u32); + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("the clean vector plans") { + if plan.dpb.stored.is_none() { + continue; + } + let vk = convert(&plan, &mut slots); + let f = &vk.pic.std().flags; + let p = &*plan.header; + frames += 1; + + let bit = |b: bool| u32::from(b); + // --- the four that change reconstruction --- + assert_eq!( + f.allow_screen_content_tools(), + bit(p.allow_screen_content_tools != 0) + ); + assert_eq!(f.allow_warped_motion(), bit(p.allow_warped_motion)); + assert_eq!(f.is_filter_switchable(), bit(p.is_filter_switchable)); + assert_eq!(f.force_integer_mv(), bit(p.force_integer_mv != 0)); + // Intra block copy is only codeable where screen-content tools are + // on, so a frame claiming intrabc without them is a contradiction a + // driver resolves however it likes. + if f.allow_intrabc() == 1 { + assert_eq!( + f.allow_screen_content_tools(), + 1, + "allow_intrabc without allow_screen_content_tools" + ); + intrabc += 1; + } + // --- the four informational ones libavcodec also sends --- + assert_eq!( + f.render_and_frame_size_different(), + bit(p.render_and_frame_size_different) + ); + assert_eq!( + f.frame_size_override_flag(), + bit(p.frame_size_override_flag) + ); + assert_eq!( + f.buffer_removal_time_present_flag(), + bit(p.buffer_removal_time_present_flag) + ); + assert_eq!( + f.frame_refs_short_signaling(), + bit(p.frame_refs_short_signaling) + ); + informational += f.render_and_frame_size_different() + + f.frame_size_override_flag() + + f.buffer_removal_time_present_flag() + + f.frame_refs_short_signaling(); + // --- the twenty that were already right --- + assert_eq!(f.error_resilient_mode(), bit(p.error_resilient_mode)); + assert_eq!(f.disable_cdf_update(), bit(p.disable_cdf_update)); + assert_eq!(f.use_superres(), bit(p.use_superres)); + assert_eq!(f.allow_high_precision_mv(), bit(p.allow_high_precision_mv)); + assert_eq!( + f.is_motion_mode_switchable(), + bit(p.is_motion_mode_switchable) + ); + assert_eq!(f.use_ref_frame_mvs(), bit(p.use_ref_frame_mvs)); + assert_eq!( + f.disable_frame_end_update_cdf(), + bit(p.disable_frame_end_update_cdf) + ); + assert_eq!(f.reduced_tx_set(), bit(p.reduced_tx_set)); + assert_eq!(f.reference_select(), bit(p.reference_select)); + assert_eq!(f.skip_mode_present(), bit(p.skip_mode_present)); + assert_eq!( + f.segmentation_enabled(), + bit(p.segmentation_params.segmentation_enabled) + ); + // ⚠ `usesChromaLr` is deliberately zero even where the spec would + // want it — see picture_info. Asserted so "fixing" it trips here + // and the reasoning gets read. + assert_eq!( + f.usesChromaLr(), + 0, + "usesChromaLr is deliberately left at libavcodec's zero" + ); + + screen += f.allow_screen_content_tools(); + warped += f.allow_warped_motion(); + switchable += f.is_filter_switchable(); + integer_mv += f.force_integer_mv(); + } + } + + assert_eq!(frames, 274); + // Measured on this vector. These are what make the four assertions above + // real: a conversion that never set them would report zero. + assert_eq!(screen, 274, "allow_screen_content_tools: 274/274"); + assert_eq!(warped, 273, "allow_warped_motion: 273/274"); + assert_eq!(switchable, 172, "is_filter_switchable: 172/274"); + assert_eq!(integer_mv, 1, "force_integer_mv: the key frame only"); + assert!(intrabc <= frames); + // ⚠ Honest gap: this vector codes none of the four informational flags, so + // their assertions above compare 0 against 0. They are covered by review + // and by the libavcodec cross-read, not by this measurement. + assert_eq!( + informational, 0, + "if this ever fires, the informational flags ARE exercised — say so \ + here rather than deleting the count" + ); + } + + /// `StdVideoAV1LoopFilter` carries FOUR levels, and the last two are chroma. + /// + /// ⚠ **Read the history before trusting an older comment about this field.** + /// The AV1 rung's frame-0 divergence was `luma IDENTICAL, chroma 319/38400 + /// bytes differ, max |delta| 4` on an RTX 5070 Ti (610.57.04), and re-decoding + /// frame 0 in software with `loop_filter_level[2]` and `[3]` forced to zero + /// reproduced it exactly — same 319 bytes, same `1:219 2:64 3-4:36` histogram, + /// same first six differing bytes. That reading was right about the SYMPTOM + /// and wrong about the cause: the conclusion drawn from it, that the driver + /// ignores these two levels, is **refuted**. It reads them. What it also read, + /// at every `vkCmdDecodeVideoKHR`, was a FREED sequence header whose recycled + /// bytes happened to say `mono_chrome = 1` — so it deblocked the frame as + /// monochrome, which skips exactly `loop_filter_level[2..3]` (7.14) and + /// nothing else. [`crate::session_av1`] carries that measurement and the fix; + /// with the backing held, all 250 frames are bit-identical to libavcodec. + /// + /// So this stays, as the guard it always was rather than as evidence for a + /// driver claim. The conversion is a whole-array assignment and the test + /// therefore looks tautological. It is not: `loop_filter_level` is the ONE Std + /// array whose entries mean different things at different indices — `[0]` and + /// `[1]` are the luma passes, `[2]` and `[3]` are U and V — and both of the + /// other rungs spell it as a two-entry array plus two named fields + /// (`filter_level_u` / `filter_level_v` in DXVA, the same in VA-API). A + /// conversion that copied "the levels" as a pair is the natural mistake, it is + /// what the DXVA layout invites, and nothing else in this file would notice. + /// The values below are additionally confirmed against what libavcodec's own + /// Vulkan hwaccel puts on the wire for this vector, captured at the API with a + /// layer: `03 00 00 00 01 07 08 0c 00 00 01 00 00 00 ff 00 ff ff 00 00 …`. + #[test] + fn the_chroma_deblocking_levels_are_the_last_two_of_four() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let (mut frames, mut with_chroma_lf) = (0u32, 0u32); + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("plans") { + if plan.dpb.stored.is_none() { + continue; + } + let vk = convert(&plan, &mut slots); + frames += 1; + let lf = &plan.header.loop_filter_params; + // SAFETY: `pLoopFilter` points at the boxed block `vk.pic` owns, + // alive for as long as `vk` is. + let sent = unsafe { *vk.pic.std().pLoopFilter }; + assert_eq!( + sent.loop_filter_level, lf.loop_filter_level, + "frame {frames}: all four levels, in order — [0] and [1] the \ + luma passes, [2] U, [3] V" + ); + assert_eq!(sent.loop_filter_sharpness, lf.loop_filter_sharpness); + assert_eq!(sent.loop_filter_ref_deltas, lf.loop_filter_ref_deltas); + assert_eq!(sent.loop_filter_mode_deltas, lf.loop_filter_mode_deltas); + if lf.loop_filter_level[2] != 0 || lf.loop_filter_level[3] != 0 { + with_chroma_lf += 1; + } + if frames == 1 { + assert_eq!( + sent.loop_filter_level, + [1, 7, 8, 12], + "frame 0's levels, and the four bytes libavcodec's Vulkan \ + hwaccel was captured sending for this same frame" + ); + assert_eq!( + sent.loop_filter_ref_deltas, + [1, 0, 0, 0, -1, 0, -1, -1], + "a PRIMARY_REF_NONE frame gets the spec's defaults from \ + setup_past_independence, and they bump every level by one — \ + luma included, which is how the parity leg's bit-exact luma \ + proves the driver read the deltas and the first two levels" + ); + } + } + } + + assert_eq!(frames, 274); + assert_eq!( + with_chroma_lf, 123, + "123 of 274 frames of this vector deblock chroma; at zero every level \ + compared above would be zero on both sides and a conversion that sent \ + only the luma pair would pass" + ); + } + + /// `StdVideoAV1CDEF`'s secondary strengths carry the CODED two-bit value. + /// + /// The defect this pins is the twin of the `LoopRestorationSize` one below: + /// the vendored parser stores what the AV1 SPEC leaves in the variable after + /// its in-place fixup (`== 3` becomes 4), and every decode API — Vulkan + /// included, because libavcodec sends CBS's unmodified two-bit read — wants the + /// value BEFORE it. It is worse than the loop-restoration one in exactly one + /// way: 4 is not an absurd number a driver would reject, it is a number that + /// overflows a two-bit field into 0, so the strongest secondary CDEF filter + /// becomes NO secondary filter and the frame is merely slightly wrong. + /// + /// Frame 0 of the vector codes it, which is why this was the AV1 parity leg's + /// FIRST divergent frame, and CDEF is in-loop, which is why every frame after + /// it diverged too. + #[test] + fn cdef_secondary_strengths_are_the_coded_value_not_the_spec_fixup() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let (mut frames, mut corrected_frames) = (0u32, 0u32); + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("plans") { + if plan.dpb.stored.is_none() { + continue; + } + let vk = convert(&plan, &mut slots); + frames += 1; + let raw = &plan.header.cdef_params; + // SAFETY: `pCDEF` points at the boxed block `vk.pic` owns, alive + // for as long as `vk` is. + let sent = unsafe { *vk.pic.std().pCDEF }; + let mut corrected = false; + for i in 0..8 { + // Two bits is all any hardware API gives this field. + assert!( + sent.cdef_y_sec_strength[i] <= 3 && sent.cdef_uv_sec_strength[i] <= 3, + "frame {frames}: a secondary strength above 3 overflows the \ + two bits VA-API, NVDEC and DXVA pack it into" + ); + // The primaries are NOT fixed up by the spec and must reach the + // driver untouched — a correction applied to the wrong one of + // the four arrays would be just as silent. + assert_eq!( + u32::from(sent.cdef_y_pri_strength[i]), + raw.cdef_y_pri_strength[i] + ); + assert_eq!( + u32::from(sent.cdef_uv_pri_strength[i]), + raw.cdef_uv_pri_strength[i] + ); + if raw.cdef_y_sec_strength[i] == 4 || raw.cdef_uv_sec_strength[i] == 4 { + corrected = true; + } + } + if corrected { + corrected_frames += 1; + } + if frames == 1 { + let coded = 1usize << raw.cdef_bits; + assert_eq!(coded, 4, "frame 0 codes cdef_bits = 2"); + assert_eq!( + ( + &sent.cdef_y_sec_strength[..coded], + &sent.cdef_uv_sec_strength[..coded] + ), + (&[1u8, 2, 0, 3][..], &[3u8, 0, 0, 0][..]), + "frame 0's secondary strengths as libavcodec sends them — \ + the parser holds 4 where these read 3" + ); + } + } + } + + assert_eq!(frames, 274); + assert_eq!( + corrected_frames, 68, + "68 of 274 frames of this vector need the correction; at zero this test \ + compares an untouched conversion against itself" + ); + } + + /// `LoopRestorationSize` carries the CODED value, not the pixel size. + /// + /// Three frames of the vector switch loop restoration on, at + /// `lr_unit_shift = 1` / `lr_uv_shift = 0` — a 128-pixel unit whose coded value + /// is 2. Sending 128 (what the parser stores, and what this conversion sent + /// until the M7 review) asks a driver that reads the field as + /// `log2_restoration_size_minus5` for a restoration unit of 2^133 pixels. + #[test] + fn loop_restoration_size_is_the_coded_value_not_the_pixel_size() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let (mut frames, mut with_lr) = (0u32, 0u32); + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("plans") { + if plan.dpb.stored.is_none() { + continue; + } + let vk = convert(&plan, &mut slots); + frames += 1; + let lr = &plan.header.loop_restoration_params; + // SAFETY: `pLoopRestoration` points at the boxed block `vk.pic` + // owns, alive for as long as `vk` is. + let sizes = unsafe { (*vk.pic.std().pLoopRestoration).LoopRestorationSize }; + assert_eq!( + sizes[0], + 1 + u16::from(lr.lr_unit_shift), + "luma: libavcodec sends 1 + lr_unit_shift" + ); + let chroma = 1 + u16::from(lr.lr_unit_shift) - u16::from(lr.lr_uv_shift); + assert_eq!(sizes[1], chroma); + assert_eq!(sizes[2], chroma); + if lr.uses_lr { + with_lr += 1; + assert_eq!(lr.loop_restoration_size, [128, 128, 128]); + assert_eq!(sizes, [2, 2, 2]); + assert_ne!( + sizes[0], lr.loop_restoration_size[0], + "the coded value and the pixel size must differ here, or \ + this test cannot tell them apart" + ); + } + } + } + assert_eq!(frames, 274); + assert_eq!( + with_lr, 3, + "three frames of this vector use loop restoration; at zero the \ + assertions above only ever saw the off state" + ); + } + + /// A reference's Std info must describe the REFERENCE, not the frame reading + /// it — and `RefFrameSignBias` must actually carry the future references this + /// vector is full of. + #[test] + fn reference_info_describes_the_reference_and_not_the_current_frame() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let mut frames = 0u32; + let (mut mixed_types, mut biased, mut with_saved_hints) = (0u32, 0u32, 0u32); + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("plans") { + if plan.dpb.stored.is_none() { + continue; + } + let vk = convert(&plan, &mut slots); + frames += 1; + let current_type = plan.header.frame_type as u8; + + for r in &vk.refs { + let by_id = plan + .refs + .iter() + .flatten() + .find(|p| p.id == r.id) + .expect("every vk ref came from a named plan reference"); + assert_eq!(r.std.frame_type, by_id.state.frame_type as u8); + assert_eq!(r.std.RefFrameSignBias, by_id.state.ref_frame_sign_bias); + assert_eq!( + r.std.flags.disable_frame_end_update_cdf(), + u32::from(by_id.state.disable_frame_end_update_cdf) + ); + assert_eq!( + r.std.flags.segmentation_enabled(), + u32::from(by_id.state.segmentation_enabled) + ); + assert_eq!(r.std.OrderHint, by_id.state.order_hint as u8); + for (sent, want) in r + .std + .SavedOrderHints + .iter() + .zip(by_id.state.saved_order_hints.iter()) + { + assert_eq!(u32::from(*sent), *want); + } + + if r.std.frame_type != current_type { + mixed_types += 1; + } + if r.std.RefFrameSignBias != 0 { + biased += 1; + } + if r.std.SavedOrderHints.iter().any(|h| *h != 0) { + with_saved_hints += 1; + } + } + // The setup picture activates a slot and is cached as that slot's + // reference info, so it must carry the current frame's own state + // through the very same path. + let own = pf_bitstream::av1::RefState::of(&plan.header); + assert_eq!(vk.setup_ref.frame_type, own.frame_type as u8); + assert_eq!(vk.setup_ref.RefFrameSignBias, own.ref_frame_sign_bias); + assert_eq!(vk.setup_ref.OrderHint, own.order_hint as u8); + } + } + + assert_eq!(frames, 274); + assert!( + mixed_types > 0, + "no reference ever had a different frame type from the frame reading \ + it, so handing every reference the CURRENT type would have passed" + ); + assert!( + biased > 0, + "no reference carried a sign bias: this is the hidden-ALTREF vector, \ + so a zero here means the mask never reached the Std struct and every \ + future reference reads as past" + ); + assert!(with_saved_hints > 0, "SavedOrderHints never carried a hint"); + eprintln!( + "refs with a foreign frame type {mixed_types} · with a sign bias \ + {biased} · with saved order hints {with_saved_hints}" + ); + } + + /// Film grain's six chroma-scaling coefficients reach the Std block. + /// + /// ⚠ The vendored vector codes NO film grain (`film_grain_params_present` is + /// false on all 274 frames), so this is a hand-built header — the only way the + /// grain path is exercised at all. It is also why the six fields could go + /// missing unnoticed: nothing that runs on the vector touches them. + #[test] + fn film_grain_carries_the_chroma_scaling_coefficients() { + let mut sequence = pf_bitstream::av1::ParsedSequenceHeader { + film_grain_params_present: true, + ..Default::default() + }; + + let mut header = pf_bitstream::av1::ParsedFrameHeader::default(); + let fg = &mut header.film_grain_params; + fg.apply_grain = true; + fg.grain_seed = 0x1234; + fg.num_y_points = 2; + fg.num_cb_points = 1; + fg.num_cr_points = 1; + fg.cb_mult = 128; + fg.cb_luma_mult = 192; + fg.cb_offset = 256; + fg.cr_mult = 129; + fg.cr_luma_mult = 193; + fg.cr_offset = 257; + + let pic = picture_info(&header, &sequence).expect("a grain header converts"); + assert_eq!(pic.std().flags.apply_grain(), 1); + assert!(!pic.std().pFilmGrain.is_null()); + // SAFETY: `pFilmGrain` points at the boxed block `pic` owns, alive here. + let grain = unsafe { *pic.std().pFilmGrain }; + assert_eq!(grain.grain_seed, 0x1234); + assert_eq!( + ( + grain.cb_mult, + grain.cb_luma_mult, + grain.cb_offset, + grain.cr_mult, + grain.cr_luma_mult, + grain.cr_offset + ), + (128, 192, 256, 129, 193, 257), + "the six chroma-scaling coefficients: nothing else describes how luma \ + feeds chroma grain, and zeroes are not 'less grain', they are \ + different grain" + ); + + // And the gate still holds: a sequence that never declared grain gets a + // null block whatever the frame says. + sequence.film_grain_params_present = false; + let pic = picture_info(&header, &sequence).expect("converts"); + assert!(pic.std().pFilmGrain.is_null()); + assert_eq!(pic.std().flags.apply_grain(), 0); + } +} diff --git a/crates/pf-vkdecode/src/pic_h265.rs b/crates/pf-vkdecode/src/pic_h265.rs new file mode 100644 index 00000000..896d1f80 --- /dev/null +++ b/crates/pf-vkdecode/src/pic_h265.rs @@ -0,0 +1,1208 @@ +//! Per-AU H.265 conversion: one [`AuPlan`] into the `StdVideoDecodeH265*` structs, +//! slice offsets and DPB slot bindings a `vkCmdDecodeVideoKHR` call is built from — +//! [`crate::pic`] one codec over (M3's CPU half; the session/recording half is a +//! later WP). +//! +//! The codec difference that shapes this module: Vulkan H.265 decode takes NO +//! per-slice reference lists. The hardware re-derives 8.3.4's lists itself from +//! the slice bits, keyed by the picture-level RPS index arrays +//! (`RefPicSetStCurrBefore`/`StCurrAfter`/`LtCurr`) — so the AU-level binding set +//! here is the union of the plan's three CURRENT reference picture sets +//! ([`pf_bitstream::h265::RpsPlan`]), not the union of slice lists as in H.264. +//! The plan's per-slice lists still exist and are used as a cross-check: every +//! list entry must be a member of the binding set, or the conversion fails closed. +//! +//! Those arrays carry **DPB slot indices**, not positions in the decode op's +//! reference list — the distinction that +//! [`DecodePlanVkH265::std_pic`] documents at length, because the two readings +//! coincide on a freshly anchored stream and diverge a handful of AUs later, which +//! makes getting it wrong silent corruption rather than an error. +//! +//! Concealment note: a lost reference is ABSENT from the plan's RPS sets (flagged +//! upstream via `PlanWarning::MissingReference`), so the Std index arrays compact +//! past it — later positions shift by one relative to the damaged stream's +//! intent. That is deliberate: there is no slot to point at, `0xFF` padding keeps +//! the arrays well-formed, and the session layer has already been told to request +//! recovery. The alternative (fabricating an entry) is exactly what this crate +//! never does. + +use ash::vk::native as hh; +use pf_bitstream::h265::AuPlan; +use pf_bitstream::h265::PicId; +use pf_bitstream::h265::RefPic; +use tracing::trace; + +use crate::slots::SlotError; +use crate::slots::SlotMap; + +/// `STD_VIDEO_DECODE_H265_REF_PIC_SET_LIST_SIZE`: each Std RPS index array holds +/// eight entries — the hard ceiling on how many CURRENT references one set may +/// carry through Vulkan (the spec itself allows up to 16 per side; beyond eight +/// is unexpressible and rejected, see [`PlanToVkH265Error::RpsSetOverflow`]). +pub const H265_RPS_LIST_SIZE: usize = 8; + +/// The Std sentinel for an unused RPS index-array entry. +const UNUSED_RPS_ENTRY: u8 = 0xFF; + +/// One active reference of the AU: its DPB slot, its Std reference info, and the +/// planner id it resolves (kept so the backend can map the slot to its image). +#[derive(Debug, Clone)] +pub struct VkRefH265 { + pub slot: u8, + pub std: hh::StdVideoDecodeH265ReferenceInfo, + pub id: PicId, +} + +/// Everything CPU-derivable of one AU's decode submission. The GPU half adds the +/// live objects: bitstream buffer, DPB images, session and command recording. +#[derive(Debug, Clone)] +pub struct DecodePlanVkH265 { + /// The picture info. Its `RefPicSetStCurrBefore`/`StCurrAfter`/`LtCurr` + /// arrays hold **DPB SLOT INDICES** (`0xFF` = unused) — the same numbers + /// `VkVideoReferenceSlotInfoKHR::slotIndex` carries, NOT positions in + /// [`Self::refs`] / `pDecodeInfo->pReferenceSlots`. + /// + /// This is the one place the two readings can be confused, and confusing + /// them is silent corruption rather than an error: they COINCIDE for as long + /// as the referenced pictures happen to sit in slots `0..refs.len()` in + /// `refs` order, which on a fresh IDR they do, so a stream decodes correctly + /// for its first few AUs and then quietly references the wrong pictures. + /// (On the vendored 25fps H.265 vector that is exactly AUs 0-2 correct and + /// AU 3 onwards wrong — its first B picture with two `StCurrAfter` entries + /// binds slots 2 and 1 in that order, so `refs` positions 1,2 and slots 2,1 + /// disagree.) + /// + /// The authority is libavcodec's Vulkan H.265 hwaccel, which every driver is + /// validated against: it fills these arrays with the index of the picture in + /// its own DPB array and hands that SAME index to `slotIndex`, while packing + /// `pReferenceSlots` densely over only the USED entries — so the two arrays + /// are provably different numberings there, and the RPS one follows the slot. + /// + /// The backend must still lay `pReferenceSlots` out in [`Self::refs`] order + /// and fail closed on a reference with no bound image — not because the + /// indices depend on the order any more, but because a slot these arrays + /// name that the decode op never binds is unresolvable for the hardware. + pub std_pic: hh::StdVideoDecodeH265PictureInfo, + /// Byte offset of each slice segment NALU in the AU as planned, START CODE + /// INCLUDED — exactly one entry per slice segment of the AU, in plan order + /// (so the recording layer's own rebased array, built by walking the same + /// slices, matches this length by construction). + /// AU-relative, NOT submission-final: the recording layer must + /// pack the SLICE NALUs alone into the bitstream buffer and rebase these + /// offsets while doing so — non-VCL NALUs inside the decode range hang VCN + /// firmware (the H.264 decoder's slices-only packing exists for exactly + /// that `vcn_unified_0` ring timeout; FFmpeg feeds slices-only for the same + /// reason). Vulkan's `pSliceSegmentOffsets` then receives the REBASED + /// offsets, each pointing at a start code within the packed buffer. + pub slice_offsets: Vec, + /// The slot the decoded picture activates (`pSetupReferenceSlot`). + pub setup_slot: u8, + /// Reference info for the setup slot: the picture's own POC, short-term. + /// HEVC has no same-AU self-marking (H.264's IDR long_term_reference_flag / + /// MMCO 6): C.3.4 marks every stored picture "used for short-term reference", + /// and a picture turns long-term only when a LATER picture's RPS lists it in + /// `RefPicSetLtCurr` — at which point that AU's [`Self::refs`] entry carries + /// the long-term flag. + pub setup_ref: hh::StdVideoDecodeH265ReferenceInfo, + /// The planner id of the decoded picture (`AuPlan.dpb.stored`) — the backend + /// keys its image bookkeeping by it. + pub setup_id: PicId, + /// Whether the decoded picture may be referenced by later pictures (false + /// for sub-layer non-reference NALU types, RASL_N/TRAIL_N and friends). When + /// `false` the setup slot exists for the decode itself plus any remaining + /// DPB residency, and must never be bound as a reference for later AUs. + pub setup_is_reference: bool, + /// The unique referenced pictures of this AU — the union of the plan's three + /// current RPS sets in set order (StCurrBefore, StCurrAfter, LtCurr), first + /// appearance first. Every slot [`Self::std_pic`]'s index arrays name appears + /// here exactly once, which is what makes those arrays resolvable against the + /// decode op's reference list. + pub refs: Vec, +} + +/// Conversion failures. Stream damage never lands here — pf-bitstream degrades +/// it to [`pf_bitstream::h265::PlanWarning`]s upstream; these are caller/session +/// bugs or envelope limits. (`PlanError::RaslSkipped` also never reaches this +/// layer: it is an error OF planning, handled as an Ok-skip by the client +/// wiring, and no plan exists to convert.) +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanToVkH265Error { + /// The plan holds no slices; there is nothing to submit. + NoSlices, + /// The plan's `DpbUpdate.stored` is `None`. `plan_au` always stores; only + /// `flush()` produces such updates, and those go to [`SlotMap::apply`] + /// directly. + NoStoredId, + /// An RPS entry's id holds no slot: an earlier plan of this stream never + /// went through this [`SlotMap`]. + UnresolvedReference(PicId), + /// A slice reference-list entry names a picture outside the plan's current + /// RPS sets. 8.3.4 builds every list FROM those sets, so this is a planner + /// contract violation — the picture would be missing from + /// `pReferenceSlots` and the hardware could not resolve it. + ReferenceOutsideRps(PicId), + Slot(SlotError), + /// A slice offset exceeds `u32` (Vulkan submits offsets as `u32`). + OffsetOverflow(usize), + /// A current RPS set holds more entries than the Std index arrays' eight + /// ([`H265_RPS_LIST_SIZE`]) — expressible in H.265, not in Vulkan; outside + /// the program envelope (punktfunk hosts keep well under it). + RpsSetOverflow { + set: &'static str, + len: usize, + }, + /// The first slice's inline `st_ref_pic_set()` predicts from an SPS + /// candidate that does not exist — `NumDeltaPocsOfRefRpsIdx` cannot be + /// derived, and the hardware would misparse the slice header. + InvalidRefRpsIdx { + curr_rps_idx: u8, + delta_idx_minus1: u8, + }, + /// The inline `st_ref_pic_set()`'s bit count exceeds `u16` (the Std field + /// `NumBitsForSTRefPicSetInSlice`) — a header that large is corrupt. + StRpsBitsOverflow(u32), + /// The predicted-from candidate's `NumDeltaPocs` exceeds `u8` (the Std + /// field `NumDeltaPocsOfRefRpsIdx`). Impossible off a real parse (≤ 32); + /// a directly-constructed plan gets an error, never a clamped count the + /// hardware would misparse the slice header with. + NumDeltaPocsOverflow(u32), + /// The map was built for a different DPB depth than this plan's + /// `max_dpb_frames` — an SPS renegotiation resized the DPB. The session + /// must rebuild the video session and its [`SlotMap`]; converting against + /// the stale map would hand out slot indices the session's image pool does + /// not have (the H.264 module's exact contract). + CapacityMismatch { + required: usize, + capacity: usize, + }, +} + +impl std::fmt::Display for PlanToVkH265Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlanToVkH265Error::NoSlices => write!(f, "the plan holds no slices"), + PlanToVkH265Error::NoStoredId => { + write!( + f, + "the plan stores no picture (flush updates go to SlotMap::apply)" + ) + } + PlanToVkH265Error::UnresolvedReference(id) => { + write!(f, "referenced picture {id} holds no DPB slot in this map") + } + PlanToVkH265Error::ReferenceOutsideRps(id) => { + write!( + f, + "slice list references picture {id} outside the current RPS sets" + ) + } + PlanToVkH265Error::Slot(err) => write!(f, "slot assignment failed: {err}"), + PlanToVkH265Error::OffsetOverflow(offset) => { + write!(f, "slice offset {offset} exceeds u32") + } + PlanToVkH265Error::RpsSetOverflow { set, len } => { + write!( + f, + "{set} holds {len} entries; Vulkan expresses at most {H265_RPS_LIST_SIZE}" + ) + } + PlanToVkH265Error::InvalidRefRpsIdx { + curr_rps_idx, + delta_idx_minus1, + } => { + write!( + f, + "inline st_ref_pic_set predicts from a nonexistent candidate \ + (CurrRpsIdx {curr_rps_idx}, delta_idx_minus1 {delta_idx_minus1})" + ) + } + PlanToVkH265Error::StRpsBitsOverflow(bits) => { + write!(f, "st_ref_pic_set bit count {bits} exceeds u16") + } + PlanToVkH265Error::NumDeltaPocsOverflow(count) => { + write!(f, "candidate NumDeltaPocs {count} exceeds u8") + } + PlanToVkH265Error::CapacityMismatch { required, capacity } => { + write!( + f, + "the plan needs {required} slots but the map holds {capacity} — \ + an SPS renegotiation resized the DPB; rebuild session and map" + ) + } + } + } +} + +impl std::error::Error for PlanToVkH265Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + PlanToVkH265Error::Slot(err) => Some(err), + _ => None, + } + } +} + +impl From for PlanToVkH265Error { + fn from(err: SlotError) -> Self { + PlanToVkH265Error::Slot(err) + } +} + +/// One [`RefPic`] as Std reference info. +fn ref_info(rp: &RefPic) -> hh::StdVideoDecodeH265ReferenceInfo { + // SAFETY: StdVideoDecodeH265ReferenceInfo is a plain-C bindgen struct of a + // bitfield word and one integer; all-zero is a valid value for every field. + let mut std: hh::StdVideoDecodeH265ReferenceInfo = unsafe { std::mem::zeroed() }; + std.flags + .set_used_for_long_term_reference(u32::from(rp.is_long_term)); + // unused_for_reference stays 0: membership in a CURRENT set is the + // definition of being used for reference by this picture. + std.PicOrderCntVal = rp.pic_order_cnt; + std +} + +/// `NumDeltaPocsOfRefRpsIdx` (the Std picture-info field): when the first +/// slice's inline `st_ref_pic_set()` uses inter-RPS prediction, the hardware +/// re-parses those slice bits and needs `NumDeltaPocs[RefRpsIdx]` of the SOURCE +/// candidate to size the `used_by_curr_pic_flag`/`use_delta_flag` loop (7.4.8); +/// otherwise 0. +fn num_delta_pocs_of_ref_rps_idx(plan: &AuPlan) -> Result { + let hdr = &plan + .slices + .first() + .expect("caller validated the plan holds slices") + .header; + // Inline means CurrRpsIdx == num_short_term_ref_pic_sets (8.3.2 NOTE 2); + // an SPS-indexed RPS re-parses nothing in the slice header. + let inline = !hdr.short_term_ref_pic_set_sps_flag + && hdr.curr_rps_idx == plan.sps.num_short_term_ref_pic_sets; + if !inline || !hdr.short_term_ref_pic_set.inter_ref_pic_set_prediction_flag { + return Ok(0); + } + // RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1), stRpsIdx = CurrRpsIdx here + // (equation 7-59). u16 arithmetic so a hostile delta cannot wrap. + let delta = hdr.short_term_ref_pic_set.delta_idx_minus1; + let source = u16::from(hdr.curr_rps_idx) + .checked_sub(u16::from(delta) + 1) + .and_then(|idx| plan.sps.short_term_ref_pic_set.get(usize::from(idx))) + .ok_or(PlanToVkH265Error::InvalidRefRpsIdx { + curr_rps_idx: hdr.curr_rps_idx, + delta_idx_minus1: delta, + })?; + // NumDeltaPocs = num_negative + num_positive <= 32 off any real parse, + // comfortably u8 — but a directly-constructed plan could exceed it, and a + // silently clamped count would misparse the slice header on hardware: + // typed error, like everything else in this file. + u8::try_from(source.num_delta_pocs) + .map_err(|_| PlanToVkH265Error::NumDeltaPocsOverflow(source.num_delta_pocs)) +} + +/// Convert one planned AU, driving `slots` through the AU's slot lifecycle. +/// +/// Unlike the H.264 [`crate::plan_to_vk`], no `sps_id` parameter: an H.265 +/// [`AuPlan`] carries its activated SPS and PPS, so every id resolves from the +/// plan itself. +/// +/// Atomicity contract (identical to the H.264 module): every fallible step runs +/// before any mutation of `slots`, so an error leaves the map exactly as it was. +/// In order: +/// 1. capacity is validated against the plan's `max_dpb_frames` (read-only); +/// 2. the RPS binding set resolves against the PRE-removal state (read-only) — +/// a current-set member always survives its own AU (8.3.2's marking keeps it +/// referenced), but resolving before removals keeps the transaction shape +/// byte-for-byte the H.264 one and costs nothing; +/// 3. slice lists are cross-checked and offsets validated (read-only); +/// 4. `removed` is applied — removals were real regardless of this AU's fate — +/// and the setup slot is assigned last. A stored-and-evicted picture (its id +/// in this same plan's `removed`) still gets its slot for the decode itself +/// and is released right after, exactly the H.264 defensive path. +pub fn plan_to_vk_h265( + plan: &AuPlan, + slots: &mut SlotMap, +) -> Result { + let first_slice = plan.slices.first().ok_or(PlanToVkH265Error::NoSlices)?; + let setup_id = plan.dpb.stored.ok_or(PlanToVkH265Error::NoStoredId)?; + + // The map must match THIS plan's DPB depth; a mismatch means an SPS + // renegotiation resized the DPB and the session must be rebuilt. + let required = plan.picture.max_dpb_frames + 1; + if slots.capacity() != required { + return Err(PlanToVkH265Error::CapacityMismatch { + required, + capacity: slots.capacity(), + }); + } + + // The AU-level binding set: the union of the three current RPS sets, first + // appearance first (module docs — Vulkan H.265 keys everything by these, + // not by slice lists). Each picture appears once even if a corrupt stream's + // concealment resolved two entries to the same stored picture. + let mut refs: Vec = Vec::new(); + let mut index_arrays = [[UNUSED_RPS_ENTRY; H265_RPS_LIST_SIZE]; 3]; + let sets: [(&'static str, &[RefPic]); 3] = [ + ("RefPicSetStCurrBefore", &plan.rps.st_curr_before), + ("RefPicSetStCurrAfter", &plan.rps.st_curr_after), + ("RefPicSetLtCurr", &plan.rps.lt_curr), + ]; + for (array, (name, set)) in index_arrays.iter_mut().zip(sets) { + if set.len() > H265_RPS_LIST_SIZE { + return Err(PlanToVkH265Error::RpsSetOverflow { + set: name, + len: set.len(), + }); + } + for (position, rp) in set.iter().enumerate() { + let entry = match refs.iter().position(|existing| existing.id == rp.id) { + Some(index) => { + // A concealment-resolved duplicate across sets (doc above): + // the stored picture binds ONCE, but if ANY occurrence + // marks it long-term the binding must say so — hardware + // treats LT references differently (no MV scaling, POC-LSB + // matching), and an `RefPicSetLtCurr` index into a + // short-term-marked slot is an internally inconsistent DPB. + if rp.is_long_term { + refs[index].std.flags.set_used_for_long_term_reference(1); + } + refs[index].slot + } + None => { + let slot = slots + .slot_of(rp.id) + .ok_or(PlanToVkH265Error::UnresolvedReference(rp.id))?; + refs.push(VkRefH265 { + slot, + std: ref_info(rp), + id: rp.id, + }); + slot + } + }; + // A DPB SLOT index, not a position in `refs` — see the + // `DecodePlanVkH265::std_pic` docs. Slots are bounded by the session's + // `maxDpbSlots` (<= 17 under this crate's envelope), so no real slot + // can collide with the 0xFF sentinel. + debug_assert_ne!(entry, UNUSED_RPS_ENTRY, "a real DPB slot is never 0xFF"); + array[position] = entry; + } + } + + // Cross-check: 8.3.4 builds every slice list from the current sets, so any + // entry outside the binding set is a planner-contract violation the + // hardware could not resolve (error-type docs). + for slice in &plan.slices { + for rp in slice.ref_list0.iter().chain(&slice.ref_list1) { + if !refs.iter().any(|existing| existing.id == rp.id) { + return Err(PlanToVkH265Error::ReferenceOutsideRps(rp.id)); + } + } + } + + let pic = &plan.picture; + + // SAFETY: StdVideoDecodeH265PictureInfo is a plain-C bindgen struct of a + // bitfield word, integers and byte arrays; all-zero is a valid value for + // every field. + let mut std_pic: hh::StdVideoDecodeH265PictureInfo = unsafe { std::mem::zeroed() }; + std_pic.flags.set_IrapPicFlag(u32::from(pic.is_irap)); + std_pic.flags.set_IdrPicFlag(u32::from(pic.is_idr)); + std_pic.flags.set_IsReference(u32::from(pic.is_reference)); + std_pic.flags.set_short_term_ref_pic_set_sps_flag(u32::from( + first_slice.header.short_term_ref_pic_set_sps_flag, + )); + std_pic.sps_video_parameter_set_id = plan.sps.video_parameter_set_id; + std_pic.pps_seq_parameter_set_id = plan.pps.seq_parameter_set_id; + std_pic.pps_pic_parameter_set_id = plan.pps.pic_parameter_set_id; + std_pic.NumDeltaPocsOfRefRpsIdx = num_delta_pocs_of_ref_rps_idx(plan)?; + std_pic.PicOrderCntVal = pic.pic_order_cnt; + // 0 when the RPS came from the SPS by index (PicturePlan field docs) — + // exactly Vulkan's convention for this field. + std_pic.NumBitsForSTRefPicSetInSlice = u16::try_from(pic.short_term_ref_pic_set_size_bits) + .map_err(|_| PlanToVkH265Error::StRpsBitsOverflow(pic.short_term_ref_pic_set_size_bits))?; + [ + std_pic.RefPicSetStCurrBefore, + std_pic.RefPicSetStCurrAfter, + std_pic.RefPicSetLtCurr, + ] = index_arrays; + + // The setup slot's reference info: the picture's own identity, short-term + // (see the DecodePlanVkH265 field docs for why there is no long-term leg + // here, unlike H.264). + // SAFETY: as above — all-zero is a valid StdVideoDecodeH265ReferenceInfo. + let mut setup_ref: hh::StdVideoDecodeH265ReferenceInfo = unsafe { std::mem::zeroed() }; + setup_ref.PicOrderCntVal = pic.pic_order_cnt; + + let mut slice_offsets = Vec::with_capacity(plan.slices.len()); + for slice in &plan.slices { + // SlicePlan.data starts at the slice NALU's start code — exactly the + // offset Vulkan wants (struct docs). + slice_offsets.push( + u32::try_from(slice.data.start) + .map_err(|_| PlanToVkH265Error::OffsetOverflow(slice.data.start))?, + ); + } + + // Mutations LAST, after every fallible step above (fn docs). Removals first + // — they were real regardless of this AU's fate — then the setup + // assignment, releasing immediately when this very plan already evicted the + // stored picture (the H.264 defensive path; the slot must still exist for + // the decode itself). + let setup_evicted = plan.dpb.removed.contains(&setup_id); + for &id in &plan.dpb.removed { + if id == setup_id { + continue; + } + if !slots.release(id) { + // Tolerated but never silent: reachable only when the caller + // skipped feeding an AU's plan through this map. + trace!(id, "DpbUpdate removed an id this SlotMap never assigned"); + } + } + let setup_slot = slots.assign(setup_id)?; + if setup_evicted { + slots.release(setup_id); + } + + Ok(DecodePlanVkH265 { + std_pic, + slice_offsets, + setup_slot, + setup_ref, + setup_id, + setup_is_reference: pic.is_reference, + refs, + }) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use std::io::Cursor; + use std::rc::Rc; + + use cros_codecs::codec::h265::parser::Nalu; + use cros_codecs::codec::h265::parser::Pps; + use cros_codecs::codec::h265::parser::ShortTermRefPicSet; + use cros_codecs::codec::h265::parser::Sps; + use pf_bitstream::h265::ColourDescription; + use pf_bitstream::h265::DisplayCrop; + use pf_bitstream::h265::DpbUpdate; + use pf_bitstream::h265::H265Planner; + use pf_bitstream::h265::Level; + use pf_bitstream::h265::NaluType; + use pf_bitstream::h265::PicturePlan; + use pf_bitstream::h265::RpsPlan; + use pf_bitstream::h265::SliceHeader; + use pf_bitstream::h265::SlicePlan; + + use super::*; + + // The same vendored vectors pf-bitstream's h265 tests plan (its goldens: + // 250 AUs / 250 slices for the 25fps clip), included from the same path. + const TEST_25FPS: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265" + ); + const TEST_64X64_I_P_B_P: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P-B-P.h265" + ); + + /// Test-only AU splitter, mirroring pf-bitstream's h265 helper (which is + /// `#[cfg(test)]`-private there): a new AU starts at a non-VCL NALU + /// following slices, or at a slice segment with + /// `first_slice_segment_in_pic_flag == 1` (the first bit of the byte after + /// the 2-byte NAL header) when the current AU already has slices. + fn split_into_aus(stream: &[u8]) -> Vec<&[u8]> { + let mut aus = Vec::new(); + let mut cursor = Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + + while let Ok(nalu) = Nalu::next(&mut cursor) { + let header_start = cursor.position() as usize; + let start = header_start - nalu.offset; + let is_slice = (nalu.header.type_ as u32) < 32; + let first_slice_flag = + is_slice && stream.get(header_start + 2).is_some_and(|b| b & 0x80 != 0); + + if au_has_slice && (!is_slice || first_slice_flag) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus + } + + #[test] + fn the_full_25fps_vector_converts_with_stable_slots_and_start_code_offsets() { + let aus = split_into_aus(TEST_25FPS); + let mut planner = H265Planner::new(); + let mut slots: Option = None; + // PicId -> the slot it was assigned; entries leave only on `removed`. + let mut held: BTreeMap = BTreeMap::new(); + let mut converted = 0usize; + // AUs whose `refs` are NOT in slot order — the AUs on which "DPB slot" and + // "position in refs" are distinguishable (see the loop body). + let mut slot_order_differs = 0usize; + + for au in &aus { + let plan = planner.plan_au(au).expect("the clean vector plans"); + let slots = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames)); + let vk = plan_to_vk_h265(&plan, slots).expect("the clean vector converts"); + converted += 1; + + // Slot stability: every reference resolves to the slot its picture + // was assigned when IT was decoded, and no ref shares the setup slot. + for r in &vk.refs { + assert_eq!( + held.get(&r.id), + Some(&r.slot), + "a referenced picture's slot changed while it was referenced" + ); + assert_ne!(r.slot, vk.setup_slot, "a reference aliases the setup slot"); + } + + // The Std index arrays resolve exactly the plan's RPS sets, in + // order, 0xFF beyond — BY DPB SLOT (struct docs), and every slot they + // name is one the decode op will bind, i.e. one of `refs`'. + for (array, set) in [ + (&vk.std_pic.RefPicSetStCurrBefore, &plan.rps.st_curr_before), + (&vk.std_pic.RefPicSetStCurrAfter, &plan.rps.st_curr_after), + (&vk.std_pic.RefPicSetLtCurr, &plan.rps.lt_curr), + ] { + for (position, entry) in array.iter().enumerate() { + match set.get(position) { + Some(rp) => { + let r = + vk.refs + .iter() + .find(|r| r.slot == *entry) + .unwrap_or_else(|| { + panic!( + "AU {converted}: RPS entry names DPB slot {entry}, \ + which this decode op does not bind ({:?}) — the \ + hardware cannot resolve it", + vk.refs.iter().map(|r| r.slot).collect::>() + ) + }); + assert_eq!(r.id, rp.id, "AU {converted}: the slot's picture"); + assert_eq!(r.std.PicOrderCntVal, rp.pic_order_cnt); + } + None => assert_eq!(*entry, UNUSED_RPS_ENTRY), + } + } + } + // …and the vector genuinely DISCRIMINATES the two readings, so this + // assertion can never quietly become vacuous: on a freshly anchored + // stream "DPB slot" and "position in refs" agree, and a test that only + // ever saw agreeing AUs would pass either way. Count the AUs where + // they disagree; the total is asserted below. + if vk + .refs + .iter() + .enumerate() + .any(|(position, r)| usize::from(r.slot) != position) + { + slot_order_differs += 1; + } + + // Slice offsets: one per slice, each at a start-code boundary of + // the AU, exactly where the plan said the slice begins. + assert_eq!(vk.slice_offsets.len(), plan.slices.len()); + for (offset, slice) in vk.slice_offsets.iter().zip(&plan.slices) { + let offset = *offset as usize; + assert_eq!(offset, slice.data.start); + let at = &au[offset..]; + assert!( + at.starts_with(&[0, 0, 1]) || at.starts_with(&[0, 0, 0, 1]), + "slice offset {offset} does not sit on a start code" + ); + } + + assert_eq!(vk.std_pic.PicOrderCntVal, plan.picture.pic_order_cnt); + assert_eq!( + u32::from(plan.picture.is_idr), + vk.std_pic.flags.IdrPicFlag() + ); + assert_eq!( + u32::from(plan.picture.is_irap), + vk.std_pic.flags.IrapPicFlag() + ); + assert_eq!( + u32::from(plan.picture.is_reference), + vk.std_pic.flags.IsReference() + ); + assert_eq!(vk.setup_ref.PicOrderCntVal, plan.picture.pic_order_cnt); + assert_eq!( + vk.std_pic.NumBitsForSTRefPicSetInSlice, + plan.picture.short_term_ref_pic_set_size_bits as u16 + ); + + // Mirror the map's bookkeeping: record the new picture, drop the + // removed. + let stored = plan.dpb.stored.unwrap(); + assert_eq!(vk.setup_id, stored); + assert_eq!(vk.setup_is_reference, plan.picture.is_reference); + held.insert(stored, vk.setup_slot); + for id in &plan.dpb.removed { + held.remove(id); + } + + // held() must mirror the plan-driven bookkeeping exactly, every AU. + let ledger: BTreeMap = slots.held().map(|(slot, id)| (id, slot)).collect(); + assert_eq!(ledger, held); + } + + assert_eq!(converted, 250, "the vector's own golden"); + // The vector's hierarchical-B RPS sets put the references out of slot order + // from AU 3 on (its first B picture with two `StCurrAfter` entries binds + // slots 2 then 1), so the index-array assertions above are decisive rather + // than accidental. 247 of 250: AUs 0-2 agree, which is precisely why the + // wrong reading survived to hardware and produced three correct frames + // followed by 247 corrupt ones. + assert_eq!( + slot_order_differs, 247, + "the vector must distinguish DPB slots from positions in refs" + ); + + // Teardown: the flush update releases every remaining slot — the + // codec-neutral DpbUpdate drives the SAME SlotMap H.264 uses. + let mut slots = slots.unwrap(); + slots.apply(&planner.flush()); + assert_eq!(slots.active(), 0); + } + + #[test] + fn the_b_frame_vector_populates_both_current_index_arrays_around_the_picture() { + let aus = split_into_aus(TEST_64X64_I_P_B_P); + let mut planner = H265Planner::new(); + let mut slots: Option = None; + let mut b_pictures_seen = 0usize; + + for au in &aus { + let plan = planner.plan_au(au).expect("the clean vector plans"); + let slots = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames)); + let vk = plan_to_vk_h265(&plan, slots).expect("the clean vector converts"); + + if plan.rps.st_curr_after.is_empty() { + continue; + } + b_pictures_seen += 1; + // 8.3.2: StCurrBefore entries sit below the picture's POC, + // StCurrAfter above — resolved through the index arrays' DPB SLOTS. + let bound = |slot: u8| { + vk.refs + .iter() + .find(|r| r.slot == slot) + .unwrap_or_else(|| panic!("RPS entry names unbound DPB slot {slot}")) + }; + let before = vk.std_pic.RefPicSetStCurrBefore[0]; + let after = vk.std_pic.RefPicSetStCurrAfter[0]; + assert!(bound(before).std.PicOrderCntVal < plan.picture.pic_order_cnt); + assert!(bound(after).std.PicOrderCntVal > plan.picture.pic_order_cnt); + assert_ne!(before, after, "distinct slots on the two sides"); + } + + assert!(b_pictures_seen > 0, "the vector must contain B pictures"); + } + + // ------- hand-built plan fixtures (the vendored crate has no H.265 + // synthesizer; every AuPlan field is public, so edge cases construct the + // planner's output shape directly — the contract under test is the plan, + // not the bitstream) ------- + + fn mini_sps() -> Rc { + Rc::new(Sps { + video_parameter_set_id: 0, + seq_parameter_set_id: 0, + chroma_format_idc: 1, + pic_width_in_luma_samples: 64, + pic_height_in_luma_samples: 64, + ..Default::default() + }) + } + + fn mini_pps(sps: &Rc) -> Rc { + // The vendored Pps derives no Default; only the fields this module + // reads (the two ids and the SPS chain) carry meaning here. + Rc::new(Pps { + pic_parameter_set_id: 0, + seq_parameter_set_id: 0, + dependent_slice_segments_enabled_flag: false, + output_flag_present_flag: false, + num_extra_slice_header_bits: 0, + sign_data_hiding_enabled_flag: false, + cabac_init_present_flag: false, + num_ref_idx_l0_default_active_minus1: 0, + num_ref_idx_l1_default_active_minus1: 0, + init_qp_minus26: 0, + constrained_intra_pred_flag: false, + transform_skip_enabled_flag: false, + cu_qp_delta_enabled_flag: false, + diff_cu_qp_delta_depth: 0, + cb_qp_offset: 0, + cr_qp_offset: 0, + slice_chroma_qp_offsets_present_flag: false, + weighted_pred_flag: false, + weighted_bipred_flag: false, + transquant_bypass_enabled_flag: false, + tiles_enabled_flag: false, + entropy_coding_sync_enabled_flag: false, + num_tile_columns_minus1: 0, + num_tile_rows_minus1: 0, + uniform_spacing_flag: true, + column_width_minus1: [0; 19], + row_height_minus1: [0; 21], + loop_filter_across_tiles_enabled_flag: true, + loop_filter_across_slices_enabled_flag: false, + deblocking_filter_control_present_flag: false, + deblocking_filter_override_enabled_flag: false, + deblocking_filter_disabled_flag: false, + beta_offset_div2: 0, + tc_offset_div2: 0, + scaling_list_data_present_flag: false, + scaling_list: Default::default(), + lists_modification_present_flag: false, + log2_parallel_merge_level_minus2: 0, + slice_segment_header_extension_present_flag: false, + extension_present_flag: false, + range_extension_flag: false, + range_extension: Default::default(), + scc_extension_flag: false, + scc_extension: Default::default(), + qp_bd_offset_y: 0, + sps: Rc::clone(sps), + }) + } + + fn mini_picture(poc: i32, max_dpb_frames: usize) -> PicturePlan { + PicturePlan { + nalu_type: if poc == 0 { + NaluType::IdrWRadl + } else { + NaluType::TrailR + }, + is_idr: poc == 0, + is_irap: poc == 0, + no_rasl_output_flag: poc == 0, + is_reference: true, + pic_order_cnt: poc, + coded_width: 64, + coded_height: 64, + display_crop: DisplayCrop { + x: 0, + y: 0, + width: 64, + height: 64, + }, + colour: ColourDescription { + colour_primaries: 2, + transfer_characteristics: 2, + matrix_coefficients: 2, + video_full_range: false, + }, + general_profile_idc: 1, + level_idc: Level::L4, + bit_depth_luma_minus8: 0, + bit_depth_chroma_minus8: 0, + chroma_format_idc: 1, + max_dpb_frames, + short_term_ref_pic_set_size_bits: 0, + recovery_point: None, + } + } + + fn mini_slice(refs0: &[RefPic], refs1: &[RefPic]) -> SlicePlan { + SlicePlan { + data: 0..32, + header: SliceHeader::default(), + ref_list0: refs0.to_vec(), + ref_list1: refs1.to_vec(), + } + } + + /// A plan storing `stored` with the given RPS sets and one slice whose + /// list0 is the concatenation the 8-8 temporal order would produce. + fn mini_plan( + stored: PicId, + poc: i32, + rps: RpsPlan, + removed: Vec, + max_dpb_frames: usize, + ) -> AuPlan { + let sps = mini_sps(); + let pps = mini_pps(&sps); + let mut list0: Vec = Vec::new(); + list0.extend(rps.st_curr_before.iter().copied()); + list0.extend(rps.st_curr_after.iter().copied()); + list0.extend(rps.lt_curr.iter().copied()); + // The marked DPB is this AU's current sets and nothing more here: the Vulkan + // rung binds `pReferenceSlots` from the CURRENT sets (the slots this decode + // operation uses), so the snapshot is not an input to anything under test. + let dpb_refs = list0.clone(); + AuPlan { + picture: mini_picture(poc, max_dpb_frames), + rps, + slices: vec![mini_slice(&list0, &[])], + dpb: DpbUpdate { + stored: Some(stored), + outputs: vec![stored], + removed, + }, + dpb_refs, + warnings: Vec::new(), + sps, + pps, + } + } + + fn st_ref(id: PicId, poc: i32) -> RefPic { + RefPic { + id, + pic_order_cnt: poc, + is_long_term: false, + } + } + + fn lt_ref(id: PicId, poc: i32) -> RefPic { + RefPic { + id, + pic_order_cnt: poc, + is_long_term: true, + } + } + + #[test] + fn a_long_term_rps_entry_carries_the_flag_and_its_index_lands_in_lt_curr() { + let mut slots = SlotMap::new(4); + // Two pictures already decoded through this map: the anchor (id 10, + // poc 0, pinned long-term) and the previous picture (id 11, poc 1). + slots.assign(10).unwrap(); + slots.assign(11).unwrap(); + + let plan = mini_plan( + 12, + 2, + RpsPlan { + st_curr_before: vec![st_ref(11, 1)], + st_curr_after: Vec::new(), + lt_curr: vec![lt_ref(10, 0)], + }, + Vec::new(), + 4, + ); + let vk = plan_to_vk_h265(&plan, &mut slots).unwrap(); + + assert_eq!(vk.refs.len(), 2); + // The entries are DPB SLOTS: id 11 took slot 1 and id 10 slot 0, while the + // RPS set order binds id 11 FIRST — so a positional reading would name slot + // 0 for the short-term set and get the long-term anchor instead. That is the + // whole M3 field failure in one fixture. + assert_eq!(vk.std_pic.RefPicSetStCurrBefore[0], 1, "id 11's DPB slot"); + assert_eq!(vk.std_pic.RefPicSetLtCurr[0], 0, "id 10's DPB slot"); + let bound = |slot: u8| vk.refs.iter().find(|r| r.slot == slot).expect("bound"); + let st = bound(vk.std_pic.RefPicSetStCurrBefore[0]); + assert_eq!(st.id, 11); + assert_eq!(st.std.flags.used_for_long_term_reference(), 0); + let lt = bound(vk.std_pic.RefPicSetLtCurr[0]); + assert_eq!(lt.id, 10); + assert_eq!(lt.std.flags.used_for_long_term_reference(), 1); + assert_eq!(lt.std.PicOrderCntVal, 0); + assert_eq!(vk.std_pic.RefPicSetStCurrAfter[0], UNUSED_RPS_ENTRY); + // The setup picture itself activates short-term (no same-AU + // self-marking in HEVC — struct docs). + assert_eq!(vk.setup_ref.flags.used_for_long_term_reference(), 0); + assert_eq!(vk.setup_ref.PicOrderCntVal, 2); + } + + #[test] + fn a_failed_conversion_leaves_the_slot_map_untouched_and_the_session_recovers() { + // A right-sized map that never saw the reference's AU, holding one + // unrelated slot: the reference must fail loudly, not resolve to a + // fabricated slot. + let mut slots = SlotMap::new(4); + slots.assign(999).unwrap(); + + let plan = mini_plan( + 5, + 1, + RpsPlan { + st_curr_before: vec![st_ref(4, 0)], + st_curr_after: Vec::new(), + lt_curr: Vec::new(), + }, + vec![3], // a removal that must NOT be applied on the failed path + 4, + ); + assert_eq!( + plan_to_vk_h265(&plan, &mut slots).unwrap_err(), + PlanToVkH265Error::UnresolvedReference(4) + ); + + // Atomicity: the failed conversion mutated nothing. + assert_eq!(slots.active(), 1); + assert_eq!(slots.held().collect::>(), vec![(0, 999)]); + + // And the session recovers: the next valid plan (an IDR restart whose + // `removed` names ids this map never assigned — tolerated by design) + // still converts on the same map. + let idr = mini_plan(6, 0, RpsPlan::default(), vec![4, 5], 4); + let vk = plan_to_vk_h265(&idr, &mut slots).unwrap(); + assert_eq!(vk.setup_slot, 1, "the lowest free slot after the held one"); + assert_eq!(slots.active(), 2); + } + + #[test] + fn an_sps_switch_that_resizes_the_dpb_is_a_capacity_mismatch_not_a_guess() { + // The map was built for a 6-deep DPB; a renegotiated stream plans with + // 16. Refuse, so the session rebuilds session + map instead of handing + // out slots the image pool does not have. + let mut slots = SlotMap::new(6); + plan_to_vk_h265( + &mini_plan(0, 0, RpsPlan::default(), Vec::new(), 6), + &mut slots, + ) + .unwrap(); + + let renegotiated = mini_plan(1, 0, RpsPlan::default(), Vec::new(), 16); + assert_eq!( + plan_to_vk_h265(&renegotiated, &mut slots).unwrap_err(), + PlanToVkH265Error::CapacityMismatch { + required: 17, + capacity: 7 + } + ); + // And the mismatch mutated nothing. + assert_eq!(slots.active(), 1); + } + + #[test] + fn empty_and_flush_shaped_plans_are_rejected_with_typed_errors() { + let mut slots = SlotMap::new(4); + + let mut no_slices = mini_plan(0, 0, RpsPlan::default(), Vec::new(), 4); + no_slices.slices.clear(); + assert_eq!( + plan_to_vk_h265(&no_slices, &mut slots).unwrap_err(), + PlanToVkH265Error::NoSlices + ); + + let mut no_stored = mini_plan(0, 0, RpsPlan::default(), Vec::new(), 4); + no_stored.dpb.stored = None; + assert_eq!( + plan_to_vk_h265(&no_stored, &mut slots).unwrap_err(), + PlanToVkH265Error::NoStoredId + ); + + let mut huge_offset = mini_plan(0, 0, RpsPlan::default(), Vec::new(), 4); + huge_offset.slices[0].data = (u32::MAX as usize + 1)..(u32::MAX as usize + 40); + assert_eq!( + plan_to_vk_h265(&huge_offset, &mut slots).unwrap_err(), + PlanToVkH265Error::OffsetOverflow(u32::MAX as usize + 1) + ); + assert_eq!(slots.active(), 0, "every rejection left the map untouched"); + } + + #[test] + fn an_rps_set_deeper_than_the_std_index_arrays_is_rejected_not_truncated() { + let mut slots = SlotMap::new(16); + for id in 0..9u64 { + slots.assign(id).unwrap(); + } + let deep: Vec = (0..9).map(|i| st_ref(i, i as i32)).collect(); + let plan = mini_plan( + 20, + 9, + RpsPlan { + st_curr_before: deep, + st_curr_after: Vec::new(), + lt_curr: Vec::new(), + }, + Vec::new(), + 16, + ); + assert_eq!( + plan_to_vk_h265(&plan, &mut slots).unwrap_err(), + PlanToVkH265Error::RpsSetOverflow { + set: "RefPicSetStCurrBefore", + len: 9 + } + ); + assert_eq!(slots.active(), 9, "the rejection mutated nothing"); + } + + #[test] + fn a_slice_list_entry_outside_the_rps_sets_fails_closed() { + let mut slots = SlotMap::new(4); + slots.assign(1).unwrap(); + slots.assign(2).unwrap(); + let mut plan = mini_plan( + 3, + 2, + RpsPlan { + st_curr_before: vec![st_ref(1, 0)], + st_curr_after: Vec::new(), + lt_curr: Vec::new(), + }, + Vec::new(), + 4, + ); + // Id 2 holds a slot but is in NO current set: it would be missing from + // pReferenceSlots, so the hardware could not resolve the list entry. + plan.slices[0].ref_list0.push(st_ref(2, 1)); + assert_eq!( + plan_to_vk_h265(&plan, &mut slots).unwrap_err(), + PlanToVkH265Error::ReferenceOutsideRps(2) + ); + } + + #[test] + fn a_stored_and_evicted_picture_still_gets_a_slot_for_the_decode_itself() { + // The defensive same-plan eviction path (H.264 parity): the stored id + // appears in its own plan's `removed` — the slot exists during the + // decode and is released right after, so the next picture can reuse it. + let mut slots = SlotMap::new(1); // capacity 2 + let mut plan = mini_plan(0, 0, RpsPlan::default(), Vec::new(), 1); + plan.dpb.removed = vec![0]; + let vk = plan_to_vk_h265(&plan, &mut slots).unwrap(); + assert_eq!(vk.setup_slot, 0); + assert_eq!(slots.active(), 0, "released after assignment"); + assert_eq!(slots.slot_of(0), None); + } + + #[test] + fn num_delta_pocs_of_ref_rps_idx_derives_from_the_predicted_inline_rps() { + let mut slots = SlotMap::new(4); + slots.assign(1).unwrap(); + + // The activated SPS carries two candidates; the inline slice RPS + // predicts from the second (delta_idx_minus1 = 0 ⇒ RefRpsIdx = 1). + let mut sps = (*mini_sps()).clone(); + sps.num_short_term_ref_pic_sets = 2; + sps.short_term_ref_pic_set = vec![ + ShortTermRefPicSet { + num_delta_pocs: 3, + ..Default::default() + }, + ShortTermRefPicSet { + num_delta_pocs: 5, + ..Default::default() + }, + ]; + let sps = Rc::new(sps); + let pps = mini_pps(&sps); + + let header = SliceHeader { + short_term_ref_pic_set_sps_flag: false, + curr_rps_idx: 2, // == num_short_term_ref_pic_sets: inline + short_term_ref_pic_set: ShortTermRefPicSet { + inter_ref_pic_set_prediction_flag: true, + delta_idx_minus1: 0, + ..Default::default() + }, + ..Default::default() + }; + + let mut picture = mini_picture(1, 4); + picture.short_term_ref_pic_set_size_bits = 23; + let plan = AuPlan { + picture, + rps: RpsPlan { + st_curr_before: vec![st_ref(1, 0)], + st_curr_after: Vec::new(), + lt_curr: Vec::new(), + }, + slices: vec![SlicePlan { + data: 0..32, + header, + ref_list0: vec![st_ref(1, 0)], + ref_list1: Vec::new(), + }], + dpb: DpbUpdate { + stored: Some(2), + outputs: vec![2], + removed: Vec::new(), + }, + dpb_refs: vec![st_ref(1, 0)], + warnings: Vec::new(), + sps: Rc::clone(&sps), + pps: Rc::clone(&pps), + }; + let vk = plan_to_vk_h265(&plan, &mut slots).unwrap(); + assert_eq!( + vk.std_pic.NumDeltaPocsOfRefRpsIdx, 5, + "the SOURCE set's count" + ); + assert_eq!(vk.std_pic.flags.short_term_ref_pic_set_sps_flag(), 0); + assert_eq!(vk.std_pic.NumBitsForSTRefPicSetInSlice, 23); + + // A prediction pointing past the candidate table cannot be derived. + let mut broken = plan.clone(); + { + let header = &mut broken.slices[0].header; + header.short_term_ref_pic_set.delta_idx_minus1 = 2; // RefRpsIdx = -1 + } + broken.dpb.stored = Some(3); + assert_eq!( + plan_to_vk_h265(&broken, &mut slots).unwrap_err(), + PlanToVkH265Error::InvalidRefRpsIdx { + curr_rps_idx: 2, + delta_idx_minus1: 2 + } + ); + } + + #[test] + fn parameter_set_ids_flow_from_the_plans_activated_sets() { + let mut slots = SlotMap::new(4); + let mut sps = (*mini_sps()).clone(); + sps.video_parameter_set_id = 3; + sps.seq_parameter_set_id = 7; + let sps = Rc::new(sps); + let mut pps = (*mini_pps(&sps)).clone(); + pps.pic_parameter_set_id = 9; + pps.seq_parameter_set_id = 7; + let pps = Rc::new(pps); + + let mut plan = mini_plan(0, 0, RpsPlan::default(), Vec::new(), 4); + plan.sps = sps; + plan.pps = pps; + let vk = plan_to_vk_h265(&plan, &mut slots).unwrap(); + assert_eq!(vk.std_pic.sps_video_parameter_set_id, 3); + assert_eq!(vk.std_pic.pps_seq_parameter_set_id, 7); + assert_eq!(vk.std_pic.pps_pic_parameter_set_id, 9); + } + + #[test] + fn a_picture_referenced_by_two_sets_binds_one_slot_listed_once() { + // Concealment can resolve an lsb-masked long-term entry and a + // short-term entry to the SAME stored picture; Vulkan wants each slot + // bound once, with both index arrays pointing at that one entry. + let mut slots = SlotMap::new(4); + slots.assign(1).unwrap(); + let plan = mini_plan( + 2, + 1, + RpsPlan { + st_curr_before: vec![st_ref(1, 0)], + st_curr_after: Vec::new(), + lt_curr: vec![lt_ref(1, 0)], + }, + Vec::new(), + 4, + ); + let vk = plan_to_vk_h265(&plan, &mut slots).unwrap(); + assert_eq!(vk.refs.len(), 1, "one binding for one picture"); + assert_eq!( + vk.std_pic.RefPicSetStCurrBefore[0], + vk.std_pic.RefPicSetLtCurr[0] + ); + // The short-term set bound the picture FIRST, but the LtCurr occurrence + // must still mark the shared binding long-term: hardware treats LT + // references differently (no MV scaling, POC-LSB matching), and an + // LtCurr index into a short-term-marked slot is an internally + // inconsistent DPB the driver may reject or mispredict from. + assert_eq!(vk.refs[0].std.flags.used_for_long_term_reference(), 1); + } +} diff --git a/crates/pf-vkdecode/src/probe.rs b/crates/pf-vkdecode/src/probe.rs new file mode 100644 index 00000000..cd5b116d --- /dev/null +++ b/crates/pf-vkdecode/src/probe.rs @@ -0,0 +1,442 @@ +//! What the driver ACTUALLY answers about video image formats, verbatim — the +//! physical-device-only probe behind `punktfunk-session --probe-decode`. +//! +//! # Why this exists +//! +//! Twice in a row an Intel Arc refusal was diagnosed from punktfunk's OWN error text +//! and twice the conclusion ("Intel driver bug") was wrong — the bug was ours, in the +//! `pNext` order of the capability query. What broke it open both times was logging +//! the driver's answer with no interpretation in front of it. This module makes that +//! the DEFAULT rather than a debugging afterthought: for every decode profile the +//! client can negotiate, it asks the driver the same question the session's caps query +//! asks, in every usage combination the image pools would create with, and records +//! what came back — including the failures, spelled as the `VkResult` the driver +//! returned. +//! +//! It shares [`crate::caps::query_formats_on`] with the real caps path on purpose. A +//! probe with its own copy of the query is a probe that eventually disagrees with the +//! code it is meant to explain, which is worse than no probe at all. +//! +//! # What it measured +//! +//! Intel Arc (Windows driver 101.8861, 2026-08): every one of the 26 decode profiles +//! reports the SAME envelope for its NV12/P010 picture format — +//! `TRANSFER_SRC | VIDEO_DECODE_DST | VIDEO_DECODE_DPB`, and `imageCreateFlags` EMPTY — +//! with `DPB_AND_OUTPUT_COINCIDE` as the only decode mode. No `SAMPLED`, so a shader +//! cannot read the decoded picture; and no `MUTABLE_FORMAT`/`EXTENDED_USAGE`, which +//! closes the spec's only escape hatch (see [`crate::caps::VideoFormat:: +//! image_create_flags`]). `TRANSFER_SRC` is the sole way out of the image — i.e. a +//! copy. NVIDIA (596.41) answers the same queries with +//! `TRANSFER_SRC|TRANSFER_DST|SAMPLED|DECODE_DST|DECODE_DPB|ENCODE_SRC|ENCODE_DPB` and +//! `MUTABLE_FORMAT|EXTENDED_USAGE`, which is what makes the zero-copy path work there. +//! +//! The Intel answers carry one genuine conformance bug, which is what made the refusal +//! read oddly: the driver ignores the REQUESTED `imageUsage` completely. Asked for +//! `SAMPLED` alone it still returns that same decode envelope, where the spec says the +//! returned `imageUsageFlags` "will contain at least the same set of image usage flags" +//! and `VK_ERROR_IMAGE_USAGE_NOT_SUPPORTED_KHR` is the documented refusal. That is why +//! derivation reported "the NV12 entry does not advertise SAMPLED" (an entry was found) +//! rather than "no NV12 in the coincide list" (nothing returned). It changes the error +//! text, not the answer. +//! +//! # What the cross-check is, and is NOT +//! +//! [`UsageProbe::image_format_support`] asks `vkGetPhysicalDeviceImageFormatProperties2` +//! the same question with the same profile list chained. It is deliberately kept, and +//! deliberately NOT treated as authority, because measuring it settled what it is worth: +//! on BOTH vendors it answers "creatable" for combinations the video-format query +//! rejects — NVIDIA included, for `SAMPLED` alone, which is not a legal video image +//! usage at all. So that entry point does not fully honour the video profile list on any +//! driver measured here, and a "creatable" from it is NOT evidence that a decode picture +//! can be sampled. It is reported because the question is otherwise re-asked by every +//! person who reads the refusal; the answer is on the record instead. +//! +//! `vkGetPhysicalDeviceVideoFormatPropertiesKHR` remains the authority, and two +//! independent implementations of it — this probe and `vulkaninfo --show-video-props` — +//! agree on every value above. + +use ash::vk; +use ash::vk::native as hh; + +use crate::caps::query_formats_on; +use crate::caps::DecodeProfile; +use crate::caps::VideoFormat; +use crate::caps::COINCIDE_USAGE; +use crate::caps::DPB_USAGE; +use crate::caps::NV12; +use crate::caps::OUTPUT_USAGE; +use crate::caps::P010; +use crate::caps_av1::Av1ProfileKey; +use crate::caps_h265::H265ProfileKey; + +/// The usage combinations the probe asks about, widest question first. +/// +/// The first three are the REAL ones — exactly what [`crate::images`] creates with, so +/// their answers are the ones derivation acts on. The rest exist to localise a refusal: +/// when `DPB|DST|SAMPLED` fails, `DPB|DST` says whether the decode roles alone are fine +/// (i.e. the gap is sampling) and `SAMPLED` alone says whether the format is sampleable +/// under this profile at all. Without them, a single failed query leaves "which half is +/// missing?" to inference — which is how this device got misdiagnosed twice. +const USAGE_MATRIX: [(&str, vk::ImageUsageFlags); 6] = [ + ("coincide DPB|DST|SAMPLED", COINCIDE_USAGE), + ("distinct DPB", DPB_USAGE), + ("distinct DST|SAMPLED", OUTPUT_USAGE), + ("DPB|DST (no SAMPLED)", DECODE_ONLY_USAGE), + ("SAMPLED alone", vk::ImageUsageFlags::SAMPLED), + ("DST alone", vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR), +]; + +/// Decode roles with sampling deliberately withheld — the discriminator between "this +/// device cannot decode this profile" and "it can decode it but not let anyone read it". +const DECODE_ONLY_USAGE: vk::ImageUsageFlags = vk::ImageUsageFlags::from_raw( + vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR.as_raw() + | vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR.as_raw(), +); + +/// One decode profile's worth of driver answers. +#[derive(Debug, Clone)] +pub struct ProfileProbe { + /// The profile as a human would name it ("H.265 Main 4:2:0 8-bit"). + pub profile: &'static str, + /// The picture format this profile decodes to — the entry the probe looks for. + pub wanted: vk::Format, + /// One row per [`USAGE_MATRIX`] entry, in that order. + pub usages: Vec, +} + +/// The driver's answer for ONE (profile, usage) question. +#[derive(Debug, Clone)] +pub struct UsageProbe { + pub label: &'static str, + pub usage: vk::ImageUsageFlags, + /// `vkGetPhysicalDeviceVideoFormatPropertiesKHR`: every entry it returned, or the + /// `VkResult` it failed with. An EMPTY vector is itself an answer — the two + /// "no formats for this combination" results + /// (`ERROR_FORMAT_NOT_SUPPORTED`/`ERROR_IMAGE_USAGE_NOT_SUPPORTED_KHR`) are mapped + /// to it by the shared query, exactly as derivation sees them. + pub formats: Result, vk::Result>, + /// `vkGetPhysicalDeviceImageFormatProperties2` for the SAME profile list, format + /// and usage. `Ok(())` means that call says an image of the shape is creatable — + /// which, as the module docs record, is a WEAKER claim than it looks: measured on + /// both vendors, this entry point does not fully honour the chained video profile + /// list, so it must not be read as permission to create a video image. + pub image_format_support: Result<(), vk::Result>, +} + +impl UsageProbe { + /// The entry for the profile's picture format, if the driver returned one. + pub fn wanted_entry(&self, wanted: vk::Format) -> Option { + self.formats + .as_ref() + .ok()? + .iter() + .copied() + .find(|f| f.format == wanted) + } +} + +/// The profiles worth probing: one per codec rung the client can negotiate, plus the +/// 10-bit legs, because an HDR stream picks a DIFFERENT Vulkan profile from an SDR one +/// and a device may well host one and not the other. +/// +/// Deliberately NOT every profile the driver supports — this answers "can punktfunk +/// decode here", and a list padded with profiles no rung ever requests makes the row +/// that matters harder to find. `vulkaninfo --show-video-props` is the tool for the +/// exhaustive sweep. +fn probed_profiles() -> Vec<(&'static str, DecodeProfile, vk::Format)> { + // Every key is built through the SAME constructor the decoders negotiate with + // (`from_negotiated`), so a profile the client could never request cannot appear + // here — and a combination those constructors refuse simply drops out of the list + // instead of being hand-rolled into existence for the probe's benefit. + // 4:2:0 is chroma_format_idc 1 in both codecs' vocabulary; H.265 states depth as + // `bit_depth_luma_minus8`, AV1 in whole bits. + let mut out: Vec<(&'static str, DecodeProfile, vk::Format)> = vec![( + "H.264 High 4:2:0 8-bit", + DecodeProfile::H264(hh::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_HIGH), + NV12, + )]; + if let Ok(key) = H265ProfileKey::from_negotiated(1, 0) { + out.push(("H.265 Main 4:2:0 8-bit", DecodeProfile::H265(key), NV12)); + } + if let Ok(key) = H265ProfileKey::from_negotiated(1, 2) { + out.push(("H.265 Main 10 4:2:0 10-bit", DecodeProfile::H265(key), P010)); + } + // AV1 without film grain: `filmGrainSupport` is part of the Vulkan PROFILE, and + // grain-less is what a punktfunk host encodes. + if let Ok(key) = Av1ProfileKey::from_negotiated(1, 8, false) { + out.push(("AV1 Main 4:2:0 8-bit", DecodeProfile::Av1(key), NV12)); + } + if let Ok(key) = Av1ProfileKey::from_negotiated(1, 10, false) { + out.push(("AV1 Main 4:2:0 10-bit", DecodeProfile::Av1(key), P010)); + } + out +} + +/// Ask one physical device every question in the matrix, for every probed profile. +/// +/// Never fails as a whole: a driver that refuses a profile outright is a ROW in the +/// output, not an error return — the point is to come back with the full picture even +/// when most of it is refusals. +/// +/// # Safety +/// +/// `instance` must be a live `VkInstance` created through `entry`, and +/// `physical_device` one of its physical devices. Nothing here creates or destroys +/// anything; every call is a physical-device query. +pub unsafe fn probe_video_formats( + entry: &ash::Entry, + instance: &ash::Instance, + physical_device: vk::PhysicalDevice, +) -> Vec { + let video_queue_instance = ash::khr::video_queue::Instance::new(entry, instance); + probed_profiles() + .into_iter() + .map(|(profile, decode_profile, wanted)| { + let usages = USAGE_MATRIX + .iter() + .map(|(label, usage)| { + // SAFETY: caller contract — live instance the video_queue table was + // loaded from, and one of its physical devices. + let formats = unsafe { + query_formats_on( + &video_queue_instance, + physical_device, + decode_profile, + *usage, + ) + }; + // SAFETY: as above. + let image_format_support = unsafe { + image_format_supported( + instance, + physical_device, + decode_profile, + wanted, + *usage, + ) + }; + UsageProbe { + label, + usage: *usage, + formats, + image_format_support, + } + }) + .collect(); + ProfileProbe { + profile, + wanted, + usages, + } + }) + .collect() +} + +/// The second opinion: can an image of (`format`, `usage`) exist for this video profile, +/// according to `vkGetPhysicalDeviceImageFormatProperties2`? +/// +/// This is the same question `vkCreateImage` will be validated against +/// (VUID-VkImageCreateInfo-pNext-06811 routes through the video format properties, but +/// the general image-format query is what reports whether the combination is creatable +/// at all), asked through a DIFFERENT entry point. Where it disagrees with the video +/// format properties, one of the two driver paths is wrong — and knowing which is the +/// difference between a bug report to Intel and a fix in this repository. +/// +/// # Safety +/// +/// As [`probe_video_formats`]. +unsafe fn image_format_supported( + instance: &ash::Instance, + physical_device: vk::PhysicalDevice, + decode_profile: DecodeProfile, + format: vk::Format, + usage: vk::ImageUsageFlags, +) -> Result<(), vk::Result> { + let mut chain = decode_profile.chain(); + let profile = chain.wire(); + let mut profile_list = + vk::VideoProfileListInfoKHR::default().profiles(std::slice::from_ref(profile)); + let info = vk::PhysicalDeviceImageFormatInfo2::default() + .format(format) + .ty(vk::ImageType::TYPE_2D) + .tiling(vk::ImageTiling::OPTIMAL) + .usage(usage) + .push_next(&mut profile_list); + let mut props = vk::ImageFormatProperties2::default(); + // SAFETY: caller contract (live instance + one of its physical devices); `info` + // roots a wired chain of locals that outlive the call, and `props` is a local the + // driver fills. + unsafe { + instance.get_physical_device_image_format_properties2(physical_device, &info, &mut props) + } +} + +/// `usage` as `NAME|NAME (0xHEX)`, with any bit this build cannot name kept VISIBLE. +/// +/// The raw value is always printed beside the words for the same reason the codec-op +/// line prints its mask: a reader must be able to check the names against the number, +/// and a bit the tool has no word for must not silently vanish from a mask it reports. +pub fn describe_usage(usage: vk::ImageUsageFlags) -> String { + // The encode trio is here because NVIDIA advertises it on DECODE pictures (measured: + // 0xC000 beside the decode bits), and a mask printed as "unrecognised" invites the + // reader to wonder whether the tool is out of date rather than reading the answer. + const BITS: [(vk::ImageUsageFlags, &str); 12] = [ + (vk::ImageUsageFlags::TRANSFER_SRC, "TRANSFER_SRC"), + (vk::ImageUsageFlags::TRANSFER_DST, "TRANSFER_DST"), + (vk::ImageUsageFlags::SAMPLED, "SAMPLED"), + (vk::ImageUsageFlags::STORAGE, "STORAGE"), + (vk::ImageUsageFlags::COLOR_ATTACHMENT, "COLOR_ATTACHMENT"), + (vk::ImageUsageFlags::INPUT_ATTACHMENT, "INPUT_ATTACHMENT"), + (vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR, "DECODE_DST"), + (vk::ImageUsageFlags::VIDEO_DECODE_SRC_KHR, "DECODE_SRC"), + (vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR, "DECODE_DPB"), + (vk::ImageUsageFlags::VIDEO_ENCODE_DST_KHR, "ENCODE_DST"), + (vk::ImageUsageFlags::VIDEO_ENCODE_SRC_KHR, "ENCODE_SRC"), + (vk::ImageUsageFlags::VIDEO_ENCODE_DPB_KHR, "ENCODE_DPB"), + ]; + describe_mask(usage.as_raw(), &BITS.map(|(f, n)| (f.as_raw(), n))) +} + +/// `imageCreateFlags` as `NAME|NAME (0xHEX)`, same accounting rule as +/// [`describe_usage`]. +pub fn describe_create_flags(flags: vk::ImageCreateFlags) -> String { + const BITS: [(vk::ImageCreateFlags, &str); 5] = [ + (vk::ImageCreateFlags::MUTABLE_FORMAT, "MUTABLE_FORMAT"), + (vk::ImageCreateFlags::EXTENDED_USAGE, "EXTENDED_USAGE"), + (vk::ImageCreateFlags::ALIAS, "ALIAS"), + (vk::ImageCreateFlags::DISJOINT, "DISJOINT"), + (vk::ImageCreateFlags::PROTECTED, "PROTECTED"), + ]; + describe_mask(flags.as_raw(), &BITS.map(|(f, n)| (f.as_raw(), n))) +} + +/// The shared naming rule: named bits joined by `|`, then the raw value, then any +/// leftover bits called out as unrecognised rather than dropped. +fn describe_mask(raw: u32, bits: &[(u32, &str)]) -> String { + if raw == 0 { + return "(none) (0x0)".to_string(); + } + let mut names: Vec<&str> = bits + .iter() + .filter(|(bit, _)| raw & bit != 0) + .map(|(_, name)| *name) + .collect(); + let named: u32 = bits.iter().map(|(bit, _)| bit).sum(); + let leftover = raw & !named; + let extra; + if leftover != 0 { + extra = format!("unrecognised 0x{leftover:X}"); + names.push(&extra); + } + format!("{} (0x{raw:X})", names.join("|")) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The matrix must contain the three combinations the pools REALLY create with — + /// a probe that answers about usages nobody creates explains nothing. + #[test] + fn the_matrix_covers_every_usage_the_pools_create_with() { + for real in [COINCIDE_USAGE, DPB_USAGE, OUTPUT_USAGE] { + assert!( + USAGE_MATRIX.iter().any(|(_, u)| *u == real), + "{real:?} is created by the image pools but never probed" + ); + } + // And the two discriminators that localise a refusal. + assert!(USAGE_MATRIX.iter().any(|(_, u)| *u == DECODE_ONLY_USAGE)); + assert!(USAGE_MATRIX + .iter() + .any(|(_, u)| *u == vk::ImageUsageFlags::SAMPLED)); + } + + /// Every profile the client can negotiate gets a row, each with the picture format + /// its caps derivation will look for — a probe that reported a 10-bit profile + /// against NV12 would "find" nothing and read as a device gap. + #[test] + fn every_probed_profile_names_the_format_derivation_wants() { + let profiles = probed_profiles(); + assert!( + profiles.len() >= 5, + "expected H.264 + H.265 8/10-bit + AV1 8/10-bit, got {}", + profiles.len() + ); + for (name, _, wanted) in &profiles { + assert!( + crate::caps::OUTPUT_FORMATS.contains(wanted), + "{name} wants {wanted:?}, which is outside this crate's output vocabulary" + ); + } + assert!(profiles.iter().any(|(_, _, w)| *w == P010), "no 10-bit leg"); + } + + /// The mask printers must never drop a bit: names AND the raw value AND anything + /// unnamed. This is the accounting rule the codec-op line already follows, and the + /// reason it exists is that a silently-dropped bit reads as a capability the device + /// does not have (or worse, hides one it does). + #[test] + fn mask_descriptions_account_for_every_bit_they_print() { + assert_eq!( + describe_usage(COINCIDE_USAGE), + "SAMPLED|DECODE_DST|DECODE_DPB (0x1404)" + ); + assert_eq!(describe_usage(vk::ImageUsageFlags::empty()), "(none) (0x0)"); + // The Intel Arc envelope, verbatim — the string a field report will contain. + let intel = vk::ImageUsageFlags::TRANSFER_SRC + | vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR + | vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR; + assert_eq!( + describe_usage(intel), + "TRANSFER_SRC|DECODE_DST|DECODE_DPB (0x1401)" + ); + // A bit with no name still shows up. + let unknown = vk::ImageUsageFlags::from_raw(0x8000_0000); + assert_eq!( + describe_usage(unknown), + "unrecognised 0x80000000 (0x80000000)" + ); + assert_eq!( + describe_create_flags( + vk::ImageCreateFlags::MUTABLE_FORMAT | vk::ImageCreateFlags::EXTENDED_USAGE + ), + "MUTABLE_FORMAT|EXTENDED_USAGE (0x108)" + ); + assert_eq!( + describe_create_flags(vk::ImageCreateFlags::empty()), + "(none) (0x0)" + ); + } + + /// `wanted_entry` picks by FORMAT, so a driver that returns several entries cannot + /// hide the one derivation will act on behind a different format. + #[test] + fn the_wanted_entry_is_found_by_format_among_others() { + let probe = UsageProbe { + label: "x", + usage: COINCIDE_USAGE, + formats: Ok(vec![ + VideoFormat { + format: P010, + image_usage: COINCIDE_USAGE, + ..Default::default() + }, + VideoFormat { + format: NV12, + image_usage: DPB_USAGE, + ..Default::default() + }, + ]), + image_format_support: Ok(()), + }; + assert_eq!(probe.wanted_entry(NV12).unwrap().image_usage, DPB_USAGE); + assert!(probe.wanted_entry(crate::caps::YUV444_8).is_none()); + // A failed query has no entry at all — distinct from "returned nothing". + let failed = UsageProbe { + formats: Err(vk::Result::ERROR_IMAGE_USAGE_NOT_SUPPORTED_KHR), + ..probe + }; + assert!(failed.wanted_entry(NV12).is_none()); + } +} diff --git a/crates/pf-vkdecode/src/recovery.rs b/crates/pf-vkdecode/src/recovery.rs new file mode 100644 index 00000000..5578897e --- /dev/null +++ b/crates/pf-vkdecode/src/recovery.rs @@ -0,0 +1,634 @@ +//! The recovery-point watch: turning the recovery point SEI into a per-picture +//! "the stream is healed HERE" mark (M4 of the native-decode program). +//! +//! # Why this exists +//! +//! A host running an **intra-refresh wave** never emits an IDR: a moving band of +//! intra blocks re-codes the whole picture over ~half a second, and loss self-heals +//! as the band sweeps. That is strictly better for a stream (no 20-40× IDR spike +//! under loss) — but it leaves the CLIENT with no decoder-visible clean point. +//! libavcodec sets `AV_FRAME_FLAG_KEY` only for true IDRs (H.264 flags key when +//! `recovery_frame_cnt == 0`, HEVC only on IRAP), so a client on the FFmpeg rungs +//! sees a healed picture as an unbroken run of ordinary P-frames. Its post-loss +//! freeze therefore holds the last good frame until the shared gate's +//! `REANCHOR_FREEZE_MAX` backstop (punktfunk-core's `reanchor` module) fires and +//! forces the very IDR intra-refresh exists to avoid — half a second of frozen +//! picture on a stream that was already clean. +//! +//! The host CAN say so on the wire (`USER_FLAG_RECOVERY_POINT`), and where it does +//! the shared gate lifts on the second mark. But exactly ONE encoder backend sets +//! it: pf-encode's Linux libav-NVENC, under the `PUNKTFUNK_INTRA_REFRESH` opt-in +//! (`EncoderCaps::intra_refresh_recovery`). The other two backends that actually +//! run a wave — Windows AMF and QSV — leave it `false` pending on-glass GDR +//! validation, so THEIR intra-refresh sessions have no wire clean point at all and +//! ride the backstop on every loss. The flag also cannot help a host that predates +//! it, and it dies with the datagram that carried it. +//! +//! The bitstream, meanwhile, says it directly, and says it in a place loss cannot +//! separate from the picture it describes: an NVENC-family encoder emits a +//! **recovery point SEI** at the start of each wave, naming the picture at which +//! output becomes correct. (Not universal — pf-encode records that AMF emits none, +//! so an AMD Windows wave stays invisible either way; this is an overlapping set +//! with the wire flag's, not a superset.) pf-bitstream parses it for both codecs +//! already ([`pf_bitstream::h264::RecoveryPoint`], +//! [`pf_bitstream::h265::RecoveryPointHevc`]) and carries it on every +//! `AuPlan::picture`. This module is the piece that was missing: the small state +//! machine that remembers an outstanding recovery point and marks the picture that +//! reaches it, so the client can observe its own heal instead of waiting one out. +//! +//! It is an ADDITIONAL, independent signal. Nothing here touches the wire flags; +//! a stream with no recovery point SEI produces no marks and behaves exactly as it +//! did before. +//! +//! # The counting rules, and why they are conservative +//! +//! The two codecs count the distance to the recovery point in different units, so +//! the watch keeps two `note_*` entries rather than pretending one unit fits both: +//! +//! * **H.265** (D.3.8) counts in PICTURE ORDER: `recovery_poc_cnt` is a signed POC +//! delta from the SEI's picture to the recovery point. That is exact arithmetic +//! on a number every plan already carries, so [`RecoveryWatch::note_h265`] simply +//! remembers `poc + recovery_poc_cnt` and marks the first picture at or past it. +//! A NEGATIVE count (a recovery point among leading pictures) marks the SEI's own +//! picture, which is what "at or past" means when the target is behind us. +//! +//! * **H.264** (D.2.8) counts in `frame_num` INCREMENTS, and `frame_num` advances +//! only for reference pictures, so there is no fixed picture-to-increment ratio. +//! [`RecoveryWatch::note_h264`] therefore counts increments the only way a +//! consumer honestly can: a picture whose `frame_num` DIFFERS from the previous +//! one's spends exactly one increment. That under-counts across a `frame_num` GAP +//! (a lost reference frame skips several increments but is charged one), which +//! makes the watch mark the recovery point LATE, never early — and late is the +//! behaviour the client already has today (the backstop). A gap also re-arms the +//! client's freeze and normally brings a fresh SEI with it, so the residue is a +//! heal reported one wave later, not a stale picture presented as clean. +//! +//! Both entries drop an outstanding watch at an IDR/IRAP: a real keyframe is a +//! whole re-anchor by itself, and its POC/`frame_num` reset would make any pending +//! target meaningless. +//! +//! # An SEI is a wave START only when its target ADVANCES +//! +//! [`RecoveryMark::sei_here`] is not "this AU carried a recovery point SEI"; it is +//! "an intra-refresh wave STARTS here", and the difference decides whether a +//! consumer may trust the mark that follows. D.2.8/D.3.8 both permit re-announcing +//! the CURRENT wave's recovery point on every picture with a shrinking count, and +//! that is exactly what x264's `--intra-refresh` emits. Under a "any SEI is a new +//! wave" reading the first picture after a loss carries a fresh-looking SEI whose +//! target is the end of a wave that began BEFORE the loss — so the consumer's +//! arm-pairing lifts on a picture whose already-swept stripes still reference the +//! lost frame, presenting a partially stale picture as clean. That is precisely +//! what the wire path's `REANCHOR_MARKS_TO_LIFT = 2` exists to prevent, and the +//! local path must not be the weaker of the two. +//! +//! So an SEI is honoured as a wave start only when the target it implies lies +//! BEYOND any outstanding one (H.265: `poc + recovery_poc_cnt` strictly greater +//! than the outstanding `Target::Poc`; H.264: `recovery_frame_cnt` strictly greater +//! than the increments still owed, both measured from the same picture). Anything +//! else is a re-announcement: the further target stands and no wave start is +//! reported. The cost is that a genuinely NEW wave which is shorter than the +//! remainder of the one in flight goes unreported — a heal missed, falling back to +//! the consumer's backstop — which is the same direction every other rule here +//! errs in: late, never early. +//! +//! # What the watch deliberately does NOT decide +//! +//! Whether a mark may LIFT a post-loss freeze is the client's decision, not this +//! module's, because it depends on something no bitstream carries: when the loss +//! happened. A recovery point whose SEI arrived BEFORE the loss guarantees nothing +//! — the wave's already-swept stripes still reference the picture that was lost — +//! so the consumer must require an SEI observed at or after the loss. That is why +//! [`RecoveryMark`] reports the SEI and the recovery point as two separate facts: +//! the client pairs them against its own arm. + +use pf_bitstream::h264::RecoveryPoint; +use pf_bitstream::h265::RecoveryPointHevc; + +/// What one planned picture is worth to the recovery-point watch. +/// +/// Two independent booleans rather than one enum because a single picture can be +/// both: an encoder that emits a recovery point SEI with a zero count is saying +/// "this very picture is the clean point", and a consumer pairing SEI-then-mark +/// must see both facts on it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct RecoveryMark { + /// The AU that produced this picture CARRIED a recovery point SEI — an + /// intra-refresh wave starts (or restarts) here. The consumer uses it to + /// decide that a later [`Self::is_recovery_point`] is about a wave that began + /// after ITS loss, which is the only case a mark may be trusted in. + pub sei_here: bool, + /// This picture IS the recovery point an outstanding SEI named: decoding from + /// that SEI's AU onward, this picture's output is correct. + pub is_recovery_point: bool, +} + +impl RecoveryMark { + /// Nothing to report — the overwhelmingly common per-picture answer. + pub const NONE: RecoveryMark = RecoveryMark { + sei_here: false, + is_recovery_point: false, + }; +} + +/// The outstanding recovery point, in the codec's own counting unit. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Target { + /// H.264: increments of `frame_num` still owed, plus the `frame_num` the last + /// charged picture carried (an increment is "this picture's differs"). + FrameNumIncrements { owed: u32, last_frame_num: u16 }, + /// H.265: the absolute `PicOrderCntVal` at or past which output is correct. + Poc(i32), +} + +/// One decoder's outstanding recovery point. Pure — no Vulkan, no allocation, two +/// words of state — so the whole rule set below is CPU-testable. +#[derive(Debug, Clone, Copy, Default)] +pub struct RecoveryWatch { + target: Option, +} + +impl RecoveryWatch { + pub fn new() -> RecoveryWatch { + RecoveryWatch { target: None } + } + + /// Is a recovery point still outstanding? (Diagnostics and tests; the decode + /// path reads the per-picture [`RecoveryMark`] instead.) + pub fn is_watching(&self) -> bool { + self.target.is_some() + } + + /// Fold one planned H.264 picture. `frame_num` and `is_idr` come off the + /// plan's `PicturePlan`, `sei` is its `recovery_point` field. + /// + /// Order matters and is deliberate: the outstanding target is CHARGED for this + /// picture first, then a NEW SEI replaces it. So a wave-start AU that also + /// completes the previous wave reports both facts, and the fresh target is not + /// charged for the picture that announced it (D.2.8 counts increments *from* + /// the SEI's picture, exclusive). + /// + /// "New" is load-bearing — see [`Self::starts_a_new_wave_h264`]. + pub fn note_h264( + &mut self, + frame_num: u16, + is_idr: bool, + sei: Option, + ) -> RecoveryMark { + if is_idr { + // A real keyframe is a whole re-anchor and resets `frame_num`; any + // pending count is meaningless past it. The client lifts on the IDR + // itself, so nothing is lost by dropping the watch here. + self.target = None; + } + let mut mark = RecoveryMark::NONE; + // How many increments the OUTSTANDING wave still owes, measured from THIS + // picture (0 = it is reached here); `None` when no wave was outstanding + // when this picture arrived. It is the yardstick a fresh SEI is judged + // against below, so it has to be captured while charging. + let mut owed_here: Option = None; + if let Some(Target::FrameNumIncrements { + owed, + last_frame_num, + }) = self.target + { + // One increment per picture whose `frame_num` differs from the last + // charged one — the honest, conservative reading (module docs). + let owed = if frame_num != last_frame_num { + owed.saturating_sub(1) + } else { + owed + }; + owed_here = Some(owed); + if owed == 0 { + mark.is_recovery_point = true; + self.target = None; + } else { + self.target = Some(Target::FrameNumIncrements { + owed, + last_frame_num: frame_num, + }); + } + } + if let Some(rp) = sei { + if Self::starts_a_new_wave_h264(owed_here, rp.recovery_frame_cnt) { + mark.sei_here = true; + if rp.recovery_frame_cnt == 0 { + // "Start here and this picture is already exact" — the SEI's + // own picture is the recovery point, no waiting. + mark.is_recovery_point = true; + self.target = None; + } else { + self.target = Some(Target::FrameNumIncrements { + owed: rp.recovery_frame_cnt, + last_frame_num: frame_num, + }); + } + } + // A RE-ANNOUNCEMENT changes nothing: the outstanding target the charge + // above left in place is the FURTHER one, and keeping it is what makes + // the mark land late rather than early (module docs). + } + mark + } + + /// Does an H.264 SEI announce a wave that STARTS here, or merely re-announce + /// the one already outstanding? + /// + /// D.2.8 permits — and x264's `--intra-refresh` does — re-emitting the current + /// wave's recovery point on every picture with a DECREASING + /// `recovery_frame_cnt`. Under a "any SEI is a new wave" reading, the first + /// picture after a loss then carries a fresh-looking SEI whose target is the + /// end of a wave that began BEFORE the loss, and the consumer's arm-pairing + /// ([`RecoveryMark::sei_here`]) lifts its freeze on a picture whose + /// already-swept stripes still reference the lost frame: a partially stale + /// picture presented as clean, the one outcome the pairing exists to prevent. + /// + /// So a new wave is one whose target lies BEYOND the outstanding one. Both + /// counts are increments measured from this picture (the outstanding one after + /// this picture's charge), so they compare directly. With no wave outstanding + /// — the ordinary case, and everything after an IDR — every SEI is new. + fn starts_a_new_wave_h264(owed_here: Option, recovery_frame_cnt: u32) -> bool { + match owed_here { + Some(owed) => recovery_frame_cnt > owed, + None => true, + } + } + + /// Fold one planned H.265 picture — `pic_order_cnt` and `is_irap` off the + /// plan's `PicturePlan`, `sei` its `recovery_point`. + /// + /// `is_irap` rather than `is_idr`: a CRA/BLA also restarts the stream's + /// prediction structure, and the POC target that predates it cannot be + /// compared against the POCs that follow. + /// + /// As on the H.264 side, only an SEI whose target ADVANCES past the + /// outstanding one starts a new wave — the arithmetic here is exact, so the + /// test is `poc + delta` strictly greater than the outstanding `Target::Poc` + /// (see [`Self::starts_a_new_wave_h264`] for why). + pub fn note_h265( + &mut self, + pic_order_cnt: i32, + is_irap: bool, + sei: Option, + ) -> RecoveryMark { + if is_irap { + self.target = None; + } + let mut mark = RecoveryMark::NONE; + // The outstanding target as this picture ARRIVED — the yardstick a fresh + // SEI is judged against, captured before the charge below can clear it. + let outstanding = match self.target { + Some(Target::Poc(target)) => Some(target), + _ => None, + }; + if let Some(target) = outstanding { + if pic_order_cnt >= target { + mark.is_recovery_point = true; + self.target = None; + } + } + if let Some(rp) = sei { + // D.3.8: the target is this picture's POC plus the signed delta. A + // delta of 0 (or negative — a recovery point among leading pictures) + // lands at or behind this picture, so it is reached immediately. + let target = pic_order_cnt.saturating_add(rp.recovery_poc_cnt); + if outstanding.is_none_or(|t| target > t) { + mark.sei_here = true; + if pic_order_cnt >= target { + mark.is_recovery_point = true; + self.target = None; + } else { + self.target = Some(Target::Poc(target)); + } + } + // Otherwise it re-announces the wave already outstanding (or an + // earlier point within it): the further target the charge left in + // place stands, and this picture reports no fresh wave start. + } + mark + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn h264_sei(recovery_frame_cnt: u32) -> Option { + Some(RecoveryPoint { + recovery_frame_cnt, + // The realistic value for a rolling wave: an encoder that filters + // across the refresh boundary cannot promise bit-exact output, and + // says so. The watch must not care — see + // `an_approximate_recovery_point_still_marks`. + exact_match: false, + broken_link: false, + }) + } + + fn h265_sei(recovery_poc_cnt: i32) -> Option { + Some(RecoveryPointHevc { + recovery_poc_cnt, + exact_match: false, + broken_link: false, + }) + } + + /// The wave: an SEI announces a recovery point N increments out, and exactly + /// the Nth picture after it is marked — not the ones before, not the ones + /// after. This is the whole point of the module, on the codec whose counting + /// unit is the awkward one. + #[test] + fn an_h264_wave_marks_the_picture_the_sei_counted_to() { + let mut w = RecoveryWatch::new(); + // The wave starts on frame_num 10 and is three increments long. + let start = w.note_h264(10, false, h264_sei(3)); + assert!(start.sei_here, "the wave start is reported"); + assert!( + !start.is_recovery_point, + "a count of 3 is not reached on the picture that announced it" + ); + assert!(w.is_watching()); + assert_eq!(w.note_h264(11, false, None), RecoveryMark::NONE); + assert_eq!(w.note_h264(12, false, None), RecoveryMark::NONE); + let healed = w.note_h264(13, false, None); + assert!(healed.is_recovery_point, "the third increment is the heal"); + assert!(!healed.sei_here); + assert!(!w.is_watching(), "and the watch is spent"); + // Nothing after it is marked — a mark is a moment, not a state. + assert_eq!(w.note_h264(14, false, None), RecoveryMark::NONE); + } + + /// `frame_num` does not advance for a non-reference picture, and D.2.8 counts + /// INCREMENTS — so a repeated `frame_num` must not be charged, or the mark + /// lands early on a picture the wave has not reached. + #[test] + fn a_repeated_frame_num_spends_no_increment() { + let mut w = RecoveryWatch::new(); + w.note_h264(4, false, h264_sei(2)); + // Two pictures at the same frame_num: one increment, not two. + assert_eq!(w.note_h264(5, false, None), RecoveryMark::NONE); + assert_eq!(w.note_h264(5, false, None), RecoveryMark::NONE); + assert!( + w.note_h264(6, false, None).is_recovery_point, + "the second true increment completes the count" + ); + } + + /// A `frame_num` GAP (the lost reference frame this whole program exists for) + /// skips several increments but is charged one, so the mark can only land + /// LATE. Late is today's behaviour (the freeze backstop); early would present + /// a half-swept picture as clean, which is the one outcome that must be + /// impossible. + #[test] + fn a_frame_num_gap_delays_the_mark_it_never_advances_it() { + let mut w = RecoveryWatch::new(); + w.note_h264(100, false, h264_sei(2)); + // frame_num jumps 101 → 104: three real increments, charged as one. + assert_eq!(w.note_h264(104, false, None), RecoveryMark::NONE); + assert!( + w.note_h264(105, false, None).is_recovery_point, + "the count still has to be walked off — never short-circuited" + ); + } + + /// `recovery_frame_cnt == 0` means "start decoding here; this picture is + /// already exact". Both facts land on the one picture. + #[test] + fn a_zero_count_marks_the_pictures_that_carries_the_sei() { + let mut w = RecoveryWatch::new(); + let m = w.note_h264(7, false, h264_sei(0)); + assert!(m.sei_here && m.is_recovery_point); + assert!(!w.is_watching()); + } + + /// A wave that reaches PAST the outstanding one supersedes it: the newest SEI + /// is the encoder's current statement about where the picture becomes correct, + /// and its count is measured from ITS own picture. + #[test] + fn a_new_sei_that_reaches_further_replaces_an_outstanding_watch() { + let mut w = RecoveryWatch::new(); + w.note_h264(1, false, h264_sei(2)); + // A second, LONGER wave two pictures in: 4 increments from here, against + // the 1 the first wave still owed. + let restart = w.note_h264(3, false, h264_sei(4)); + assert!(restart.sei_here, "it reaches further — a fresh wave starts"); + assert!(!restart.is_recovery_point); + for fnum in 4..7 { + assert_eq!(w.note_h264(fnum, false, None), RecoveryMark::NONE); + } + assert!( + w.note_h264(7, false, None).is_recovery_point, + "the SECOND wave's count is what completes, not the first's" + ); + } + + /// The re-announcement rule, on the codec whose counting unit is the awkward + /// one. x264's `--intra-refresh` re-emits the CURRENT wave's recovery point on + /// every picture with a DECREASING `recovery_frame_cnt` (legal under D.2.8). + /// Read as a wave START, every one of those would let a consumer that armed a + /// freeze mid-wave lift on the tail of a wave that began BEFORE its loss — a + /// half-stale picture presented as clean. + #[test] + fn a_decreasing_re_announcement_is_the_same_wave_not_a_new_one() { + let mut w = RecoveryWatch::new(); + // The wave starts on frame_num 10, four increments long, and re-announces + // itself on every picture: 4, 3, 2, 1, 0. + let start = w.note_h264(10, false, h264_sei(4)); + assert!(start.sei_here, "the FIRST announcement is a wave start"); + for (fnum, cnt) in [(11, 3), (12, 2), (13, 1)] { + let m = w.note_h264(fnum, false, h264_sei(cnt)); + assert!( + !m.sei_here, + "frame_num {fnum} re-announces the same wave — not a fresh start" + ); + assert!(!m.is_recovery_point, "and the wave is not there yet"); + } + // The wave completes, and the trailing `recovery_frame_cnt == 0` on that + // very picture must NOT read as a brand-new wave either: a consumer that + // armed mid-wave would otherwise see sei_here + is_recovery_point on one + // picture and lift on the wave it already discounted. + let healed = w.note_h264(14, false, h264_sei(0)); + assert!(healed.is_recovery_point, "the wave really did complete"); + assert!( + !healed.sei_here, + "…but nothing NEW started here — the count only walked to zero" + ); + assert!(!w.is_watching()); + } + + /// The H.265 twin, in exact POC arithmetic: a wave announced at POC 10 for POC + /// 20, re-announced on every picture with the same absolute target. + #[test] + fn an_h265_re_announcement_of_the_same_target_is_not_a_new_wave() { + let mut w = RecoveryWatch::new(); + assert!(w.note_h265(10, false, h265_sei(10)).sei_here); + for poc in 11..20 { + let m = w.note_h265(poc, false, h265_sei(20 - poc)); + assert!(!m.sei_here, "poc {poc} re-announces target 20"); + assert!(!m.is_recovery_point); + } + let healed = w.note_h265(20, false, h265_sei(0)); + assert!(healed.is_recovery_point); + assert!(!healed.sei_here, "the target did not advance past 20"); + // A genuinely later wave DOES start, and is reported. + assert!(w.note_h265(21, false, h265_sei(8)).sei_here); + } + + /// An SEI that reaches SHORT of the outstanding target is a re-announcement + /// too, and the further target is what stands — the module's "late, never + /// early" rule. (A real new-but-shorter wave therefore goes unreported: one + /// heal missed, falling back to the consumer's backstop, which is the safe + /// side of this trade.) + #[test] + fn an_sei_that_reaches_short_of_the_outstanding_target_never_shortens_the_watch() { + let mut w = RecoveryWatch::new(); + w.note_h265(0, false, h265_sei(20)); // target 20 + let short = w.note_h265(1, false, h265_sei(2)); // would target 3 + assert!(!short.sei_here); + assert_eq!(w.note_h265(3, false, None), RecoveryMark::NONE, "not at 20"); + assert!(w.note_h265(20, false, None).is_recovery_point); + + let mut w = RecoveryWatch::new(); + w.note_h264(0, false, h264_sei(6)); + assert!(!w.note_h264(1, false, h264_sei(2)).sei_here); + // The original six increments are still what has to be walked off. + for fnum in 2..6 { + assert_eq!(w.note_h264(fnum, false, None), RecoveryMark::NONE); + } + assert!(w.note_h264(6, false, None).is_recovery_point); + } + + /// An IDR/IRAP clears the outstanding target, so the very next SEI is a wave + /// start again however small its count — the re-announcement rule must not + /// leave a stream unable to report a fresh wave after a keyframe. + #[test] + fn a_keyframe_makes_the_next_sei_a_wave_start_again() { + let mut w = RecoveryWatch::new(); + w.note_h264(10, false, h264_sei(9)); + w.note_h264(0, true, None); // IDR + assert!( + w.note_h264(1, false, h264_sei(1)).sei_here, + "a one-increment wave after an IDR is still a fresh wave" + ); + + let mut w = RecoveryWatch::new(); + w.note_h265(100, false, h265_sei(50)); + w.note_h265(0, true, None); // IRAP + assert!(w.note_h265(1, false, h265_sei(2)).sei_here); + } + + /// An IDR is a whole re-anchor and resets `frame_num`; a target counted + /// against the old numbering must not survive it. + #[test] + fn an_idr_drops_a_pending_h264_watch() { + let mut w = RecoveryWatch::new(); + w.note_h264(200, false, h264_sei(2)); + assert!(w.is_watching()); + let idr = w.note_h264(0, true, None); + assert_eq!( + idr, + RecoveryMark::NONE, + "the IDR is not a recovery-point mark" + ); + assert!(!w.is_watching()); + // The pictures after it are ordinary — no stale mark fires. + assert_eq!(w.note_h264(1, false, None), RecoveryMark::NONE); + assert_eq!(w.note_h264(2, false, None), RecoveryMark::NONE); + } + + /// H.265 counts in POC, which is exact arithmetic — the target is hit at or + /// past the announced value even when POC steps by more than one. + #[test] + fn an_h265_wave_marks_the_first_picture_at_or_past_the_target_poc() { + let mut w = RecoveryWatch::new(); + let start = w.note_h265(10, false, h265_sei(4)); + assert!(start.sei_here && !start.is_recovery_point); + assert_eq!(w.note_h265(11, false, None), RecoveryMark::NONE); + assert_eq!(w.note_h265(13, false, None), RecoveryMark::NONE); + // POC 14 is the target exactly. + assert!(w.note_h265(14, false, None).is_recovery_point); + assert!(!w.is_watching()); + + // …and a stream whose POC steps OVER the target still marks: "at or past" + // is the rule, because nothing guarantees the exact value is coded. + let mut w = RecoveryWatch::new(); + w.note_h265(0, false, h265_sei(3)); + assert!(w.note_h265(8, false, None).is_recovery_point); + } + + /// `recovery_poc_cnt` is se(v)-coded and may be negative (a recovery point + /// among leading pictures) — the target is then behind us and the SEI's own + /// picture is already the clean one. It must not become an infinite watch. + #[test] + fn a_negative_or_zero_poc_count_marks_immediately() { + for count in [0, -1, -7] { + let mut w = RecoveryWatch::new(); + let m = w.note_h265(30, false, h265_sei(count)); + assert!(m.sei_here && m.is_recovery_point, "count {count}"); + assert!(!w.is_watching(), "count {count}"); + } + } + + /// Any IRAP — not just an IDR — restarts prediction and re-bases POC, so a + /// target counted against the previous numbering cannot be compared past it. + #[test] + fn an_irap_drops_a_pending_h265_watch() { + let mut w = RecoveryWatch::new(); + w.note_h265(500, false, h265_sei(3)); + assert!(w.is_watching()); + assert_eq!(w.note_h265(0, true, None), RecoveryMark::NONE); + assert!(!w.is_watching()); + assert_eq!(w.note_h265(1, false, None), RecoveryMark::NONE); + } + + /// `exact_match_flag == 0` is the NORMAL value for a rolling wave (loop + /// filtering bleeds across the refresh boundary, so the encoder promises + /// approximate rather than bit-exact output). Requiring exactness would make + /// this whole module never fire on the streams it was written for — and an + /// approximately-correct picture is not the failure mode the freeze exists to + /// hide, which is a gray plate with motion painted on it. + #[test] + fn an_approximate_recovery_point_still_marks() { + let mut w = RecoveryWatch::new(); + assert!( + w.note_h264( + 1, + false, + Some(RecoveryPoint { + recovery_frame_cnt: 0, + exact_match: false, + broken_link: true, + }) + ) + .is_recovery_point, + "neither exact_match nor broken_link may veto the mark" + ); + let mut w = RecoveryWatch::new(); + assert!( + w.note_h265( + 1, + false, + Some(RecoveryPointHevc { + recovery_poc_cnt: 0, + exact_match: false, + broken_link: true, + }) + ) + .is_recovery_point + ); + } + + /// A stream with no recovery point SEI at all — every punktfunk host today — + /// produces no marks whatsoever. The signal is purely additive. + #[test] + fn a_stream_without_recovery_point_seis_never_marks() { + let mut w = RecoveryWatch::new(); + for n in 0..64u16 { + assert_eq!(w.note_h264(n, n == 0, None), RecoveryMark::NONE); + } + let mut w = RecoveryWatch::new(); + for n in 0..64i32 { + assert_eq!(w.note_h265(n, n == 0, None), RecoveryMark::NONE); + } + } +} diff --git a/crates/pf-vkdecode/src/ring.rs b/crates/pf-vkdecode/src/ring.rs new file mode 100644 index 00000000..c82c7fa6 --- /dev/null +++ b/crates/pf-vkdecode/src/ring.rs @@ -0,0 +1,938 @@ +//! Host-visible bitstream upload ring: one persistent-mapped `VIDEO_DECODE_SRC` +//! buffer cut into equal slots, honouring the profile's +//! `minBitstreamBufferOffsetAlignment`/`SizeAlignment`. +//! +//! Split like the rest of the crate: [`RingLayout`] + [`SlotStates`] are the pure, +//! unit-tested halves (offset/alignment math including growth, and the recycle +//! bookkeeping); [`BitstreamRing`] is the thin Vulkan half that allocates the +//! buffer and copies AU bytes. Slots recycle when the timeline value of the submit +//! that consumed them completes; an AU larger than the slot size grows the ring by +//! RECREATING the buffer (after draining every in-flight slot) — growth is rare +//! (an IDR burst outsizing the initial slots) and a stall there beats permanently +//! oversized slots. + +use ash::vk; +use tracing::debug; + +use crate::caps::DecodeProfile; +use crate::device::find_memory_type; +use crate::device::AllocError; +use crate::device::DecodeDevice; + +/// Initial per-slot capacity. Sized for comfort at streaming bitrates (a 4K IDR at +/// punktfunk rates is a few hundred KiB); the ring grows on first contact with a +/// larger AU rather than pre-reserving worst cases. +pub const INITIAL_SLOT_SIZE: u64 = 2 * 1024 * 1024; +/// Slot count: enough to keep uploads ahead of a couple of in-flight decodes; the +/// pipeline depth itself is bounded by the output/query rings, not by this. +pub const RING_SLOTS: u32 = 4; + +/// `x` rounded up to a multiple of power-of-two `align`. +const fn align_up(x: u64, align: u64) -> u64 { + (x + align - 1) & !(align - 1) +} + +/// Pure geometry of the ring buffer. Both Vulkan alignments are powers of two per +/// the spec's alignment-value convention, which [`RingLayout::new`] debug-asserts; +/// the slot size is a multiple of BOTH, so every slot offset satisfies the offset +/// alignment and every full-slot range satisfies the size alignment. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RingLayout { + pub slot_size: u64, + pub slots: u32, + pub offset_alignment: u64, + pub size_alignment: u64, +} + +impl RingLayout { + pub fn new(min_slot_size: u64, slots: u32, offset_alignment: u64, size_alignment: u64) -> Self { + debug_assert!( + offset_alignment.is_power_of_two() && size_alignment.is_power_of_two(), + "Vulkan alignment values are powers of two" + ); + debug_assert!(slots > 0 && min_slot_size > 0); + let align = offset_alignment.max(size_alignment); + Self { + slot_size: align_up(min_slot_size, align), + slots, + offset_alignment, + size_alignment, + } + } + + /// Byte offset of `slot` — a `minBitstreamBufferOffsetAlignment` multiple by + /// construction. + pub fn offset_of(&self, slot: u32) -> u64 { + debug_assert!(slot < self.slots); + u64::from(slot) * self.slot_size + } + + /// Whether an AU of `len` bytes fits one slot (its aligned range included). + pub fn fits(&self, len: u64) -> bool { + self.record_range(len) <= self.slot_size + } + + /// The `srcBufferRange` to record for an AU of `len` bytes: the length rounded + /// up to `minBitstreamBufferSizeAlignment`. + pub fn record_range(&self, len: u64) -> u64 { + align_up(len, self.size_alignment) + } + + /// Total buffer size. + pub fn buffer_size(&self) -> u64 { + self.slot_size * u64::from(self.slots) + } + + /// The layout a recreation adopts so an AU of `len` bytes fits with headroom: + /// slot size doubles from the current one until sufficient (geometric growth — + /// one recreation per size class, not one per oversized AU). + pub fn grown_for(&self, len: u64) -> Self { + let mut slot = self.slot_size.max(1); + while align_up(len, self.size_alignment) > slot { + slot *= 2; + } + Self::new(slot, self.slots, self.offset_alignment, self.size_alignment) + } +} + +/// One AU's slice NALUs as they will actually sit in a ring slot: the AU byte +/// ranges to concatenate, and the offsets those ranges land at. +/// +/// The two are produced TOGETHER by [`pack_slices`] and consumed together +/// ([`BitstreamRing::upload`] writes `segments`, the recording layer submits +/// `offsets`) precisely because they cannot be allowed to disagree: an offset +/// that does not land on the byte the segment starts at points the hardware at +/// the middle of somebody else's slice, which is silent corruption rather than +/// an error. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct PackedSlices { + /// The AU ranges to concatenate, each starting at a THREE-byte Annex-B start + /// code (see [`three_byte_prefix`]). + pub(crate) segments: Vec>, + /// The `pSliceOffsets` / `pSliceSegmentOffsets` those ranges land at once + /// concatenated — one per segment, in the same order. + pub(crate) offsets: Vec, +} + +/// `segment` with any Annex-B `zero_byte`s ahead of its start code dropped, so it +/// begins at exactly `00 00 01`. +/// +/// **This is load-bearing, not tidying.** Annex-B admits both a three-byte start +/// code and a four-byte one (a `zero_byte` ahead of it, B.1.2/B.2.2) and leaves the +/// choice to the encoder, which is exactly the trap: the vendored H.265 vector +/// carries `00 00 00 01` on 249 of its 250 slice segments, while its H.264 twin +/// carries `00 00 01` on all 500 of its. Both planners hand out ranges that begin at +/// whichever form the stream happened to use, so WITHOUT this normalisation the +/// codec that works and the codec that corrupts are decided by the encoder that +/// produced the file — which is precisely how this shipped: the H.264 rung was +/// bit-exact on 250/250 frames while H.265 failed on 247/250, through the same ring. +/// NOTHING structural protects H.264; it is latently exposed to any stream whose +/// encoder prefixes slices with four bytes (NVENC and AMF among them). +/// +/// Vulkan's slice offsets, however, are consumed by drivers written against +/// libavcodec's `ff_vk_decode_add_slice`, which DISCARDS the stream's prefix and +/// writes its own `{ 0x00, 0x00, 0x01 }` before each slice, pointing the offset at +/// that. A three-byte prefix at the offset is therefore the only byte pattern any +/// driver has been validated on, and NVIDIA's takes it literally: it reads the +/// slice segment header at `offset + 3 + 2` (prefix plus the two-byte H.265 NAL +/// unit header) rather than scanning for the prefix. A four-byte prefix shifts its +/// bit reader one byte early — onto the NAL header's SECOND byte — and every +/// syntax element it decodes afterwards is garbage; the driver says so +/// (`Invalid PPS/SPS id in slice header (pps_id=115)`, 115 and 119 being what +/// `nuh_temporal_id_plus1 = 1` followed by this vector's two slice-header first +/// bytes decode to as `ue(v)`). +/// +/// The sibling DXVA packer (`pf-dxvadec`'s `pack`) normalises to three bytes for +/// exactly this reason and says so; the Vulkan path did not, which is the bug this +/// function fixes. Dropping leading zeros achieves the same normalisation as +/// FFmpeg's rewrite without a second copy: the prefix shrinks to `00 00 01` and the +/// NALU behind it is untouched. +/// +/// A range that is not a start code at all (a hand-built plan) is returned +/// unchanged — the loop only ever drops a zero that is followed by two more zeros, +/// so it can never eat into `00 00 01` itself. +fn three_byte_prefix(au: &[u8], segment: &std::ops::Range) -> std::ops::Range { + let mut start = segment.start; + while segment.end - start > 3 && au[start..start + 3] == [0, 0, 0] { + start += 1; + } + start..segment.end +} + +/// How `segments` of `au` pack into one ring slot: prefixes normalised, offsets +/// rebased out of AU coordinates. +/// +/// The rebase exists because the bitstream buffer carries the SLICE NALUs ONLY +/// (module docs: non-VCL NALUs in the submitted range hang VCN firmware), while +/// both planners hand out AU-RELATIVE offsets. Submitting the plan's offsets +/// unchanged would point the hardware at bytes that were never uploaded — for +/// H.265 that is the whole reason [`crate::DecodePlanVkH265::slice_offsets`] +/// documents itself as "NOT submission-final". Both codecs' recording paths call +/// this, so the packing is written (and tested) once. +/// +/// Offsets are `u32` because Vulkan's are; a packed AU large enough to overflow +/// one cannot fit any ring slot this crate allocates (4 GiB of slice data), and +/// the sum is taken in `u64` so the check is real rather than a wrapped compare. +pub(crate) fn pack_slices(au: &[u8], segments: &[std::ops::Range]) -> Option { + let mut packed = Vec::with_capacity(segments.len()); + let mut offsets = Vec::with_capacity(segments.len()); + let mut cursor: u64 = 0; + for segment in segments { + let segment = three_byte_prefix(au, segment); + offsets.push(u32::try_from(cursor).ok()?); + cursor += segment.len() as u64; + packed.push(segment); + } + Some(PackedSlices { + segments: packed, + offsets, + }) +} + +/// One AU's AV1 tile payloads as they will sit in a ring slot: the AU byte ranges +/// to concatenate, and the offset each lands at. +/// +/// [`PackedSlices`]' AV1 twin, and a separate type rather than a flag because the +/// two differ in exactly the thing that must never be confused: an Annex-B slice +/// gets its start-code prefix NORMALISED ([`three_byte_prefix`]) and an AV1 tile +/// must not be touched at all. AV1 has no start codes — a tile payload is entropy- +/// coded bytes that may legitimately begin `00 00 00`, and trimming those would +/// silently shorten the tile the driver decodes. +/// +/// The segments are the RAW TILE PAYLOADS, not the OBUs that carried them: the +/// bitstream buffer holds nothing else (see [`crate::decoder_av1`]), so `offsets[i]` +/// is directly tile `i`'s `pTileOffsets` entry. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct PackedAv1Tiles { + /// The AU ranges to concatenate, verbatim and in order. + pub(crate) segments: Vec>, + /// The offset each range lands at once concatenated — one per segment, in the + /// same order. + pub(crate) offsets: Vec, +} + +/// How AV1 `tiles` of `au` pack into one ring slot: verbatim, with the offset each +/// lands at. +/// +/// The offsets exist for the reason [`pack_slices`]' do — the plan's ranges are +/// AU-relative and the buffer holds only what was uploaded — but the packing itself +/// is a plain concatenation: see [`PackedAv1Tiles`] for why no prefix normalisation +/// happens (or may happen) here. +/// +/// Offsets are `u32` because Vulkan's are; a packed AU large enough to overflow +/// one cannot fit any ring slot this crate allocates, and the sum is taken in +/// `u64` so the check is real rather than a wrapped compare. +pub(crate) fn pack_av1_tiles(tiles: &[std::ops::Range]) -> Option { + let mut offsets = Vec::with_capacity(tiles.len()); + let mut cursor: u64 = 0; + for tile in tiles { + offsets.push(u32::try_from(cursor).ok()?); + cursor += tile.len() as u64; + } + // The END of the last segment must also be expressible: `pTileSizes` and the + // recorded `srcBufferRange` are read against it. + u32::try_from(cursor).ok()?; + Some(PackedAv1Tiles { + segments: tiles.to_vec(), + offsets, + }) +} + +/// Concatenate `segments` of `au` into `dst`, zeroing whatever is left of it. +/// +/// The zero tail matters: `dst` is a whole recorded `srcBufferRange` (the packed +/// length rounded up to `minBitstreamBufferSizeAlignment`), so without it the +/// driver would be handed the previous AU's bytes past this one's end. +/// +/// Shared with [`BitstreamRing::upload`] rather than inlined there so the CPU +/// tests assert against the bytes the ring ACTUALLY receives. +/// +/// # Panics +/// +/// If `segments` are not in-bounds ranges of `au`, or their total length exceeds +/// `dst` — both caller invariants [`BitstreamRing::upload`] establishes from the +/// layout (and a panic beats a wild write either way). +pub(crate) fn pack_into(dst: &mut [u8], au: &[u8], segments: &[std::ops::Range]) { + let mut cursor = 0usize; + for segment in segments { + let bytes = &au[segment.clone()]; + dst[cursor..cursor + bytes.len()].copy_from_slice(bytes); + cursor += bytes.len(); + } + dst[cursor..].fill(0); +} + +/// Pure recycle bookkeeping: which slots are free, which carry an in-flight token. +/// Generic over the token so the FIFO/recycle behaviour is testable without a +/// device (the ring instantiates `T = (vk::Semaphore, u64)`). +#[derive(Debug)] +pub(crate) struct SlotStates { + pending: Vec>, + /// Round-robin cursor: slots are handed out in order, so the slot AT the + /// cursor is always the oldest in-flight one — the right one to wait on. + cursor: usize, +} + +impl SlotStates { + pub(crate) fn new(slots: usize) -> Self { + Self { + pending: (0..slots).map(|_| None).collect(), + cursor: 0, + } + } + + /// Acquire the next slot in round-robin order. `is_done` is consulted when the + /// slot still carries a token (`Ok(true)` frees it); returning `Ok(false)` + /// yields `Ok(None)` — the caller then waits on [`Self::oldest`]'s token and + /// retries. Errors pass through untouched. + pub(crate) fn acquire( + &mut self, + mut is_done: impl FnMut(&T) -> Result, + ) -> Result, E> { + let slot = self.cursor; + if let Some(token) = &self.pending[slot] { + if !is_done(token)? { + return Ok(None); + } + self.pending[slot] = None; + } + self.cursor = (self.cursor + 1) % self.pending.len(); + Ok(Some(slot)) + } + + /// The oldest in-flight token (the one blocking [`Self::acquire`]), if any. + pub(crate) fn oldest(&self) -> Option<&T> { + self.pending[self.cursor].as_ref() + } + + /// Record `token` as `slot`'s in-flight use. + pub(crate) fn set_pending(&mut self, slot: usize, token: T) { + debug_assert!( + self.pending[slot].is_none(), + "slot handed out while pending" + ); + self.pending[slot] = Some(token); + } + + /// All in-flight tokens (drain-before-recreate walks these). + pub(crate) fn in_flight(&self) -> impl Iterator { + self.pending.iter().filter_map(Option::as_ref) + } + + /// Forget every token (after the caller has drained them). + pub(crate) fn clear(&mut self) { + for p in &mut self.pending { + *p = None; + } + self.cursor = 0; + } +} + +/// One uploaded AU: what `vkCmdDecodeVideoKHR` needs plus the slot to mark pending +/// once the submit's timeline token exists. +#[derive(Debug, Clone, Copy)] +pub(crate) struct UploadedAu { + pub offset: u64, + pub range: u64, + pub slot: usize, +} + +/// The in-flight token a used slot waits on: a timeline (semaphore, value) pair — +/// the same pair the submit that consumed the slot signalled. +pub(crate) type Token = (vk::Semaphore, u64); + +/// The Vulkan half: buffer + memory + persistent map. Created against the session's +/// video profile (the spec requires the src buffer to be profile-listed). +pub(crate) struct BitstreamRing { + device: ash::Device, + layout: RingLayout, + profile: DecodeProfile, + buffer: vk::Buffer, + memory: vk::DeviceMemory, + ptr: *mut u8, + pub(crate) pending: SlotStates, +} + +impl BitstreamRing { + /// Allocate the buffer for `layout`. + /// + /// # Safety + /// + /// `dev` wraps live handles ([`crate::DeviceHandles`] contract). + pub(crate) unsafe fn create( + dev: &DecodeDevice, + layout: RingLayout, + profile: DecodeProfile, + ) -> Result { + // SAFETY: live device; allocate_backing only creates objects it returns. + let (buffer, memory, ptr) = unsafe { Self::allocate_backing(dev, &layout, profile)? }; + Ok(Self { + device: dev.ash().clone(), + layout, + profile, + buffer, + memory, + ptr, + pending: SlotStates::new(layout.slots as usize), + }) + } + + pub(crate) fn buffer(&self) -> vk::Buffer { + self.buffer + } + + /// # Safety + /// + /// As [`Self::create`]. + unsafe fn allocate_backing( + dev: &DecodeDevice, + layout: &RingLayout, + decode_profile: DecodeProfile, + ) -> Result<(vk::Buffer, vk::DeviceMemory, *mut u8), AllocError> { + let mut chain = decode_profile.chain(); + let profile = chain.wire(); + let mut profile_list = + vk::VideoProfileListInfoKHR::default().profiles(std::slice::from_ref(profile)); + let ci = vk::BufferCreateInfo::default() + .size(layout.buffer_size()) + .usage(vk::BufferUsageFlags::VIDEO_DECODE_SRC_KHR) + .sharing_mode(vk::SharingMode::EXCLUSIVE) + .push_next(&mut profile_list); + // SAFETY: live device; `ci` roots a chain of locals outliving the call. + let buffer = unsafe { dev.ash().create_buffer(&ci, None)? }; + // SAFETY: `buffer` was just created on this device. + let req = unsafe { dev.ash().get_buffer_memory_requirements(buffer) }; + let mem_props = dev.memory_properties(); + let type_index = match find_memory_type( + &mem_props, + req.memory_type_bits, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + ) { + Ok(index) => index, + Err(e) => { + // SAFETY: destroying the just-created, never-bound buffer. + unsafe { dev.ash().destroy_buffer(buffer, None) }; + return Err(e); + } + }; + let alloc = vk::MemoryAllocateInfo::default() + .allocation_size(req.size) + .memory_type_index(type_index); + // SAFETY: live device; on failure the buffer is destroyed before returning + // so nothing leaks. + let memory = match unsafe { dev.ash().allocate_memory(&alloc, None) } { + Ok(m) => m, + Err(e) => { + // SAFETY: destroying the just-created, never-bound buffer. + unsafe { dev.ash().destroy_buffer(buffer, None) }; + return Err(e.into()); + } + }; + // SAFETY: fresh buffer + fresh memory of at least the required size. + if let Err(e) = unsafe { dev.ash().bind_buffer_memory(buffer, memory, 0) } { + // SAFETY: unwinding the two objects created above (unbound/unused). + unsafe { + dev.ash().destroy_buffer(buffer, None); + dev.ash().free_memory(memory, None); + } + return Err(e.into()); + } + // SAFETY: `memory` is HOST_VISIBLE and unmapped; WHOLE_SIZE maps its full + // range for the buffer's lifetime (vkFreeMemory implicitly unmaps). + let ptr = match unsafe { + dev.ash() + .map_memory(memory, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty()) + } { + Ok(p) => p.cast::(), + Err(e) => { + // SAFETY: unwinding the two objects created above. + unsafe { + dev.ash().destroy_buffer(buffer, None); + dev.ash().free_memory(memory, None); + } + return Err(e.into()); + } + }; + Ok((buffer, memory, ptr)) + } + + /// Upload one AU, recycling or growing as needed. + /// + /// `poll`/`wait` bridge to the caller's timeline-semaphore facts: `poll` + /// answers "has this token completed?" without blocking; `wait` blocks until + /// it has (bounded by the caller's timeout policy). The split keeps this + /// module free of any semaphore knowledge. + /// + /// `segments` are the byte ranges of `au` to upload, CONCATENATED — the + /// decoder passes the SLICE NALUs only. The buffer must contain nothing but + /// slice data: the VCN firmware scans the submitted range itself, and + /// non-slice NALUs (AUD/SEI/SPS/PPS, which real AUs open with) in the range + /// hang it — the 2026-08 .25 `vcn_unified_0 ring timeout`. FFmpeg's decoder + /// feeds slices-only for the same reason; parameter sets ride the session + /// parameters object instead. + /// + /// They must be [`pack_slices`]' output, not the plan's raw ranges: the + /// offsets the recording layer submits are computed from the same call, and + /// the start-code normalisation there is what keeps the driver's slice-header + /// parse in step with the bytes ([`three_byte_prefix`]). + /// + /// # Safety + /// + /// Live device (contract); `segments` are in-bounds ranges of `au`; and the + /// tokens passed to prior [`SlotStates::set_pending`] calls genuinely cover + /// every GPU read of their slots — recycling rewrites slot bytes as soon as a + /// token reports done. + pub(crate) unsafe fn upload>( + &mut self, + dev: &DecodeDevice, + au: &[u8], + segments: &[std::ops::Range], + poll: &mut dyn FnMut(&Token) -> Result, + wait: &mut dyn FnMut(&Token) -> Result<(), E>, + ) -> Result { + let len: u64 = segments.iter().map(|s| s.len() as u64).sum(); + if !self.layout.fits(len) { + // Grow: drain EVERYTHING in flight (their reads target the old buffer), + // then recreate the backing under the grown layout. + for token in self.pending.in_flight() { + wait(token)?; + } + self.pending.clear(); + let grown = self.layout.grown_for(len); + debug!( + old = self.layout.slot_size, + new = grown.slot_size, + au = len, + "bitstream ring grows for an oversized AU" + ); + // SAFETY: every in-flight read was drained above; destroy_backing only + // touches this ring's own objects. + unsafe { self.destroy_backing() }; + // SAFETY: caller's live-device contract. + let (buffer, memory, ptr) = + unsafe { Self::allocate_backing(dev, &grown, self.profile)? }; + self.layout = grown; + self.buffer = buffer; + self.memory = memory; + self.ptr = ptr; + self.pending = SlotStates::new(grown.slots as usize); + } + + let slot = match self.pending.acquire(&mut *poll)? { + Some(slot) => slot, + None => { + // The oldest slot is still in flight: wait it out, then retry — + // guaranteed to succeed now. + if let Some(token) = self.pending.oldest() { + wait(token)?; + } + self.pending + .acquire(|_| Ok(true))? + .expect("the waited slot is free") + } + }; + + let offset = self.layout.offset_of(slot as u32); + let range = self.layout.record_range(len); + // SAFETY: `ptr` is the live persistent mapping of a buffer of + // `layout.buffer_size()` bytes; `offset + range <= buffer_size` because + // `range <= slot_size` (fits/grown above) and offset is `slot * slot_size` + // with `slot < slots`, so the `range` bytes from `offset` are one whole + // initialized, aliasing-free slot of the mapping. The slot is not + // concurrently read: its previous use completed (poll/wait above) and its + // next use is submitted after this copy. + let slot_bytes = unsafe { + std::slice::from_raw_parts_mut(self.ptr.add(offset as usize), range as usize) + }; + // Segments are in-bounds ranges of `au` (fn contract) summing to `len`, + // and `len <= range` (fits/grown above), so `pack_into` cannot panic. + pack_into(slot_bytes, au, segments); + Ok(UploadedAu { + offset, + range, + slot, + }) + } + + /// Destroy buffer + memory (which implicitly unmaps). Callers must have + /// drained in-flight reads first. + /// + /// # Safety + /// + /// Live device; no submitted-and-unfinished GPU work reads the buffer. + unsafe fn destroy_backing(&mut self) { + // SAFETY: the fn-level contract — objects are this ring's own, reads drained. + unsafe { + self.device.destroy_buffer(self.buffer, None); + self.device.free_memory(self.memory, None); + } + self.buffer = vk::Buffer::null(); + self.memory = vk::DeviceMemory::null(); + self.ptr = std::ptr::null_mut(); + } +} + +impl Drop for BitstreamRing { + fn drop(&mut self) { + if self.buffer == vk::Buffer::null() { + return; + } + // SAFETY: the owning decoder drains its queue before dropping state (and the + // borrowed device is alive by the DeviceHandles liveness contract). + unsafe { self.destroy_backing() }; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn slot_offsets_and_ranges_honour_both_alignments() { + // Deliberately DIFFERENT alignments: offset 256, size 64. + let layout = RingLayout::new(1000, 4, 256, 64); + // Slot size rounds up to a multiple of max(256, 64). + assert_eq!(layout.slot_size, 1024); + for slot in 0..4 { + assert_eq!(layout.offset_of(slot) % 256, 0, "offset alignment"); + } + assert_eq!(layout.buffer_size(), 4096); + // Ranges round to the SIZE alignment, independent of the offset one. + assert_eq!(layout.record_range(1), 64); + assert_eq!(layout.record_range(64), 64); + assert_eq!(layout.record_range(65), 128); + assert!(layout.fits(1024)); + assert!(!layout.fits(1025)); + } + + #[test] + fn growth_doubles_the_slot_size_until_the_au_fits_and_keeps_alignment() { + let layout = RingLayout::new(1024, 4, 128, 128); + let grown = layout.grown_for(5000); + assert_eq!(grown.slot_size, 8192, "1024 → 2048 → 4096 → 8192"); + assert_eq!(grown.slots, 4); + assert!(grown.fits(5000)); + assert_eq!(grown.offset_of(3) % 128, 0); + + // An AU already fitting changes nothing. + assert_eq!(layout.grown_for(512), layout); + + // The aligned RANGE drives growth, not the raw length: a 1025-byte AU has + // a 1152-byte range under a 128 alignment and needs the next size up. + assert_eq!(layout.grown_for(1025).slot_size, 2048); + } + + #[test] + fn one_byte_alignments_degenerate_cleanly() { + let layout = RingLayout::new(100, 2, 1, 1); + assert_eq!(layout.slot_size, 100); + assert_eq!(layout.record_range(37), 37); + assert!(layout.fits(100)); + assert!(!layout.fits(101)); + } + + #[test] + fn slots_recycle_in_fifo_order_only_after_their_token_completes() { + let mut states: SlotStates = SlotStates::new(2); + let s0 = states.acquire(|_| Ok::<_, ()>(true)).unwrap().unwrap(); + states.set_pending(s0, 10); + let s1 = states.acquire(|_| Ok::<_, ()>(true)).unwrap().unwrap(); + states.set_pending(s1, 11); + assert_ne!(s0, s1); + + // Ring full, oldest (slot 0, token 10) not done: acquire yields None and + // names the token to wait on. + assert_eq!(states.acquire(|&t| Ok::<_, ()>(t > 10)).unwrap(), None); + assert_eq!(states.oldest(), Some(&10)); + + // Once done, the OLDEST slot is the one handed back (FIFO, not LIFO). + let s2 = states.acquire(|_| Ok::<_, ()>(true)).unwrap().unwrap(); + assert_eq!(s2, s0); + + // Errors from the completion probe pass through untouched. + states.set_pending(s2, 12); + assert_eq!(states.acquire(|_| Err("gpu gone")).unwrap_err(), "gpu gone"); + } + + #[test] + fn slice_offsets_rebase_out_of_au_coordinates_into_the_packed_slot() { + // A realistic AU: AUD at 0, SPS/PPS, then two slice NALUs at 40 and 900, + // both with three-byte prefixes (so nothing is trimmed here). Only the + // slices are uploaded, so their PACKED offsets are 0 and (900 - 40) = + // 860's worth of the first slice's own length — never the AU ones. + let mut au = vec![0xAAu8; 1500]; + au[40..43].copy_from_slice(&[0, 0, 1]); + au[900..903].copy_from_slice(&[0, 0, 1]); + let segments = [40..900, 900..1500]; + let packed = pack_slices(&au, &segments).unwrap(); + assert_eq!(packed.offsets, vec![0, 860]); + assert_eq!(packed.segments, segments); + + // A single-slice AU always records offset 0, whatever the AU offset was. + // (Via `from_ref`: a one-element array literal of a range reads to clippy + // as a mis-typed range-fill, and the lint is right to say so.) + let single = 400usize..1000; + assert_eq!( + pack_slices(&au, std::slice::from_ref(&single)) + .unwrap() + .offsets, + vec![0] + ); + // No slices, no offsets (the callers reject empty plans before this). + assert_eq!(pack_slices(&au, &[]).unwrap().offsets, Vec::::new()); + + // Three segments accumulate by LENGTH, not by AU position (a gap between + // slice 1 and 2 — an SEI mid-AU — must not shift the third offset). + let segments = [0..100, 500..600, 1000..1100]; + assert_eq!( + pack_slices(&au, &segments).unwrap().offsets, + vec![0, 100, 200] + ); + } + + #[test] + fn a_four_byte_annex_b_prefix_is_trimmed_to_three_and_the_offsets_follow() { + // Two slices, the first with the four-byte prefix real encoders put on the + // first NALU of an access unit, the second with a three-byte one. Both must + // land on `00 00 01`, and — the part a separate `rebased_offsets` call got + // wrong by construction — the SECOND offset must count the FIRST slice's + // trimmed length, not its AU length. + let mut au = vec![0xAAu8; 200]; + au[0..4].copy_from_slice(&[0, 0, 0, 1]); + au[100..103].copy_from_slice(&[0, 0, 1]); + let packed = pack_slices(&au, &[0..100, 100..200]).unwrap(); + assert_eq!(packed.segments, vec![1..100, 100..200]); + assert_eq!(packed.offsets, vec![0, 99], "99, not 100"); + + let mut slot = vec![0xFFu8; 256]; + pack_into(&mut slot, &au, &packed.segments); + for (offset, segment) in packed.offsets.iter().zip(&packed.segments) { + let at = &slot[*offset as usize..]; + assert_eq!( + at[..3], + [0, 0, 1], + "the packed slice at offset {offset} must open with a THREE-byte \ + start code, or a driver reaching the slice header by a fixed \ + `+3 +2` skip lands a byte early" + ); + assert_eq!(&at[..segment.len()], &au[segment.clone()]); + } + // And the alignment tail is zeroed, never a previous AU's bytes. + let packed_len: usize = packed.segments.iter().map(|s| s.len()).sum(); + assert!(slot[packed_len..].iter().all(|&b| b == 0)); + + // Annex-B allows more than one leading zero byte; all of them go. + let mut au = vec![0xAAu8; 64]; + au[0..6].copy_from_slice(&[0, 0, 0, 0, 0, 1]); + assert_eq!( + pack_slices(&au, std::slice::from_ref(&(0usize..64))) + .unwrap() + .segments, + vec![3..64] + ); + + // A range that is not a start code at all (a hand-built plan) is left + // exactly as it came: the trim only ever drops a zero followed by two more. + let au = vec![0x42u8; 32]; + assert_eq!( + pack_slices(&au, std::slice::from_ref(&(0usize..32))) + .unwrap() + .segments, + vec![0..32] + ); + } + + /// The regression test for the M3 HEVC field failure: what the ring ACTUALLY + /// receives for every AU of the vendored vectors, checked against the streams' + /// own facts rather than against a golden the same code produced. + /// + /// It exists because nothing CPU-side checked the submitted bytes against the + /// offsets that describe them, and the consequence was invisible without a GPU: + /// 249 of this vector's 250 slice segments carry a four-byte Annex-B prefix, so + /// every offset pointed a `+3 +2`-skipping driver at the second byte of the NAL + /// unit header instead of the slice header, and NVIDIA answered with + /// `Invalid PPS/SPS id in slice header (pps_id=115)` on every AU. + /// + /// Both codecs run the same assertions on purpose. H.264's vector happens to + /// carry three-byte prefixes on all 500 of its slices — an encoder convention, + /// not a structural guarantee — which is exactly why it never tripped this and + /// why it CANNOT serve as the canary. Its leg here is the guard that the + /// normalisation stays a no-op where nothing needs normalising; the four-byte + /// case for both codecs is covered by + /// [`super::tests::a_four_byte_annex_b_prefix_is_trimmed_to_three_and_the_offsets_follow`], + /// which is codec-neutral for the same reason. + #[test] + fn every_packed_slice_of_both_vendored_vectors_opens_at_its_own_nal_header() { + // H.265: three-byte prefix, then the TWO-byte NAL unit header + // (forbidden_zero_bit 0, nal_unit_type < 32 for a VCL NALU, nuh_layer_id 0), + // then the slice segment header — whose first bit is + // first_slice_segment_in_pic_flag. + let mut planner = pf_bitstream::h265::H265Planner::new(); + let mut aus = 0usize; + for au in split_h265_aus(TEST_25FPS_H265) { + let plan = planner.plan_au(au).expect("the clean vector plans"); + let plan_segments: Vec> = + plan.slices.iter().map(|s| s.data.clone()).collect(); + let packed = pack_slices(au, &plan_segments).expect("offsets fit u32"); + assert_eq!(packed.offsets.len(), plan.slices.len(), "one per segment"); + let slot = packed_slot(au, &packed); + for (index, offset) in packed.offsets.iter().enumerate() { + let at = &slot[*offset as usize..]; + assert_eq!( + at[..3], + [0, 0, 1], + "AU {aus} segment {index}: a three-byte start code" + ); + assert_eq!(at[3] & 0x80, 0, "AU {aus} segment {index}: forbidden_zero"); + let nal_type = (at[3] >> 1) & 0x3F; + assert!( + nal_type < 32, + "AU {aus} segment {index}: VCL type, not {nal_type}" + ); + let layer_id = ((at[3] & 1) << 5) | (at[4] >> 3); + assert_eq!(layer_id, 0, "AU {aus} segment {index}: nuh_layer_id"); + assert!( + at[4] & 7 > 0, + "AU {aus} segment {index}: temporal_id_plus1 > 0" + ); + // The slice segment header begins at `+5`. Only the FIRST segment of + // a picture sets first_slice_segment_in_pic_flag. + assert_eq!( + at[5] & 0x80 != 0, + index == 0, + "AU {aus} segment {index}: first_slice_segment_in_pic_flag" + ); + } + aus += 1; + } + assert_eq!(aus, 250, "the vector's own golden"); + + // H.264: three-byte prefix, then the ONE-byte NAL unit header, then the + // slice header — whose first element is first_mb_in_slice `ue(v)`, so a + // leading 1 bit means "0", i.e. the first slice of the picture. + let mut planner = pf_bitstream::h264::H264Planner::new(); + let mut aus = 0usize; + for au in split_h264_aus(TEST_25FPS_H264) { + let plan = planner.plan_au(au).expect("the clean vector plans"); + let plan_segments: Vec> = + plan.slices.iter().map(|s| s.data.clone()).collect(); + let packed = pack_slices(au, &plan_segments).expect("offsets fit u32"); + let slot = packed_slot(au, &packed); + for (index, offset) in packed.offsets.iter().enumerate() { + let at = &slot[*offset as usize..]; + assert_eq!( + at[..3], + [0, 0, 1], + "AU {aus} segment {index}: a three-byte start code" + ); + assert_eq!(at[3] & 0x80, 0, "AU {aus} segment {index}: forbidden_zero"); + let nal_type = at[3] & 0x1F; + assert!( + nal_type == 1 || nal_type == 5, + "AU {aus} segment {index}: a coded slice, not type {nal_type}" + ); + assert_eq!( + at[4] & 0x80 != 0, + index == 0, + "AU {aus} segment {index}: first_mb_in_slice == 0" + ); + } + // The vector this crate's H.264 parity leg decodes is MULTI-slice; if it + // ever stopped being, its leg would stop covering the multi-segment + // offset arithmetic and this file's H.265 leg would be the only cover. + assert!(packed.offsets.len() >= 2, "AU {aus}: multi-slice"); + aus += 1; + } + assert_eq!(aus, 250, "the vector's own golden"); + } + + /// The vendored H.265 vector, at the path `pic_h265`'s tests use. + const TEST_25FPS_H265: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265" + ); + /// Its H.264 twin — the codec that shares this module and must not regress. + const TEST_25FPS_H264: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264" + ); + + /// One AU packed exactly as [`BitstreamRing::upload`] would pack it, into a + /// buffer as long as the recorded `srcBufferRange` under the widest alignment + /// any driver in the fleet reports. + fn packed_slot(au: &[u8], packed: &PackedSlices) -> Vec { + let len: u64 = packed.segments.iter().map(|s| s.len() as u64).sum(); + let layout = RingLayout::new(INITIAL_SLOT_SIZE, RING_SLOTS, 256, 256); + let mut slot = vec![0xFFu8; layout.record_range(len) as usize]; + pack_into(&mut slot, au, &packed.segments); + slot + } + + /// Test-only H.265 AU splitter — the same one `pic_h265`'s tests, the GPU legs' + /// `tests/common` and pf-bitstream's own tests each carry (it is `#[cfg(test)]` + /// private there): a new AU starts at a non-VCL NALU following slices, or at a + /// slice segment whose `first_slice_segment_in_pic_flag` (the top bit of the + /// byte after the TWO-byte NAL header) is set while the current AU already has + /// slices. + fn split_h265_aus(stream: &[u8]) -> Vec<&[u8]> { + use cros_codecs::codec::h265::parser::Nalu; + + let mut aus = Vec::new(); + let mut cursor = std::io::Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + while let Ok(nalu) = Nalu::next(&mut cursor) { + let header_start = cursor.position() as usize; + let start = header_start - nalu.offset; + let is_slice = (nalu.header.type_ as u32) < 32; + let first_slice_flag = + is_slice && stream.get(header_start + 2).is_some_and(|b| b & 0x80 != 0); + if au_has_slice && (!is_slice || first_slice_flag) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus + } + + /// [`split_h265_aus`]' H.264 twin: a ONE-byte NAL header, so `first_mb_in_slice` + /// is the top bit of the byte after it, and "is a slice" is the two-type enum. + fn split_h264_aus(stream: &[u8]) -> Vec<&[u8]> { + use cros_codecs::codec::h264::parser::Nalu; + use cros_codecs::codec::h264::parser::NaluType; + + let mut aus = Vec::new(); + let mut cursor = std::io::Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + while let Ok(nalu) = Nalu::next(&mut cursor) { + let nalu_offset = cursor.position() as usize; + let start = nalu_offset - nalu.offset; + let is_slice = matches!(nalu.header.type_, NaluType::Slice | NaluType::SliceIdr); + let first_mb_zero = + is_slice && stream.get(nalu_offset + 1).is_some_and(|b| b & 0x80 != 0); + if au_has_slice && (!is_slice || first_mb_zero) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus + } + + #[test] + fn clear_forgets_every_token_and_restarts_the_cursor() { + let mut states: SlotStates = SlotStates::new(3); + for token in 0..3 { + let s = states.acquire(|_| Ok::<_, ()>(true)).unwrap().unwrap(); + states.set_pending(s, token); + } + assert_eq!(states.in_flight().count(), 3); + states.clear(); + assert_eq!(states.in_flight().count(), 0); + assert_eq!(states.acquire(|_| Ok::<_, ()>(true)).unwrap(), Some(0)); + } +} diff --git a/crates/pf-vkdecode/src/session.rs b/crates/pf-vkdecode/src/session.rs new file mode 100644 index 00000000..773222ad --- /dev/null +++ b/crates/pf-vkdecode/src/session.rs @@ -0,0 +1,1051 @@ +//! `VkVideoSessionKHR` + `VkVideoSessionParametersKHR` lifecycle. +//! +//! The session is created from the STREAM's facts (the SPS's coded extent and DPB +//! depth), its memory requirements bound exactly like the encoder does, and its +//! parameters object holds WP-A's converted `StdVideoH264*ParameterSet`s. Parameter +//! versioning follows Vulkan's rules precisely: +//! +//! - a NEW (sps-id / pps-id) is ADDED via `vkUpdateVideoSessionParametersKHR` with +//! `updateSequenceCount` = previous + 1 (the spec's exact-increment rule); +//! - an EXISTING id whose content changed cannot be updated in place — the object +//! is RECREATED (Vulkan forbids replacing a stored parameter set), as is an +//! object whose capacity would overflow; +//! - a stream renegotiation that resizes the DPB or the coded extent recreates the +//! whole session — `plan_to_vk`'s `CapacityMismatch` is the trigger the decoder +//! sees for the DPB half, the extent comparison covers the other. +//! +//! ⚠⚠⚠ **The Std sets' heap blocks must outlive the parameters OBJECT, not just the +//! call that hands them over.** Vulkan reads as though parameter data were captured +//! by `vkCreateVideoSessionParametersKHR`, and all three codecs in this crate +//! assumed it. NVIDIA 610.57.04 does not: for AV1 it was measured keeping +//! `StdVideoAV1SequenceHeader::pColorConfig` and dereferencing it when a decode is +//! RECORDED, which decoded every frame against recycled heap ([`crate::session_av1`] +//! carries the measurement). H.264's Std sets embed the same kind of pointer — +//! `pOffsetForRefFrame` and `pScalingLists` on the SPS, `pScalingLists` on the PPS — +//! so [`StoredParams`] holds the object and its backings in ONE value with one +//! lifetime, and both the recreate path and `Drop` destroy the object before that +//! value is released. +//! +//! **Both LEVELS of pointer are covered, not just the one the measurement caught.** +//! Fixing the inner pointers left the OUTER ones — `pStdSPSs`/`pStdPPSs`, and AV1's +//! `pStdSequenceHeader` — still addressing function locals, and a driver retaining +//! those instead would reproduce the same bug with the same silent signature. So the +//! Std structs are boxed inside their wrappers ([`crate::OwnedStdSps`]) and the +//! contiguous arrays are FIELDS of `StoredParams`: no address the driver is given +//! is a temporary's. The line is drawn at Std DATA — `VkVideoSessionParametersCreateInfoKHR` +//! and its `pNext`/`pParametersAddInfo` plumbing stay function-local, because those +//! are ordinary create-info structures every `vkCreate*` in Vulkan reads during the +//! call; it is the `pStd*` members whose retention the spec's wording left ambiguous +//! and this fleet was measured exercising. +//! +//! [`ParamsLedger`] is the pure half of that decision table (unit-tested); +//! [`VideoSession`] is the thin Vulkan half. + +use std::rc::Rc; + +use ash::vk; +use ash::vk::native as hh; +use cros_codecs::codec::h264::parser::Pps; +use cros_codecs::codec::h264::parser::Sps; +use tracing::debug; + +use crate::caps::DecodeCaps; +use crate::caps::H264ProfileChain; +use crate::device::find_memory_type_preferring; +use crate::device::AllocError; +use crate::device::DecodeDevice; +use crate::params::pps_to_std; +use crate::params::sps_to_std; +use crate::params::OwnedStdPps; +use crate::params::OwnedStdSps; +use crate::params::ParamsError; +use crate::params_av1::ParamsAv1Error; +use crate::params_h265::H265ParamsError; + +/// Parameter-object capacity. Punktfunk hosts emit one SPS + one PPS per stream; +/// the headroom absorbs id churn across renegotiations without recreation, and an +/// overflow beyond it recreates rather than fails. +pub(crate) const MAX_STD_SPS: usize = 4; +pub(crate) const MAX_STD_PPS: usize = 8; + +/// What the ledger decided for one (SPS, PPS) activation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParamsAction { + /// Both sets are already stored with identical content — nothing to do. + Current, + /// At least one set is new; one update call (seq += 1) adds what is missing. + Add { add_sps: bool, add_pps: bool }, + /// A stored id changed content, or capacity would overflow: recreate the + /// parameters object (Vulkan cannot replace or evict a stored set). + Recreate, +} + +/// Pure bookkeeping for the parameters object: which sets it holds (by id AND +/// content — the parser re-parses in-band parameter sets every keyframe, so +/// pointer identity means nothing) and the update sequence counter. +#[derive(Debug, Default)] +pub(crate) struct ParamsLedger { + sps: Vec<(u8, Rc)>, + pps: Vec<((u8, u8), Rc)>, + update_seq: u32, +} + +impl ParamsLedger { + /// Decide the action for activating (`sps`, `pps`). Pure — mutate via + /// [`Self::commit`]. + pub(crate) fn plan(&self, sps: &Rc, pps: &Rc) -> ParamsAction { + let sps_key = sps.seq_parameter_set_id; + let pps_key = (pps.seq_parameter_set_id, pps.pic_parameter_set_id); + + let stored_sps = self.sps.iter().find(|(id, _)| *id == sps_key); + let stored_pps = self.pps.iter().find(|(id, _)| *id == pps_key); + if let Some((_, stored)) = stored_sps { + if **stored != **sps { + return ParamsAction::Recreate; + } + } + if let Some((_, stored)) = stored_pps { + if **stored != **pps { + return ParamsAction::Recreate; + } + } + let add_sps = stored_sps.is_none(); + let add_pps = stored_pps.is_none(); + if !add_sps && !add_pps { + return ParamsAction::Current; + } + if (add_sps && self.sps.len() >= MAX_STD_SPS) || (add_pps && self.pps.len() >= MAX_STD_PPS) + { + return ParamsAction::Recreate; + } + ParamsAction::Add { add_sps, add_pps } + } + + /// Apply a decided action. `Add` bumps the sequence count by EXACTLY one (the + /// Vulkan update rule — one call may carry both sets); `Recreate` resets the + /// ledger to just the current pair with a fresh object's zero counter (any + /// other id the stream still references simply re-Adds on next activation). + pub(crate) fn commit(&mut self, action: ParamsAction, sps: &Rc, pps: &Rc) { + match action { + ParamsAction::Current => {} + ParamsAction::Add { add_sps, add_pps } => { + if add_sps { + self.sps.push((sps.seq_parameter_set_id, Rc::clone(sps))); + } + if add_pps { + self.pps.push(( + (pps.seq_parameter_set_id, pps.pic_parameter_set_id), + Rc::clone(pps), + )); + } + self.update_seq += 1; + } + ParamsAction::Recreate => { + self.sps.clear(); + self.pps.clear(); + self.sps.push((sps.seq_parameter_set_id, Rc::clone(sps))); + self.pps.push(( + (pps.seq_parameter_set_id, pps.pic_parameter_set_id), + Rc::clone(pps), + )); + self.update_seq = 0; + } + } + } + + /// The sequence count the NEXT `vkUpdateVideoSessionParametersKHR` must carry. + pub(crate) fn next_update_seq(&self) -> u32 { + self.update_seq + 1 + } +} + +/// The session's create-time shape; a plan disagreeing with it forces a rebuild. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SessionConfig { + pub max_coded_extent: vk::Extent2D, + pub max_dpb_slots: u32, + pub max_active_references: u32, + /// The Std profile the session was created against (a profile change is a + /// renegotiation too). + pub std_profile_idc: hh::StdVideoH264ProfileIdc, +} + +/// Session creation/parameter failures the decoder maps into its error type. +#[derive(Debug)] +pub(crate) enum SessionError { + Vk(vk::Result), + Params(ParamsError), + /// An H.265 parameter set has no Std representation (the H.265 session's + /// counterpart of [`SessionError::Params`]). + ParamsH265(H265ParamsError), + /// An AV1 sequence header has no Std representation (the AV1 session's + /// counterpart of [`SessionError::Params`]). + ParamsAv1(ParamsAv1Error), + /// Session memory binding found no matching memory type (never a fallback). + NoMemoryType { + type_bits: u32, + flags: vk::MemoryPropertyFlags, + }, +} + +impl From for SessionError { + fn from(r: vk::Result) -> Self { + SessionError::Vk(r) + } +} + +impl From for SessionError { + fn from(e: ParamsError) -> Self { + SessionError::Params(e) + } +} + +impl From for SessionError { + fn from(e: H265ParamsError) -> Self { + SessionError::ParamsH265(e) + } +} + +impl From for SessionError { + fn from(e: ParamsAv1Error) -> Self { + SessionError::ParamsAv1(e) + } +} + +impl From for SessionError { + fn from(e: AllocError) -> Self { + match e { + AllocError::Vk(r) => SessionError::Vk(r), + AllocError::NoMemoryType { type_bits, flags } => { + SessionError::NoMemoryType { type_bits, flags } + } + } + } +} + +/// A [`bind_session_memory`] failure and whatever allocations the CALLER must now +/// take over. +/// +/// The distinction is a lifetime rule, not bookkeeping taste. Vulkan defines no +/// partial-bind rollback: once `vkBindVideoSessionMemoryKHR` has been called, some +/// bind indices may have taken, and memory bound into a live session may NOT be +/// freed while that session exists. So: +/// +/// - an ALLOCATE failure happens before any bind — nothing is attached to the +/// session, the function frees everything itself, and `allocations` is empty; +/// - a BIND failure hands the allocations back UNFREED, because the caller's +/// session object must be destroyed FIRST. The caller parks them where its own +/// `Drop` frees them after the destroy (that is exactly [`VideoSession`]'s and +/// [`crate::session_h265::VideoSessionH265`]'s field order). +pub(crate) struct BindFailure { + /// Allocations that may be bound into the session — free them only AFTER the + /// session is destroyed. Empty when the failure preceded any bind. + pub(crate) allocations: Vec, + pub(crate) error: SessionError, +} + +/// Query and bind one video session's memory requirements (the encoder's exact +/// shape), returning the allocations the session now owns. Codec-agnostic — +/// `VkVideoSessionKHR` memory binding says nothing about H.264 vs H.265 — so both +/// session types call this, and the NVIDIA placement rationale below lives once. +/// +/// Failure hands back a [`BindFailure`] whose `allocations` the caller must adopt +/// (see its docs for the destroy-before-free rule); an allocate-stage failure +/// frees eagerly and hands back none. +/// +/// # Safety +/// +/// `dev` wraps live handles ([`crate::DeviceHandles`] contract) and `session` is a +/// live, not-yet-memory-bound session created on it. +pub(crate) unsafe fn bind_session_memory( + dev: &DecodeDevice, + session: vk::VideoSessionKHR, +) -> Result, BindFailure> { + let device = dev.ash(); + let get = dev + .video_queue() + .fp() + .get_video_session_memory_requirements_khr; + let mut count = 0u32; + // SAFETY: live device + session (fn contract); null pointer is the + // count-query form. + let _ = unsafe { get(device.handle(), session, &mut count, std::ptr::null_mut()) }; + let mut reqs = vec![vk::VideoSessionMemoryRequirementsKHR::default(); count as usize]; + // SAFETY: as above with an array of the reported count. + let _ = unsafe { get(device.handle(), session, &mut count, reqs.as_mut_ptr()) }; + + let props = dev.memory_properties(); + let mut allocated: Vec = Vec::with_capacity(reqs.len()); + let mut binds = Vec::with_capacity(reqs.len()); + // Free everything allocated so far — for the ALLOCATE-stage exits only, which + // are reached before `vkBindVideoSessionMemoryKHR` is ever called. The + // BIND-stage exit must NOT come through here (BindFailure docs). + let unwind = |device: &ash::Device, allocated: &[vk::DeviceMemory]| { + for &memory in allocated { + // SAFETY: allocations made in this function on this live device, none + // of which the bind call has been reached for — so none can be bound + // into any session, and freeing them here cannot outlive-order a + // session destroy. + unsafe { device.free_memory(memory, None) }; + } + }; + for rq in &reqs { + let mr = rq.memory_requirements; + // DEVICE_LOCAL preferred, any type from `memoryTypeBits` accepted: + // NVIDIA (610.88) constrains some session bindings to host-visible-only + // types, and the driver knows where its own session state belongs. NEVER + // a hard DEVICE_LOCAL requirement — that shape is unsatisfiable there. + let type_index = match find_memory_type_preferring( + &props, + mr.memory_type_bits, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + ) { + Ok(index) => index, + Err(e) => { + unwind(device, &allocated); + return Err(BindFailure { + allocations: Vec::new(), + error: e.into(), + }); + } + }; + let alloc = vk::MemoryAllocateInfo::default() + .allocation_size(mr.size) + .memory_type_index(type_index); + // SAFETY: live device (fn contract). + let memory = match unsafe { device.allocate_memory(&alloc, None) } { + Ok(memory) => memory, + Err(e) => { + unwind(device, &allocated); + return Err(BindFailure { + allocations: Vec::new(), + error: SessionError::Vk(e), + }); + } + }; + allocated.push(memory); + binds.push( + vk::BindVideoSessionMemoryInfoKHR::default() + .memory_bind_index(rq.memory_bind_index) + .memory(memory) + .memory_offset(0) + .memory_size(mr.size), + ); + } + // SAFETY: session + freshly allocated memory, one bind per requirement. + let r = unsafe { + (dev.video_queue().fp().bind_video_session_memory_khr)( + device.handle(), + session, + binds.len() as u32, + binds.as_ptr(), + ) + }; + if r != vk::Result::SUCCESS { + // NOT freed here: a partial bind may have attached some of these to + // `session`, and Vulkan has no rollback for that. They go back to the + // caller, whose session object destroys BEFORE freeing them. + return Err(BindFailure { + allocations: allocated, + error: SessionError::Vk(r), + }); + } + Ok(allocated) +} + +/// A live parameters object **and every Std parameter set it was given**, in one +/// field — because the two may not drift apart. +/// +/// The wrapper is not decoration and not defensive: a driver in this fleet keeps +/// the embedded pointers out of a Std set and dereferences them long after the call +/// that handed them over returned (module docs), so releasing the backing early +/// hands it freed memory. One value rather than two fields makes "an object whose +/// backing is gone" unrepresentable, which is the only shape of this bug — and the +/// shape a `let owned = …;` local silently had. +/// +/// What is pinned, precisely: the wrappers' BOXED blocks, which is what the driver +/// was measured retaining. The contiguous array of outer `StdVideoH264*` structs +/// each call receives is a short-lived temporary, and the driver copies THAT before +/// returning — which is what the AV1 fix itself rests on, its Std header being moved +/// into storage after the create call on a rung that is now 250/250 bit-exact. So +/// moving these wrappers, or reallocating the `Vec`s holding them, disturbs nothing +/// the driver kept; `params::moving_the_wrapper_leaves_the_driver_s_pointers_put` +/// pins the half that matters. +struct StoredParams { + object: vk::VideoSessionParametersKHR, + /// One entry per set the OBJECT stores, held for the object's whole life. + /// Never read by this crate after the create/update call; the DRIVER reads the + /// blocks they own. + sps: Vec, + pps: Vec, + /// The contiguous Std ARRAYS the create call was handed as `pStdSPSs`/`pStdPPSs` + /// — the OUTER pointers, held for the object's life for the reason the wrappers + /// are. They were function-local `Vec`s, dropped the moment + /// [`VideoSession::create_parameters_object`] returned; nothing but the spec's + /// wording said a driver may not keep them, and that wording is what the AV1 + /// measurement already disproved for the pointers one level in. Built by + /// [`Self::assemble`] at their final address, so the pointer the driver is given + /// never moves at all. + std_sps: Vec, + std_pps: Vec, +} + +impl StoredParams { + /// The wrappers plus the contiguous Std arrays the create call reads its + /// `pStdSPSs`/`pStdPPSs` out of, with a NULL object the caller fills in once + /// `vkCreateVideoSessionParametersKHR` has succeeded. + /// + /// Assembling BEFORE the call is the point: the arrays are copies of the + /// wrappers' Std structs, and building them here puts them at the address they + /// will keep for the object's whole life rather than in a temporary the call + /// outlives. + fn assemble(sps: Vec, pps: Vec) -> Self { + // COPIES of each wrapper's Std struct (it is `Copy`); the embedded pointers + // they carry still address the wrappers' own boxed blocks, which is why + // both halves have to be kept. + let std_sps = sps.iter().map(|o| *o.std()).collect(); + let std_pps = pps.iter().map(|o| *o.std()).collect(); + Self { + object: vk::VideoSessionParametersKHR::null(), + sps, + pps, + std_sps, + std_pps, + } + } + + /// The placeholder a half-built session holds. `vkDestroyVideoSessionParametersKHR` + /// ignores a NULL handle, so a [`VideoSession::create`] that fails before the + /// object exists still drops cleanly. + fn none() -> Self { + Self::assemble(Vec::new(), Vec::new()) + } + + /// Take over sets an `Add` just handed to the live object — they belong to the + /// OBJECT now, so their blocks live as long as it does rather than as long as + /// the update call. Only ever reached after that call SUCCEEDED: a failed + /// update stored nothing, and its wrappers are dropped instead. + fn adopt(&mut self, sps: Option, pps: Option) { + self.sps.extend(sps); + self.pps.extend(pps); + } +} + +/// The Vulkan half: session + bound memory + parameters object. +pub(crate) struct VideoSession { + device: ash::Device, + video_queue: ash::khr::video_queue::Device, + session: vk::VideoSessionKHR, + memory: Vec, + parameters: StoredParams, + ledger: ParamsLedger, + pub(crate) config: SessionConfig, + /// The session has never run a coding scope: the first one records a + /// `VK_VIDEO_CODING_CONTROL_RESET_BIT_KHR` control before anything else (the + /// spec's initialization requirement; same shape as the encoder's first-frame + /// RESET install). + needs_reset: ResetArm, +} + +impl VideoSession { + /// Create the session + an EMPTY parameters object (sets arrive via + /// [`Self::ensure_parameters`], which the decoder calls before the first + /// decode). + /// + /// # Safety + /// + /// `dev` wraps live handles ([`crate::DeviceHandles`] contract). + pub(crate) unsafe fn create( + dev: &DecodeDevice, + caps: &DecodeCaps, + config: SessionConfig, + ) -> Result { + let mut chain = H264ProfileChain::new(config.std_profile_idc); + let profile = chain.wire(); + let std_header_version = caps.std_header_version; + let session_ci = vk::VideoSessionCreateInfoKHR::default() + .queue_family_index(dev.decode_qf()) + .video_profile(profile) + .picture_format(caps.output_format) + .max_coded_extent(config.max_coded_extent) + .reference_picture_format(caps.dpb_format) + .max_dpb_slots(config.max_dpb_slots) + .max_active_reference_pictures(config.max_active_references) + .std_header_version(&std_header_version); + let mut session = vk::VideoSessionKHR::null(); + // SAFETY: live device; `session_ci` roots locals (chain, header version) + // that outlive the call. + let r = unsafe { + (dev.video_queue().fp().create_video_session_khr)( + dev.ash().handle(), + &session_ci, + std::ptr::null(), + &mut session, + ) + }; + if r != vk::Result::SUCCESS { + return Err(SessionError::Vk(r)); + } + + let mut built = Self { + device: dev.ash().clone(), + video_queue: dev.video_queue().clone(), + session, + memory: Vec::new(), + parameters: StoredParams::none(), + ledger: ParamsLedger::default(), + config, + needs_reset: ResetArm::armed(), + }; + // SAFETY: fn contract; on error `built` drops and unwinds the session + + // whatever memory was bound. + unsafe { + // A bind failure hands its allocations BACK: parking them in `built` + // is what makes the early return destroy the session before freeing + // them (BindFailure docs — Vulkan defines no partial-bind rollback). + match bind_session_memory(dev, session) { + Ok(memory) => built.memory = memory, + Err(failure) => { + built.memory = failure.allocations; + return Err(failure.error); + } + } + built.parameters = built.create_parameters_object(Vec::new(), Vec::new())?; + } + Ok(built) + } + + /// Create a parameters object holding exactly `sps`/`pps` (either may be + /// empty), **fused with the wrappers whose heap blocks it points at**. + /// + /// Taking the wrappers BY VALUE rather than as Std slices is the point: there is + /// no way to reach `vkCreateVideoSessionParametersKHR` from here without the + /// resulting object taking ownership of everything it will go on dereferencing + /// (module docs, [`StoredParams`]). + /// + /// # Safety + /// + /// Live device + live session. + unsafe fn create_parameters_object( + &self, + sps: Vec, + pps: Vec, + ) -> Result { + // Assembled FIRST so the arrays `pStdSPSs`/`pStdPPSs` will point at are + // already where they will stay: `stored` is returned by value, and moving a + // `Vec` moves its handle, not the block the driver was given. + let mut stored = StoredParams::assemble(sps, pps); + let add = vk::VideoDecodeH264SessionParametersAddInfoKHR::default() + .std_sp_ss(&stored.std_sps) + .std_pp_ss(&stored.std_pps); + let mut h264 = vk::VideoDecodeH264SessionParametersCreateInfoKHR::default() + .max_std_sps_count(MAX_STD_SPS as u32) + .max_std_pps_count(MAX_STD_PPS as u32) + .parameters_add_info(&add); + let ci = vk::VideoSessionParametersCreateInfoKHR::default() + .video_session(self.session) + .push_next(&mut h264); + let mut object = vk::VideoSessionParametersKHR::null(); + // SAFETY: fn contract; `ci` roots locals outliving the call, and everything + // the driver may retain past it — the Std arrays AND the blocks their + // embedded pointers address — is owned by `stored`, which is returned + // rather than dropped here. + let r = unsafe { + (self.video_queue.fp().create_video_session_parameters_khr)( + self.device.handle(), + &ci, + std::ptr::null(), + &mut object, + ) + }; + if r != vk::Result::SUCCESS { + return Err(SessionError::Vk(r)); + } + stored.object = object; + Ok(stored) + } + + /// The ledger's verdict for activating (`sps`, `pps`), without mutating + /// anything — the decoder consults this BEFORE [`Self::ensure_parameters`] so + /// a [`ParamsAction::Recreate`] can be preceded by a full in-flight drain + /// (the destroy inside the recreate must never race a submitted decode). + pub(crate) fn parameters_action(&self, sps: &Rc, pps: &Rc) -> ParamsAction { + self.ledger.plan(sps, pps) + } + + /// Make the parameters object hold this AU's activated (SPS, PPS), converting + /// through WP-A and Adding/Recreating per the ledger's decision. + /// + /// # Safety + /// + /// Live device; when [`Self::parameters_action`] says `Recreate`, the caller + /// has ALREADY drained every in-flight decode (waited each output slot's + /// newest submitted timeline value) — the old object is destroyed here, and a + /// still-executing decode reading it would be use-after-free at the driver + /// level. The decoder enforces exactly that ordering in `decode_inner`; + /// `Current`/`Add` touch no object a submitted decode can be reading. + pub(crate) unsafe fn ensure_parameters( + &mut self, + sps: &Rc, + pps: &Rc, + ) -> Result<(), SessionError> { + let action = self.ledger.plan(sps, pps); + match action { + ParamsAction::Current => Ok(()), + ParamsAction::Add { add_sps, add_pps } => { + let owned_sps = if add_sps { + Some(sps_to_std(sps)?) + } else { + None + }; + let owned_pps = if add_pps { + Some(pps_to_std(pps)?) + } else { + None + }; + let sps_slice: &[hh::StdVideoH264SequenceParameterSet] = match &owned_sps { + Some(o) => std::slice::from_ref(o.std()), + None => &[], + }; + let pps_slice: &[hh::StdVideoH264PictureParameterSet] = match &owned_pps { + Some(o) => std::slice::from_ref(o.std()), + None => &[], + }; + let mut add = vk::VideoDecodeH264SessionParametersAddInfoKHR::default() + .std_sp_ss(sps_slice) + .std_pp_ss(pps_slice); + let update = vk::VideoSessionParametersUpdateInfoKHR::default() + .update_sequence_count(self.ledger.next_update_seq()) + .push_next(&mut add); + // SAFETY: live device + parameters object; `update` roots locals + // (incl. the OwnedStd backings) outliving the call — and the + // backings go on outliving it, adopted below. + let r = unsafe { + (self.video_queue.fp().update_video_session_parameters_khr)( + self.device.handle(), + self.parameters.object, + &update, + ) + }; + if r != vk::Result::SUCCESS { + return Err(SessionError::Vk(r)); + } + // ⚠ The added sets now belong to the OBJECT, so their heap blocks + // must too: an Add whose wrappers died at the end of this arm would + // be the AV1 use-after-free with an update call in front of it. + self.parameters.adopt(owned_sps, owned_pps); + self.ledger.commit(action, sps, pps); + Ok(()) + } + ParamsAction::Recreate => { + debug!( + sps_id = sps.seq_parameter_set_id, + pps_id = pps.pic_parameter_set_id, + "recreating session parameters (content change or capacity)" + ); + let owned_sps = sps_to_std(sps)?; + let owned_pps = pps_to_std(pps)?; + // SAFETY: fn contract — live device + live session. The wrappers + // are MOVED IN and come back owned by the fresh object, so they + // live as long as it does rather than merely across the call. + let fresh = + unsafe { self.create_parameters_object(vec![owned_sps], vec![owned_pps])? }; + // The old object goes FIRST and its backings with it — installing + // `fresh` through a local keeps the destroy ahead of the free, + // which is the order a driver still holding the old pointers needs. + let old = std::mem::replace(&mut self.parameters, fresh); + // SAFETY: the fn-level contract — the caller drained every + // in-flight decode before a Recreate reached here (checked via + // parameters_action), so no submitted work reads the old object; + // it is this session's own handle, on a live device. + unsafe { + (self.video_queue.fp().destroy_video_session_parameters_khr)( + self.device.handle(), + old.object, + std::ptr::null(), + ); + } + // Explicit, because the ORDER is the whole point: every Std block + // `old` owns is released only now, after the object that pointed at + // them is gone. + drop(old); + self.ledger.commit(action, sps, pps); + Ok(()) + } + } + } + + pub(crate) fn session(&self) -> vk::VideoSessionKHR { + self.session + } + + pub(crate) fn parameters(&self) -> vk::VideoSessionParametersKHR { + self.parameters.object + } + + /// Whether the next coding scope must record the initialization RESET — + /// `true` exactly once per session, PROVIDED the command buffer that recorded + /// it actually reaches the queue: a recording/submit failure after this + /// returned `true` must call [`Self::re_arm_reset`], or the session would run + /// its whole life uninitialized. + pub(crate) fn take_needs_reset(&mut self) -> bool { + self.needs_reset.take() + } + + /// Undo a consumed [`Self::take_needs_reset`] whose RESET never reached the + /// queue (end/submit failed after recording it). + pub(crate) fn re_arm_reset(&mut self) { + self.needs_reset.re_arm(); + } +} + +/// The one-shot session-RESET arm, its own type so the take/re-arm cycle is +/// testable without a live session object. +#[derive(Debug)] +pub(crate) struct ResetArm(bool); + +impl ResetArm { + pub(crate) fn armed() -> Self { + Self(true) + } + + pub(crate) fn take(&mut self) -> bool { + std::mem::take(&mut self.0) + } + + pub(crate) fn re_arm(&mut self) { + self.0 = true; + } +} + +impl Drop for VideoSession { + fn drop(&mut self) { + // SAFETY: all handles are this session's own on the (contract-live) device; + // the owning decoder drains GPU work before dropping state. The destroy + // entry points ignore NULL handles, covering half-built sessions. The + // ORDER is load-bearing, not stylistic: memory bound into a session may + // not be freed while the session lives, so the session is destroyed first + // — which is also why a failed bind hands its allocations back here + // instead of freeing them itself ([`BindFailure`]). The Std backings are + // freed after both, by the `parameters` field's own drop, which Rust runs + // AFTER this body — the same reason `ensure_parameters` destroys before it + // replaces. + unsafe { + (self.video_queue.fp().destroy_video_session_parameters_khr)( + self.device.handle(), + self.parameters.object, + std::ptr::null(), + ); + (self.video_queue.fp().destroy_video_session_khr)( + self.device.handle(), + self.session, + std::ptr::null(), + ); + for memory in self.memory.drain(..) { + self.device.free_memory(memory, None); + } + } + } +} + +#[cfg(test)] +mod tests { + use cros_codecs::codec::h264::parser::PpsBuilder; + use cros_codecs::codec::h264::parser::Profile; + use cros_codecs::codec::h264::parser::SpsBuilder; + use pf_bitstream::h264::Level; + + use super::*; + + fn authored(sps_id: u8, pps_id: u8, qp: u8) -> (Rc, Rc) { + let sps = SpsBuilder::new() + .seq_parameter_set_id(sps_id) + .profile_idc(Profile::Main) + .level_idc(Level::L4) + .frame_mbs_only_flag(true) + .direct_8x8_inference_flag(true) + .max_num_ref_frames(4) + .resolution(64, 64) + .build(); + let pps = PpsBuilder::new(Rc::clone(&sps)) + .pic_parameter_set_id(pps_id) + .pic_init_qp(qp) + .build(); + (sps, pps) + } + + /// An `Add` hands NEW Std sets to an EXISTING parameters object, so their heap + /// blocks must live as long as that OBJECT — not as long as the update call + /// that carried them. [`StoredParams::adopt`] is where the transfer happens, + /// and this pins that it genuinely takes ownership: `ensure_parameters` drops + /// its local wrappers the instant this returns, and a driver holding + /// `pScalingLists` would be reading freed heap from the next frame on + /// ([`crate::session_av1`] for the measurement that made this real). + #[test] + fn an_added_set_keeps_its_blocks_alive_past_the_update_call() { + let sps = SpsBuilder::new() + .seq_parameter_set_id(0) + .profile_idc(Profile::Main) + .level_idc(Level::L4) + .frame_mbs_only_flag(true) + .direct_8x8_inference_flag(true) + .max_num_ref_frames(4) + .resolution(64, 64) + // The one SPS pointer a builder can attach. + .seq_scaling_matrix_present_flag(true) + .build(); + let owned = sps_to_std(&sps).expect("converts"); + let lists = owned.std().pScalingLists; + assert!(!lists.is_null(), "the fixture attaches scaling lists"); + + let mut stored = StoredParams::none(); + stored.adopt(Some(owned), None); + assert_eq!( + (stored.sps.len(), stored.pps.len()), + (1, 0), + "the object took the set itself, not a borrow of it" + ); + assert_eq!( + stored.sps[0].std().pScalingLists, + lists, + "and it is the same block the driver was handed" + ); + // SAFETY: `stored` owns the block — which is exactly the property here. + let read_back = unsafe { (*lists).ScalingList4x4[0] }; + assert_eq!(read_back, [0; 16], "the fixture's lists, read back live"); + } + + /// …and it keeps the ADDRESS too, not merely the blocks. + /// + /// The Add path hands `vkUpdateVideoSessionParametersKHR` a + /// `std::slice::from_ref(o.std())` — a one-element array that IS the wrapper's + /// own Std struct — and then moves the wrapper into [`StoredParams`]. The test + /// above covers a driver retaining `pScalingLists` (an INNER pointer); this + /// covers one retaining `pStdSPSs`/`pStdPPSs`, which the same wording in the + /// spec permits just as much. Boxing the Std struct inside the wrapper is what + /// makes the two addresses equal; un-boxing it would leave every other test in + /// this crate green and hand the driver a moved-from stack slot. + #[test] + fn an_added_set_keeps_the_address_the_update_call_was_given() { + let (sps, pps) = authored(0, 0, 26); + let owned_sps = sps_to_std(&sps).expect("converts"); + let owned_pps = pps_to_std(&pps).expect("converts"); + // Exactly what `ensure_parameters` puts in `pStdSPSs`/`pStdPPSs`. + let handed_sps = std::ptr::from_ref(owned_sps.std()); + let handed_pps = std::ptr::from_ref(owned_pps.std()); + + let mut stored = StoredParams::none(); + stored.adopt(Some(owned_sps), Some(owned_pps)); + assert_eq!( + std::ptr::from_ref(stored.sps[0].std()), + handed_sps, + "the SPS address handed to Vulkan must be the one the object keeps" + ); + assert_eq!( + std::ptr::from_ref(stored.pps[0].std()), + handed_pps, + "and likewise the PPS" + ); + // SAFETY: `stored` owns both structs — which is exactly the property here. + let ids = unsafe { + ( + (*handed_sps).seq_parameter_set_id, + (*handed_pps).pic_parameter_set_id, + ) + }; + assert_eq!( + ids, + (0, 0), + "read back through the pointers the driver holds" + ); + } + + /// The create path's OUTER pointers: `pStdSPSs`/`pStdPPSs` address contiguous + /// COPIES of the wrappers' Std structs, and those arrays must outlive the + /// create call the same way the wrappers do. + /// + /// [`StoredParams::assemble`] builds them at their final address — inside the + /// value the parameters object is returned in — so the pointer the driver is + /// given never moves at all. Before this, they were function-local `Vec`s that + /// were dropped the instant `create_parameters_object` returned: a driver + /// retaining the array (rather than the embedded pointer this fleet was + /// measured retaining) would have been reading freed heap from the first frame. + #[test] + fn the_std_arrays_the_create_call_is_given_are_the_ones_the_object_keeps() { + let sps = SpsBuilder::new() + .seq_parameter_set_id(0) + .profile_idc(Profile::Main) + .level_idc(Level::L4) + .frame_mbs_only_flag(true) + .direct_8x8_inference_flag(true) + .max_num_ref_frames(4) + .resolution(64, 64) + // So the copied Std struct carries a NON-null embedded pointer and the + // "still addresses the wrapper's live block" assertion below can bite. + .seq_scaling_matrix_present_flag(true) + .build(); + let pps = PpsBuilder::new(Rc::clone(&sps)) + .pic_parameter_set_id(0) + .pic_init_qp(26) + .build(); + let owned_sps = sps_to_std(&sps).expect("converts"); + let owned_pps = pps_to_std(&pps).expect("converts"); + + let stored = StoredParams::assemble(vec![owned_sps], vec![owned_pps]); + // Exactly what `create_parameters_object` puts in `pStdSPSs`/`pStdPPSs`. + let (handed_sps, handed_pps) = (stored.std_sps.as_ptr(), stored.std_pps.as_ptr()); + // The move `create_parameters_object` ends with: `Ok(stored)`. + let stored = std::hint::black_box(stored); + assert_eq!((stored.std_sps.len(), stored.std_pps.len()), (1, 1)); + assert_eq!( + (stored.std_sps.as_ptr(), stored.std_pps.as_ptr()), + (handed_sps, handed_pps), + "the arrays handed to Vulkan must be the ones the object keeps" + ); + // And their COPIES still address the wrappers' own live blocks. + let lists = stored.std_sps[0].pScalingLists; + assert!(!lists.is_null(), "the fixture attaches scaling lists"); + assert_eq!(lists, stored.sps[0].std().pScalingLists); + // SAFETY: `stored` owns the block the copy points at — the property here. + let read_back = unsafe { (*lists).ScalingList4x4[0] }; + assert_eq!(read_back, [0; 16], "the fixture's lists, read back live"); + assert_eq!( + stored.std_pps[0].pic_parameter_set_id, + stored.pps[0].std().pic_parameter_set_id, + "the PPS copy is the wrapper's, field for field" + ); + } + + #[test] + fn a_reactivated_identical_pair_is_current_even_across_reparses() { + let (sps_a, pps_a) = authored(0, 0, 26); + // The parser re-parses in-band sets each keyframe: same content, NEW Rcs. + let (sps_b, pps_b) = authored(0, 0, 26); + assert!(!Rc::ptr_eq(&sps_a, &sps_b)); + + let mut ledger = ParamsLedger::default(); + let first = ledger.plan(&sps_a, &pps_a); + assert_eq!( + first, + ParamsAction::Add { + add_sps: true, + add_pps: true + } + ); + ledger.commit(first, &sps_a, &pps_a); + assert_eq!(ledger.plan(&sps_b, &pps_b), ParamsAction::Current); + } + + #[test] + fn a_new_pps_id_over_a_stored_sps_adds_only_the_pps() { + let (sps, pps0) = authored(0, 0, 26); + let pps1 = PpsBuilder::new(Rc::clone(&sps)) + .pic_parameter_set_id(1) + .pic_init_qp(26) + .build(); + + let mut ledger = ParamsLedger::default(); + let a = ledger.plan(&sps, &pps0); + ledger.commit(a, &sps, &pps0); + assert_eq!( + ledger.plan(&sps, &pps1), + ParamsAction::Add { + add_sps: false, + add_pps: true + } + ); + } + + #[test] + fn changed_content_under_a_stored_id_recreates_and_resets_the_sequence() { + let (sps, pps) = authored(0, 0, 26); + let mut ledger = ParamsLedger::default(); + let a = ledger.plan(&sps, &pps); + ledger.commit(a, &sps, &pps); + assert_eq!(ledger.next_update_seq(), 2, "one Add happened"); + + // Same ids, different content (qp changed): Vulkan cannot replace a + // stored set, so this must recreate. + let (sps2, pps2) = authored(0, 0, 30); + let action = ledger.plan(&sps2, &pps2); + assert_eq!(action, ParamsAction::Recreate); + ledger.commit(action, &sps2, &pps2); + assert_eq!( + ledger.next_update_seq(), + 1, + "a fresh object restarts its counter" + ); + // And the pair is now Current under the new content. + assert_eq!(ledger.plan(&sps2, &pps2), ParamsAction::Current); + } + + #[test] + fn capacity_overflow_recreates_with_just_the_current_pair() { + let mut ledger = ParamsLedger::default(); + // Fill the PPS capacity under one SPS. + let (sps, first) = authored(0, 0, 26); + let a = ledger.plan(&sps, &first); + ledger.commit(a, &sps, &first); + for pps_id in 1..MAX_STD_PPS as u8 { + let pps = PpsBuilder::new(Rc::clone(&sps)) + .pic_parameter_set_id(pps_id) + .pic_init_qp(26) + .build(); + let a = ledger.plan(&sps, &pps); + assert!(matches!(a, ParamsAction::Add { .. })); + ledger.commit(a, &sps, &pps); + } + assert_eq!(ledger.next_update_seq() - 1, MAX_STD_PPS as u32); + + // One past capacity: recreate; afterwards the evicted first PPS re-Adds. + let overflow = PpsBuilder::new(Rc::clone(&sps)) + .pic_parameter_set_id(MAX_STD_PPS as u8) + .pic_init_qp(26) + .build(); + let action = ledger.plan(&sps, &overflow); + assert_eq!(action, ParamsAction::Recreate); + ledger.commit(action, &sps, &overflow); + assert_eq!( + ledger.plan(&sps, &first), + ParamsAction::Add { + add_sps: false, + add_pps: true + }, + "sets evicted by a recreate re-add on next activation" + ); + } + + #[test] + fn the_reset_arm_fires_once_unless_the_failed_submit_re_arms_it() { + let mut arm = ResetArm::armed(); + assert!(arm.take(), "a fresh session needs its RESET"); + assert!( + !arm.take(), + "consumed — the next scope must NOT reset again" + ); + + // The recorded RESET never reached the queue (end/submit failed): the + // re-arm makes the next successful recording carry it instead. + arm.re_arm(); + assert!(arm.take()); + assert!(!arm.take()); + } + + #[test] + fn update_sequence_counts_one_per_add_call_not_per_set() { + let (sps, pps) = authored(0, 0, 26); + let mut ledger = ParamsLedger::default(); + assert_eq!(ledger.next_update_seq(), 1); + // One call carries BOTH sets: the counter moves by exactly one. + let a = ledger.plan(&sps, &pps); + assert_eq!( + a, + ParamsAction::Add { + add_sps: true, + add_pps: true + } + ); + ledger.commit(a, &sps, &pps); + assert_eq!(ledger.next_update_seq(), 2); + } +} diff --git a/crates/pf-vkdecode/src/session_av1.rs b/crates/pf-vkdecode/src/session_av1.rs new file mode 100644 index 00000000..b5144a7a --- /dev/null +++ b/crates/pf-vkdecode/src/session_av1.rs @@ -0,0 +1,502 @@ +//! `VkVideoSessionKHR` + `VkVideoSessionParametersKHR` lifecycle for AV1 — +//! [`crate::session_h265`] one codec over, and much the smaller of the two. +//! +//! AV1's parameter surface is ONE sequence header. +//! `VkVideoDecodeAV1SessionParametersCreateInfoKHR` carries a single +//! `pStdSequenceHeader` and there is no add-info structure at all — no PPS array, +//! no VPS array, and nothing `vkUpdateVideoSessionParametersKHR` can add. That +//! collapses the H.265 ledger's three-way decision table to two states, and BOTH +//! of them are forced by Vulkan rather than chosen here: +//! +//! - the stored header is byte-identical to the one this frame activates ⇒ +//! [`ParamsActionAv1::Current`], nothing to do; +//! - anything else — a first sequence header, or a content change under way — +//! ⇒ [`ParamsActionAv1::Recreate`]. Vulkan cannot REPLACE a stored parameter +//! set, and for AV1 it cannot ADD one either, so recreation is the only move. +//! +//! One consequence is worth stating because it differs from the other two codecs: +//! **the parameters object is not created with the session.** H.264 and H.265 +//! create an empty object up front and Add sets into it; an AV1 parameters object +//! has no empty form (`pStdSequenceHeader` must be a valid pointer), so +//! [`VideoSessionAv1::create`] leaves the handle NULL and the first +//! [`VideoSessionAv1::ensure_parameters`] creates it. A decode recorded before +//! that would bind a NULL parameters object, which is why the decoder calls +//! `ensure_parameters` before every submission and nothing else may create the +//! session's coding scope. +//! +//! ⚠⚠⚠ **The Std sequence header's heap blocks must outlive the parameters +//! OBJECT, not just the create call.** Vulkan reads as though parameter data were +//! captured by `vkCreateVideoSessionParametersKHR`, and this module assumed it — +//! [`sequence_to_std`]'s wrapper was a local, dropped the moment the call +//! returned. NVIDIA 610.57.04 keeps the pointer instead and dereferences +//! `pColorConfig` when a decode is RECORDED, so every AV1 frame was decoded +//! against whatever the allocator had since put in those 24 bytes. Measured on an +//! RTX 5070 Ti: correct at create, `23 00 00 00 00 00 00 00 77 29 …` by the first +//! `vkCmdDecodeVideoKHR` — which reads as `mono_chrome = 1`, so the driver +//! deblocked the frame as monochrome and skipped `loop_filter_level[2..3]` +//! entirely. That is the whole of the AV1 rung's parity gap (250/250 frames +//! divergent; 0/250 with the backing held, [`StoredParamsAv1`]). +//! +//! ⚠⚠ That fix stabilised the INNER pointers only. `pStdSequenceHeader` — the +//! address `VkVideoDecodeAV1SessionParametersCreateInfoKHR` itself carries — was +//! still [`sequence_to_std`]'s stack local, dead the moment `ensure_parameters` +//! returned, and a driver retaining IT rather than `pColorConfig` would reproduce +//! the bug exactly. The Std struct is now boxed inside the wrapper, so the address +//! handed over is the one `StoredParamsAv1` keeps ([`crate::session`]'s module +//! docs carry the argument and the line it draws). +//! +//! `ParamsLedgerAv1` is the pure half of the decision (unit-tested); +//! [`VideoSessionAv1`] is the thin Vulkan half. + +use std::rc::Rc; + +use ash::vk; +use cros_codecs::codec::av1::parser::SequenceHeaderObu; +use tracing::debug; + +use crate::caps::DecodeCaps; +use crate::caps_av1::Av1ProfileChain; +use crate::caps_av1::Av1ProfileKey; +use crate::device::DecodeDevice; +use crate::params_av1::sequence_to_std; +use crate::params_av1::OwnedStdAv1SequenceHeader; +use crate::session::bind_session_memory; +use crate::session::ResetArm; +use crate::session::SessionError; + +/// What the ledger decided for one sequence-header activation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParamsActionAv1 { + /// The identical sequence header is already stored — nothing to do. + Current, + /// No object exists yet, or the stored header's content changed: create a + /// fresh parameters object. There is deliberately no `Add` — AV1 session + /// parameters hold exactly one sequence header and Vulkan offers no update + /// path for it (module docs). + Recreate, +} + +/// Pure bookkeeping for the parameters object: which sequence header it holds, by +/// CONTENT. +/// +/// By content rather than by pointer for the reason the other two ledgers give: +/// the parser re-parses the in-band sequence header at every keyframe, so a +/// perfectly unchanged stream hands out a fresh `Rc` several times a second, and +/// keying on identity would recreate the parameters object — and with it stall the +/// pipeline for a drain — at every one of them. +#[derive(Debug, Default)] +pub(crate) struct ParamsLedgerAv1 { + sequence: Option>, +} + +impl ParamsLedgerAv1 { + /// Decide the action for activating `sequence`. Pure — mutate via + /// [`Self::commit`]. + pub(crate) fn plan(&self, sequence: &Rc) -> ParamsActionAv1 { + match &self.sequence { + Some(stored) if **stored == **sequence => ParamsActionAv1::Current, + _ => ParamsActionAv1::Recreate, + } + } + + /// Apply a decided action. + pub(crate) fn commit(&mut self, action: ParamsActionAv1, sequence: &Rc) { + match action { + ParamsActionAv1::Current => {} + ParamsActionAv1::Recreate => self.sequence = Some(Rc::clone(sequence)), + } + } +} + +/// The session's create-time shape; a plan disagreeing with it forces a rebuild. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SessionConfigAv1 { + pub max_coded_extent: vk::Extent2D, + pub max_dpb_slots: u32, + pub max_active_references: u32, + /// The profile the session was created against — Std profile, sampling, bit + /// depth AND the film-grain flag, every one of which a stream can renegotiate + /// (a sequence header switching 8-bit → 10-bit, or turning film grain on, is a + /// session rebuild, not a parameters update). + pub profile: Av1ProfileKey, +} + +/// A live parameters object **and the Std sequence header it was created from**, +/// in one field — because the two may not drift apart. +/// +/// The wrapper is not decoration and not defensive: the driver dereferences the +/// header's `pColorConfig` long after the create call returned (module docs), so +/// dropping the backing early hands it freed memory. One field rather than two +/// makes "an object whose backing is gone" unrepresentable, which is the only +/// shape of this bug — and the shape a `let owned = …;` local silently had. +struct StoredParamsAv1 { + object: vk::VideoSessionParametersKHR, + /// Held for the OBJECT's whole life. Never read by this crate after the + /// create call; the DRIVER reads it — potentially through `pStdSequenceHeader` + /// itself, which is why the wrapper boxes its Std struct rather than holding it + /// inline (module docs). + _sequence: OwnedStdAv1SequenceHeader, +} + +/// The Vulkan half: session + bound memory + parameters object. +pub(crate) struct VideoSessionAv1 { + device: ash::Device, + video_queue: ash::khr::video_queue::Device, + session: vk::VideoSessionKHR, + memory: Vec, + /// `None` until the first [`Self::ensure_parameters`] — an AV1 parameters + /// object has no empty form (module docs). + parameters: Option, + ledger: ParamsLedgerAv1, + pub(crate) config: SessionConfigAv1, + /// The session has never run a coding scope: the first one records a + /// `VK_VIDEO_CODING_CONTROL_RESET_BIT_KHR` control before anything else. + needs_reset: ResetArm, +} + +impl VideoSessionAv1 { + /// Create the session. The parameters object follows at the first + /// [`Self::ensure_parameters`], which the decoder calls before every decode. + /// + /// # Safety + /// + /// `dev` wraps live handles ([`crate::DeviceHandles`] contract). + pub(crate) unsafe fn create( + dev: &DecodeDevice, + caps: &DecodeCaps, + config: SessionConfigAv1, + ) -> Result { + let mut chain = Av1ProfileChain::new(config.profile); + let profile = chain.wire(); + let std_header_version = caps.std_header_version; + let session_ci = vk::VideoSessionCreateInfoKHR::default() + .queue_family_index(dev.decode_qf()) + .video_profile(profile) + .picture_format(caps.output_format) + .max_coded_extent(config.max_coded_extent) + .reference_picture_format(caps.dpb_format) + .max_dpb_slots(config.max_dpb_slots) + .max_active_reference_pictures(config.max_active_references) + .std_header_version(&std_header_version); + let mut session = vk::VideoSessionKHR::null(); + // SAFETY: live device; `session_ci` roots locals (chain, header version) + // that outlive the call. + let r = unsafe { + (dev.video_queue().fp().create_video_session_khr)( + dev.ash().handle(), + &session_ci, + std::ptr::null(), + &mut session, + ) + }; + if r != vk::Result::SUCCESS { + return Err(SessionError::Vk(r)); + } + + let mut built = Self { + device: dev.ash().clone(), + video_queue: dev.video_queue().clone(), + session, + memory: Vec::new(), + parameters: None, + ledger: ParamsLedgerAv1::default(), + config, + needs_reset: ResetArm::armed(), + }; + // SAFETY: fn contract; on error `built` drops and unwinds the session + + // whatever memory was bound. + unsafe { + // A bind failure hands its allocations BACK: parking them in `built` + // is what makes the early return destroy the session before freeing + // them (BindFailure docs — Vulkan defines no partial-bind rollback). + match bind_session_memory(dev, session) { + Ok(memory) => built.memory = memory, + Err(failure) => { + built.memory = failure.allocations; + return Err(failure.error); + } + } + } + Ok(built) + } + + /// The ledger's verdict for activating `sequence`, without mutating anything — + /// the decoder consults this BEFORE [`Self::ensure_parameters`] so a + /// [`ParamsActionAv1::Recreate`] over an EXISTING object can be preceded by a + /// full in-flight drain (the destroy inside the recreate must never race a + /// submitted decode). + pub(crate) fn parameters_action(&self, sequence: &Rc) -> ParamsActionAv1 { + self.ledger.plan(sequence) + } + + /// Whether a parameters object exists at all. The decoder pairs this with + /// [`Self::parameters_action`]: the FIRST `Recreate` of a session's life + /// destroys nothing and needs no drain, every later one does. + pub(crate) fn has_parameters(&self) -> bool { + self.parameters.is_some() + } + + /// Make the parameters object hold this frame's active sequence header. + /// + /// # Safety + /// + /// Live device; when [`Self::parameters_action`] says `Recreate` AND + /// [`Self::has_parameters`] is true, the caller has ALREADY drained every + /// in-flight decode — the old object is destroyed here, and a still-executing + /// decode reading it would be use-after-free at the driver level. + /// `Current` touches no object a submitted decode can be reading. + pub(crate) unsafe fn ensure_parameters( + &mut self, + sequence: &Rc, + ) -> Result<(), SessionError> { + let action = self.ledger.plan(sequence); + match action { + ParamsActionAv1::Current => Ok(()), + ParamsActionAv1::Recreate => { + debug!( + first = !self.has_parameters(), + "creating AV1 session parameters (first activation or a \ + sequence-header content change)" + ); + // ⚠ The owned wrapper is MOVED INTO the stored parameters below + // and lives as long as the object does — not merely across the + // create call. Module docs carry the measurement; the short of it + // is that a driver in this fleet dereferences `pColorConfig` at + // every `vkCmdDecodeVideoKHR`, so an early drop decodes the whole + // stream against recycled heap. + let owned = sequence_to_std(sequence).map_err(SessionError::ParamsAv1)?; + let mut av1 = vk::VideoDecodeAV1SessionParametersCreateInfoKHR::default() + .std_sequence_header(owned.std()); + let ci = vk::VideoSessionParametersCreateInfoKHR::default() + .video_session(self.session) + .push_next(&mut av1); + let mut fresh = vk::VideoSessionParametersKHR::null(); + // SAFETY: live device + live session; `ci` roots locals (incl. the + // OwnedStd backing, which outlives the call AND the object it + // creates — see the module docs on why the second half matters). + let r = unsafe { + (self.video_queue.fp().create_video_session_parameters_khr)( + self.device.handle(), + &ci, + std::ptr::null(), + &mut fresh, + ) + }; + if r != vk::Result::SUCCESS { + return Err(SessionError::Vk(r)); + } + // The old object goes FIRST and its backing with it — taking the + // whole `StoredParamsAv1` keeps the destroy ahead of the free, + // which is the order a driver holding the pointer needs. + if let Some(old) = self.parameters.take() { + // SAFETY: the fn-level contract — the caller drained every + // in-flight decode before a Recreate over an existing object + // reached here (checked via parameters_action + + // has_parameters), so no submitted work reads the old object; + // it is this session's own handle, on a live device. + unsafe { + (self.video_queue.fp().destroy_video_session_parameters_khr)( + self.device.handle(), + old.object, + std::ptr::null(), + ); + } + // `old` (and its sequence-header blocks) drops here, after the + // object that pointed at them is gone. + } + self.parameters = Some(StoredParamsAv1 { + object: fresh, + _sequence: owned, + }); + self.ledger.commit(action, sequence); + Ok(()) + } + } + } + + pub(crate) fn session(&self) -> vk::VideoSessionKHR { + self.session + } + + pub(crate) fn parameters(&self) -> vk::VideoSessionParametersKHR { + self.parameters + .as_ref() + .map_or(vk::VideoSessionParametersKHR::null(), |p| p.object) + } + + /// Whether the next coding scope must record the initialization RESET — + /// `true` exactly once per session, PROVIDED the command buffer that recorded + /// it actually reaches the queue: a recording/submit failure after this + /// returned `true` must call [`Self::re_arm_reset`], or the session would run + /// its whole life uninitialized. + pub(crate) fn take_needs_reset(&mut self) -> bool { + self.needs_reset.take() + } + + /// Undo a consumed [`Self::take_needs_reset`] whose RESET never reached the + /// queue (end/submit failed after recording it). + pub(crate) fn re_arm_reset(&mut self) { + self.needs_reset.re_arm(); + } +} + +impl Drop for VideoSessionAv1 { + fn drop(&mut self) { + // SAFETY: all handles are this session's own on the (contract-live) device; + // the owning decoder drains GPU work before dropping state. The destroy + // entry points ignore NULL handles, covering half-built sessions AND the + // session that never got a parameters object. The ORDER is load-bearing, + // not stylistic: memory bound into a session may not be freed while the + // session lives, so the session is destroyed first — which is also why a + // failed bind hands its allocations back here instead of freeing them + // itself (`crate::session::BindFailure`). The sequence-header backing is + // freed after both, by the field's own drop, for the same reason + // `ensure_parameters` destroys before it replaces. + unsafe { + (self.video_queue.fp().destroy_video_session_parameters_khr)( + self.device.handle(), + self.parameters(), + std::ptr::null(), + ); + (self.video_queue.fp().destroy_video_session_khr)( + self.device.handle(), + self.session, + std::ptr::null(), + ); + for memory in self.memory.drain(..) { + self.device.free_memory(memory, None); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A sequence header carrying just the fields the ledger compares. The + /// vendored AV1 parser has no builders, and `SequenceHeaderObu` derives + /// `Default` + `PartialEq`, so the fixtures are authored by field. + fn authored(max_frame_width_minus_1: u16, film_grain: bool) -> Rc { + Rc::new(SequenceHeaderObu { + max_frame_width_minus_1, + max_frame_height_minus_1: 1079, + film_grain_params_present: film_grain, + ..Default::default() + }) + } + + /// The OUTER pointer — `pStdSequenceHeader` itself — must still address the + /// stored wrapper's Std struct after the wrapper has been moved into + /// [`StoredParamsAv1`]. + /// + /// [`crate::params_av1`]'s `moving_the_wrapper_leaves_the_driver_s_pointers_put` + /// pins the INNER pointers (`pColorConfig`, `pTimingInfo`); this pins the one + /// the create info carries. `ensure_parameters` converts into a local, hands + /// `owned.std()` to `vkCreateVideoSessionParametersKHR`, and only THEN moves + /// the wrapper into the stored value — so a driver retaining + /// `pStdSequenceHeader` (rather than the `pColorConfig` this fleet was measured + /// retaining) would read a moved-from slot, with the same silent signature as + /// the original bug: plausible pictures, wrong content, no error and no + /// counter. Boxing the Std struct inside the wrapper is what makes the two + /// addresses equal, and this is the assertion that stops it being un-boxed. + #[test] + fn the_sequence_header_address_the_create_call_is_given_survives_being_stored() { + let seq = authored(1919, false); + let owned = sequence_to_std(&seq).expect("a plain 8-bit header converts"); + // Exactly what `ensure_parameters` puts in `pStdSequenceHeader`. + let handed = std::ptr::from_ref(owned.std()); + // `ensure_parameters`' own final move: `self.parameters = Some(…)`. + let parameters = Some(StoredParamsAv1 { + object: vk::VideoSessionParametersKHR::null(), + _sequence: owned, + }); + let Some(stored) = parameters else { + unreachable!("just installed") + }; + assert_eq!( + std::ptr::from_ref(stored._sequence.std()), + handed, + "the address handed to Vulkan must be the address the object keeps" + ); + // SAFETY: `stored` owns the header — which is exactly the property here. + let width = unsafe { (*handed).max_frame_width_minus_1 }; + assert_eq!(width, 1919, "the fixture's width, read back through it"); + } + + #[test] + fn the_first_activation_recreates_because_there_is_no_empty_parameters_object() { + let seq = authored(1919, false); + let mut ledger = ParamsLedgerAv1::default(); + // Not `Add`: AV1 session parameters have no update path, and no object + // exists yet — the session was created without one. + assert_eq!(ledger.plan(&seq), ParamsActionAv1::Recreate); + ledger.commit(ParamsActionAv1::Recreate, &seq); + assert_eq!(ledger.plan(&seq), ParamsActionAv1::Current); + } + + #[test] + fn a_reparsed_identical_sequence_header_is_current_not_a_recreate() { + // The parser re-parses the in-band sequence header at every keyframe: + // same content, a NEW Rc. Keying on identity would drain and rebuild the + // parameters object several times a second on a perfectly steady stream. + let a = authored(1919, false); + let b = authored(1919, false); + assert!(!Rc::ptr_eq(&a, &b)); + + let mut ledger = ParamsLedgerAv1::default(); + ledger.commit(ParamsActionAv1::Recreate, &a); + assert_eq!(ledger.plan(&b), ParamsActionAv1::Current); + } + + #[test] + fn a_changed_sequence_header_recreates_and_the_new_one_is_then_current() { + let small = authored(1279, false); + let large = authored(1919, false); + let mut ledger = ParamsLedgerAv1::default(); + ledger.commit(ParamsActionAv1::Recreate, &small); + assert_eq!(ledger.plan(&small), ParamsActionAv1::Current); + + // A resize is a content change, so the object is rebuilt — and this is + // the ONLY path AV1 has: there is no in-place replacement for a stored + // sequence header. + assert_eq!(ledger.plan(&large), ParamsActionAv1::Recreate); + ledger.commit(ParamsActionAv1::Recreate, &large); + assert_eq!(ledger.plan(&large), ParamsActionAv1::Current); + assert_eq!( + ledger.plan(&small), + ParamsActionAv1::Recreate, + "the ledger holds exactly one header — the old one is gone" + ); + } + + #[test] + fn turning_film_grain_on_is_a_content_change_the_ledger_sees() { + // It is ALSO a profile change, which rebuilds the whole session + // (SessionConfigAv1::profile) — but the ledger must not depend on the + // session layer having noticed: a sequence header that differs only in + // its grain flag is a different stored set, full stop. + let plain = authored(1919, false); + let grainy = authored(1919, true); + assert_ne!(plain, grainy); + let mut ledger = ParamsLedgerAv1::default(); + ledger.commit(ParamsActionAv1::Recreate, &plain); + assert_eq!(ledger.plan(&grainy), ParamsActionAv1::Recreate); + } + + #[test] + fn committing_current_leaves_the_stored_header_alone() { + // `commit(Current, ..)` is reachable on every steady-state frame; it must + // be a genuine no-op rather than a silent re-store of an equal value. + let a = authored(1919, false); + let mut ledger = ParamsLedgerAv1::default(); + assert!(ledger.sequence.is_none()); + ledger.commit(ParamsActionAv1::Current, &a); + assert!( + ledger.sequence.is_none(), + "Current must not install a header the object does not hold" + ); + // And the next plan still says the object needs building. + assert_eq!(ledger.plan(&a), ParamsActionAv1::Recreate); + } +} diff --git a/crates/pf-vkdecode/src/session_h265.rs b/crates/pf-vkdecode/src/session_h265.rs new file mode 100644 index 00000000..345af934 --- /dev/null +++ b/crates/pf-vkdecode/src/session_h265.rs @@ -0,0 +1,1139 @@ +//! `VkVideoSessionKHR` + `VkVideoSessionParametersKHR` lifecycle for H.265 — +//! [`crate::session`] one codec over, with the leg H.264 does not have: the VPS. +//! +//! Vulkan's H.265 session parameters hold THREE parameter-set arrays (VPS, SPS, +//! PPS) behind `VkVideoDecodeH265SessionParametersAddInfoKHR`, and +//! `StdVideoDecodeH265PictureInfo` names all three by id +//! (`sps_video_parameter_set_id`, `pps_seq_parameter_set_id`, +//! `pps_pic_parameter_set_id`) — so the object must hold the VPS its SPS names, or +//! the decode op resolves nothing. The versioning rules are Vulkan's, unchanged +//! from the H.264 ledger: +//! +//! - a NEW (vps-id / sps-id / pps-id) is ADDED via +//! `vkUpdateVideoSessionParametersKHR` with `updateSequenceCount` = previous + 1 +//! (the spec's exact-increment rule — ONE call may carry all three sets, and it +//! counts as ONE); +//! - an EXISTING id whose content changed cannot be updated in place — the object +//! is RECREATED (Vulkan forbids replacing a stored parameter set), as is an +//! object whose capacity would overflow; +//! - a stream renegotiation that resizes the DPB or the coded extent recreates the +//! whole session — `plan_to_vk_h265`'s `CapacityMismatch` is the trigger for the +//! DPB half, the extent comparison covers the other. +//! +//! **The missing-VPS case is real and handled, not assumed away.** The vendored +//! parser attaches a VPS to an SPS only when it actually saw the VPS NALU; a +//! stream joined mid-flight (punktfunk clients join live sessions) can therefore +//! carry an SPS whose VPS never arrived. `VpsSource` makes that a first-class +//! state: the ledger stores either the parsed VPS or the SPS the fallback was +//! synthesized from ([`fallback_vps_from_sps`]), and dedups on THAT — so when the +//! real VPS finally arrives, the content differs from the fallback and the object +//! RECREATES onto the real one, exactly as any other content change would. +//! +//! ⚠⚠⚠ **The Std sets' heap blocks must outlive the parameters OBJECT, not just the +//! call that hands them over.** Vulkan reads as though parameter data were captured +//! by `vkCreateVideoSessionParametersKHR`, and all three codecs in this crate +//! assumed it. NVIDIA 610.57.04 does not: for AV1 it was measured keeping +//! `StdVideoAV1SequenceHeader::pColorConfig` and dereferencing it when a decode is +//! RECORDED, which decoded every frame against recycled heap ([`crate::session_av1`] +//! carries the measurement). H.265 embeds MORE such pointers than any other codec +//! here — the SPS alone carries seven — so [`StoredParamsH265`] holds the object and +//! its backings in ONE value with one lifetime, and both the recreate path and +//! `Drop` destroy the object before that value is released. The OUTER pointers +//! (`pStdVPSs`/`pStdSPSs`/`pStdPPSs`) are held the same way and for the same reason +//! — [`crate::session`]'s module docs carry the argument and the line it draws. +//! +//! `ParamsLedgerH265` is the pure half of that decision table (unit-tested); +//! `VideoSessionH265` is the thin Vulkan half. + +use std::rc::Rc; + +use ash::vk; +use ash::vk::native as hh; +use tracing::debug; + +use crate::caps::DecodeCaps; +use crate::caps_h265::H265ProfileChain; +use crate::caps_h265::H265ProfileKey; +use crate::device::DecodeDevice; +use crate::params_h265::fallback_vps_from_sps; +use crate::params_h265::pps_to_std_h265; +use crate::params_h265::sps_to_std_h265; +use crate::params_h265::vps_to_std_h265; +use crate::params_h265::H265ParamsError; +use crate::params_h265::OwnedStdH265Pps; +use crate::params_h265::OwnedStdH265Sps; +use crate::params_h265::OwnedStdH265Vps; +use crate::params_h265::Pps; +use crate::params_h265::Sps; +use crate::params_h265::Vps; +use crate::session::bind_session_memory; +use crate::session::ResetArm; +use crate::session::SessionError; + +/// Parameter-object capacity. Punktfunk hosts emit one VPS + one SPS + one PPS per +/// stream; the headroom absorbs id churn across renegotiations without recreation, +/// and an overflow beyond it recreates rather than fails. +pub(crate) const MAX_STD_VPS: usize = 4; +pub(crate) const MAX_STD_SPS: usize = 4; +pub(crate) const MAX_STD_PPS: usize = 8; + +/// Where the VPS an activation needs comes from — and, equally, the ledger's +/// identity for it. +/// +/// Comparing whole values (not just ids) is what makes the fallback→real +/// transition correct: `Parsed` and `FromSps` under the same id are never equal, +/// so the arrival of the genuine VPS is a content change and recreates the object +/// instead of silently keeping the synthesized stand-in. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum VpsSource { + /// The stream's own VPS, as the parser attached it to the SPS. + Parsed(Rc), + /// No VPS NALU was ever seen; the SPS it would be synthesized from + /// ([`fallback_vps_from_sps`]) stands in as the identity. + FromSps(Rc), +} + +impl VpsSource { + /// The VPS an SPS activation needs. + pub(crate) fn for_sps(sps: &Rc) -> Self { + match &sps.vps { + Some(vps) => VpsSource::Parsed(Rc::clone(vps)), + None => VpsSource::FromSps(Rc::clone(sps)), + } + } + + /// `vps_video_parameter_set_id` — the id the parameters object stores it under + /// and the SPS names it by. + pub(crate) fn id(&self) -> u8 { + match self { + VpsSource::Parsed(vps) => vps.video_parameter_set_id, + VpsSource::FromSps(sps) => sps.video_parameter_set_id, + } + } + + /// Convert to the Std struct (owning wrapper). + pub(crate) fn to_std(&self) -> Result { + match self { + VpsSource::Parsed(vps) => vps_to_std_h265(vps), + VpsSource::FromSps(sps) => fallback_vps_from_sps(sps), + } + } +} + +/// What the ledger decided for one (VPS, SPS, PPS) activation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParamsActionH265 { + /// All three sets are already stored with identical content — nothing to do. + Current, + /// At least one set is new; ONE update call (seq += 1) adds what is missing. + Add { + add_vps: bool, + add_sps: bool, + add_pps: bool, + }, + /// A stored id changed content, or capacity would overflow: recreate the + /// parameters object (Vulkan cannot replace or evict a stored set). + Recreate, +} + +/// Pure bookkeeping for the parameters object: which sets it holds (by id AND +/// content — the parser re-parses in-band parameter sets every IRAP, so pointer +/// identity means nothing) and the update sequence counter. +#[derive(Debug, Default)] +pub(crate) struct ParamsLedgerH265 { + vps: Vec<(u8, VpsSource)>, + sps: Vec<(u8, Rc)>, + /// Keyed `(seq_parameter_set_id, pic_parameter_set_id)`, the pair Vulkan + /// resolves a stored PPS by. + pps: Vec<((u8, u8), Rc)>, + update_seq: u32, +} + +impl ParamsLedgerH265 { + /// Decide the action for activating (`vps`, `sps`, `pps`). Pure — mutate via + /// [`Self::commit`]. + pub(crate) fn plan(&self, vps: &VpsSource, sps: &Rc, pps: &Rc) -> ParamsActionH265 { + let vps_key = vps.id(); + let sps_key = sps.seq_parameter_set_id; + let pps_key = (pps.seq_parameter_set_id, pps.pic_parameter_set_id); + + let stored_vps = self.vps.iter().find(|(id, _)| *id == vps_key); + let stored_sps = self.sps.iter().find(|(id, _)| *id == sps_key); + let stored_pps = self.pps.iter().find(|(id, _)| *id == pps_key); + // Content changes under a stored id — including a fallback VPS being + // superseded by the real one (VpsSource docs). + if let Some((_, stored)) = stored_vps { + if stored != vps { + return ParamsActionH265::Recreate; + } + } + if let Some((_, stored)) = stored_sps { + if **stored != **sps { + return ParamsActionH265::Recreate; + } + } + if let Some((_, stored)) = stored_pps { + if **stored != **pps { + return ParamsActionH265::Recreate; + } + } + let add_vps = stored_vps.is_none(); + let add_sps = stored_sps.is_none(); + let add_pps = stored_pps.is_none(); + if !add_vps && !add_sps && !add_pps { + return ParamsActionH265::Current; + } + if (add_vps && self.vps.len() >= MAX_STD_VPS) + || (add_sps && self.sps.len() >= MAX_STD_SPS) + || (add_pps && self.pps.len() >= MAX_STD_PPS) + { + return ParamsActionH265::Recreate; + } + ParamsActionH265::Add { + add_vps, + add_sps, + add_pps, + } + } + + /// Apply a decided action. `Add` bumps the sequence count by EXACTLY one (the + /// Vulkan update rule — one call may carry all three sets); `Recreate` resets + /// the ledger to just the current triple with a fresh object's zero counter + /// (any other id the stream still references simply re-Adds on next + /// activation). + pub(crate) fn commit( + &mut self, + action: ParamsActionH265, + vps: &VpsSource, + sps: &Rc, + pps: &Rc, + ) { + match action { + ParamsActionH265::Current => {} + ParamsActionH265::Add { + add_vps, + add_sps, + add_pps, + } => { + if add_vps { + self.vps.push((vps.id(), vps.clone())); + } + if add_sps { + self.sps.push((sps.seq_parameter_set_id, Rc::clone(sps))); + } + if add_pps { + self.pps.push(( + (pps.seq_parameter_set_id, pps.pic_parameter_set_id), + Rc::clone(pps), + )); + } + self.update_seq += 1; + } + ParamsActionH265::Recreate => { + self.vps.clear(); + self.sps.clear(); + self.pps.clear(); + self.vps.push((vps.id(), vps.clone())); + self.sps.push((sps.seq_parameter_set_id, Rc::clone(sps))); + self.pps.push(( + (pps.seq_parameter_set_id, pps.pic_parameter_set_id), + Rc::clone(pps), + )); + self.update_seq = 0; + } + } + } + + /// The sequence count the NEXT `vkUpdateVideoSessionParametersKHR` must carry. + pub(crate) fn next_update_seq(&self) -> u32 { + self.update_seq + 1 + } +} + +/// The session's create-time shape; a plan disagreeing with it forces a rebuild. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SessionConfigH265 { + pub max_coded_extent: vk::Extent2D, + pub max_dpb_slots: u32, + pub max_active_references: u32, + /// The profile the session was created against — profile idc AND the chroma + /// format / bit depths, all four of which a stream can renegotiate (an SPS + /// switching Main→Main 10 mid-stream is a session rebuild, not an update). + pub profile: H265ProfileKey, +} + +/// A live parameters object **and every Std parameter set it was given**, in one +/// field — because the two may not drift apart. `session::StoredParams` one codec +/// over, with the VPS leg H.264 does not have. +/// +/// The wrapper is not decoration and not defensive: a driver in this fleet keeps +/// the embedded pointers out of a Std set and dereferences them long after the call +/// that handed them over returned (module docs), so releasing the backing early +/// hands it freed memory. One value rather than two fields makes "an object whose +/// backing is gone" unrepresentable, which is the only shape of this bug — and the +/// shape a `let owned = …;` local silently had. H.265 has the most to lose: its Std +/// SPS points at a profile/tier/level block, a DPB-manager block, scaling lists, +/// the short-term RPS candidate array and the long-term SPS candidates. +/// +/// What is pinned, precisely: the wrappers' BOXED blocks, which is what the driver +/// was measured retaining. The contiguous array of outer `StdVideoH265*` structs +/// each call receives is a short-lived temporary, and the driver copies THAT before +/// returning — which is what the AV1 fix itself rests on, its Std header being moved +/// into storage after the create call on a rung that is now 250/250 bit-exact. So +/// moving these wrappers, or reallocating the `Vec`s holding them, disturbs nothing +/// the driver kept; `params_h265::moving_the_wrapper_leaves_the_driver_s_pointers_put` +/// pins the half that matters. +struct StoredParamsH265 { + object: vk::VideoSessionParametersKHR, + /// One entry per set the OBJECT stores, held for the object's whole life. + /// Never read by this crate after the create/update call; the DRIVER reads the + /// blocks they own. + vps: Vec, + sps: Vec, + pps: Vec, + /// The contiguous Std ARRAYS the create call was handed as + /// `pStdVPSs`/`pStdSPSs`/`pStdPPSs` — the OUTER pointers, held for the object's + /// life for the reason the wrappers are ([`crate::session`]'s `StoredParams` + /// carries the argument). Built by [`Self::assemble`] at their final address. + std_vps: Vec, + std_sps: Vec, + std_pps: Vec, +} + +impl StoredParamsH265 { + /// The wrappers plus the contiguous Std arrays the create call reads its + /// `pStdVPSs`/`pStdSPSs`/`pStdPPSs` out of, with a NULL object the caller fills + /// in once `vkCreateVideoSessionParametersKHR` has succeeded + /// ([`crate::session`]'s `StoredParams::assemble` for why it happens here). + fn assemble( + vps: Vec, + sps: Vec, + pps: Vec, + ) -> Self { + // COPIES of each wrapper's Std struct (it is `Copy`); the embedded pointers + // they carry still address the wrappers' own boxed blocks, which is why + // both halves have to be kept. + let std_vps = vps.iter().map(|o| *o.std()).collect(); + let std_sps = sps.iter().map(|o| *o.std()).collect(); + let std_pps = pps.iter().map(|o| *o.std()).collect(); + Self { + object: vk::VideoSessionParametersKHR::null(), + vps, + sps, + pps, + std_vps, + std_sps, + std_pps, + } + } + + /// The placeholder a half-built session holds. `vkDestroyVideoSessionParametersKHR` + /// ignores a NULL handle, so a [`VideoSessionH265::create`] that fails before + /// the object exists still drops cleanly. + fn none() -> Self { + Self::assemble(Vec::new(), Vec::new(), Vec::new()) + } + + /// Take over sets an `Add` just handed to the live object — they belong to the + /// OBJECT now, so their blocks live as long as it does rather than as long as + /// the update call. Only ever reached after that call SUCCEEDED: a failed + /// update stored nothing, and its wrappers are dropped instead. + fn adopt( + &mut self, + vps: Option, + sps: Option, + pps: Option, + ) { + self.vps.extend(vps); + self.sps.extend(sps); + self.pps.extend(pps); + } +} + +/// The Vulkan half: session + bound memory + parameters object. +pub(crate) struct VideoSessionH265 { + device: ash::Device, + video_queue: ash::khr::video_queue::Device, + session: vk::VideoSessionKHR, + memory: Vec, + parameters: StoredParamsH265, + ledger: ParamsLedgerH265, + pub(crate) config: SessionConfigH265, + /// The session has never run a coding scope: the first one records a + /// `VK_VIDEO_CODING_CONTROL_RESET_BIT_KHR` control before anything else. + needs_reset: ResetArm, +} + +impl VideoSessionH265 { + /// Create the session + an EMPTY parameters object (sets arrive via + /// [`Self::ensure_parameters`], which the decoder calls before the first + /// decode). + /// + /// # Safety + /// + /// `dev` wraps live handles ([`crate::DeviceHandles`] contract). + pub(crate) unsafe fn create( + dev: &DecodeDevice, + caps: &DecodeCaps, + config: SessionConfigH265, + ) -> Result { + let mut chain = H265ProfileChain::new(config.profile); + let profile = chain.wire(); + let std_header_version = caps.std_header_version; + let session_ci = vk::VideoSessionCreateInfoKHR::default() + .queue_family_index(dev.decode_qf()) + .video_profile(profile) + .picture_format(caps.output_format) + .max_coded_extent(config.max_coded_extent) + .reference_picture_format(caps.dpb_format) + .max_dpb_slots(config.max_dpb_slots) + .max_active_reference_pictures(config.max_active_references) + .std_header_version(&std_header_version); + let mut session = vk::VideoSessionKHR::null(); + // SAFETY: live device; `session_ci` roots locals (chain, header version) + // that outlive the call. + let r = unsafe { + (dev.video_queue().fp().create_video_session_khr)( + dev.ash().handle(), + &session_ci, + std::ptr::null(), + &mut session, + ) + }; + if r != vk::Result::SUCCESS { + return Err(SessionError::Vk(r)); + } + + let mut built = Self { + device: dev.ash().clone(), + video_queue: dev.video_queue().clone(), + session, + memory: Vec::new(), + parameters: StoredParamsH265::none(), + ledger: ParamsLedgerH265::default(), + config, + needs_reset: ResetArm::armed(), + }; + // SAFETY: fn contract; on error `built` drops and unwinds the session + + // whatever memory was bound. + unsafe { + // A bind failure hands its allocations BACK: parking them in `built` + // is what makes the early return destroy the session before freeing + // them (BindFailure docs — Vulkan defines no partial-bind rollback). + match bind_session_memory(dev, session) { + Ok(memory) => built.memory = memory, + Err(failure) => { + built.memory = failure.allocations; + return Err(failure.error); + } + } + built.parameters = + built.create_parameters_object(Vec::new(), Vec::new(), Vec::new())?; + } + Ok(built) + } + + /// Create a parameters object holding exactly `vps`/`sps`/`pps` (any may be + /// empty), **fused with the wrappers whose heap blocks it points at**. + /// + /// Taking the wrappers BY VALUE rather than as Std slices is the point: there is + /// no way to reach `vkCreateVideoSessionParametersKHR` from here without the + /// resulting object taking ownership of everything it will go on dereferencing + /// (module docs, [`StoredParamsH265`]). + /// + /// # Safety + /// + /// Live device + live session. + unsafe fn create_parameters_object( + &self, + vps: Vec, + sps: Vec, + pps: Vec, + ) -> Result { + // Assembled FIRST so the arrays `pStdVPSs`/`pStdSPSs`/`pStdPPSs` will point + // at are already where they will stay: `stored` is returned by value, and + // moving a `Vec` moves its handle, not the block the driver was given. + let mut stored = StoredParamsH265::assemble(vps, sps, pps); + let add = vk::VideoDecodeH265SessionParametersAddInfoKHR::default() + .std_vp_ss(&stored.std_vps) + .std_sp_ss(&stored.std_sps) + .std_pp_ss(&stored.std_pps); + let mut h265 = vk::VideoDecodeH265SessionParametersCreateInfoKHR::default() + .max_std_vps_count(MAX_STD_VPS as u32) + .max_std_sps_count(MAX_STD_SPS as u32) + .max_std_pps_count(MAX_STD_PPS as u32) + .parameters_add_info(&add); + let ci = vk::VideoSessionParametersCreateInfoKHR::default() + .video_session(self.session) + .push_next(&mut h265); + let mut object = vk::VideoSessionParametersKHR::null(); + // SAFETY: fn contract; `ci` roots locals outliving the call, and everything + // the driver may retain past it — the Std arrays AND the blocks their + // embedded pointers address — is owned by `stored`, which is returned + // rather than dropped here. + let r = unsafe { + (self.video_queue.fp().create_video_session_parameters_khr)( + self.device.handle(), + &ci, + std::ptr::null(), + &mut object, + ) + }; + if r != vk::Result::SUCCESS { + return Err(SessionError::Vk(r)); + } + stored.object = object; + Ok(stored) + } + + /// The ledger's verdict for activating (`vps`, `sps`, `pps`), without mutating + /// anything — the decoder consults this BEFORE [`Self::ensure_parameters`] so + /// a [`ParamsActionH265::Recreate`] can be preceded by a full in-flight drain + /// (the destroy inside the recreate must never race a submitted decode). + pub(crate) fn parameters_action( + &self, + vps: &VpsSource, + sps: &Rc, + pps: &Rc, + ) -> ParamsActionH265 { + self.ledger.plan(vps, sps, pps) + } + + /// Make the parameters object hold this AU's activated (VPS, SPS, PPS), + /// converting through the params module and Adding/Recreating per the ledger. + /// + /// # Safety + /// + /// Live device; when [`Self::parameters_action`] says `Recreate`, the caller + /// has ALREADY drained every in-flight decode — the old object is destroyed + /// here, and a still-executing decode reading it would be use-after-free at + /// the driver level. `Current`/`Add` touch no object a submitted decode can be + /// reading. + pub(crate) unsafe fn ensure_parameters( + &mut self, + vps: &VpsSource, + sps: &Rc, + pps: &Rc, + ) -> Result<(), SessionError> { + let action = self.ledger.plan(vps, sps, pps); + match action { + ParamsActionH265::Current => Ok(()), + ParamsActionH265::Add { + add_vps, + add_sps, + add_pps, + } => { + // Every owned wrapper below stays alive until after the update + // call: the Std structs embed pointers into their heap blocks. + let owned_vps = if add_vps { Some(vps.to_std()?) } else { None }; + let owned_sps = if add_sps { + Some(sps_to_std_h265(sps)?) + } else { + None + }; + let owned_pps = if add_pps { + Some(pps_to_std_h265(pps)?) + } else { + None + }; + let vps_slice: &[hh::StdVideoH265VideoParameterSet] = match &owned_vps { + Some(o) => std::slice::from_ref(o.std()), + None => &[], + }; + let sps_slice: &[hh::StdVideoH265SequenceParameterSet] = match &owned_sps { + Some(o) => std::slice::from_ref(o.std()), + None => &[], + }; + let pps_slice: &[hh::StdVideoH265PictureParameterSet] = match &owned_pps { + Some(o) => std::slice::from_ref(o.std()), + None => &[], + }; + let mut add = vk::VideoDecodeH265SessionParametersAddInfoKHR::default() + .std_vp_ss(vps_slice) + .std_sp_ss(sps_slice) + .std_pp_ss(pps_slice); + let update = vk::VideoSessionParametersUpdateInfoKHR::default() + .update_sequence_count(self.ledger.next_update_seq()) + .push_next(&mut add); + // SAFETY: live device + parameters object; `update` roots locals + // (incl. the OwnedStd backings) outliving the call — and the + // backings go on outliving it, adopted below. + let r = unsafe { + (self.video_queue.fp().update_video_session_parameters_khr)( + self.device.handle(), + self.parameters.object, + &update, + ) + }; + if r != vk::Result::SUCCESS { + return Err(SessionError::Vk(r)); + } + // ⚠ The added sets now belong to the OBJECT, so their heap blocks + // must too: an Add whose wrappers died at the end of this arm would + // be the AV1 use-after-free with an update call in front of it. + self.parameters.adopt(owned_vps, owned_sps, owned_pps); + self.ledger.commit(action, vps, sps, pps); + Ok(()) + } + ParamsActionH265::Recreate => { + debug!( + vps_id = vps.id(), + sps_id = sps.seq_parameter_set_id, + pps_id = pps.pic_parameter_set_id, + "recreating H.265 session parameters (content change or capacity)" + ); + let owned_vps = vps.to_std()?; + let owned_sps = sps_to_std_h265(sps)?; + let owned_pps = pps_to_std_h265(pps)?; + // SAFETY: fn contract — live device + live session. The wrappers + // are MOVED IN and come back owned by the fresh object, so they + // live as long as it does rather than merely across the call. + let fresh = unsafe { + self.create_parameters_object( + vec![owned_vps], + vec![owned_sps], + vec![owned_pps], + )? + }; + // The old object goes FIRST and its backings with it — installing + // `fresh` through a local keeps the destroy ahead of the free, + // which is the order a driver still holding the old pointers needs. + let old = std::mem::replace(&mut self.parameters, fresh); + // SAFETY: the fn-level contract — the caller drained every + // in-flight decode before a Recreate reached here (checked via + // parameters_action), so no submitted work reads the old object; + // it is this session's own handle, on a live device. + unsafe { + (self.video_queue.fp().destroy_video_session_parameters_khr)( + self.device.handle(), + old.object, + std::ptr::null(), + ); + } + // Explicit, because the ORDER is the whole point: every Std block + // `old` owns is released only now, after the object that pointed at + // them is gone. + drop(old); + self.ledger.commit(action, vps, sps, pps); + Ok(()) + } + } + } + + pub(crate) fn session(&self) -> vk::VideoSessionKHR { + self.session + } + + pub(crate) fn parameters(&self) -> vk::VideoSessionParametersKHR { + self.parameters.object + } + + /// Whether the next coding scope must record the initialization RESET — + /// `true` exactly once per session, PROVIDED the command buffer that recorded + /// it actually reaches the queue: a recording/submit failure after this + /// returned `true` must call [`Self::re_arm_reset`], or the session would run + /// its whole life uninitialized. + pub(crate) fn take_needs_reset(&mut self) -> bool { + self.needs_reset.take() + } + + /// Undo a consumed [`Self::take_needs_reset`] whose RESET never reached the + /// queue (end/submit failed after recording it). + pub(crate) fn re_arm_reset(&mut self) { + self.needs_reset.re_arm(); + } +} + +impl Drop for VideoSessionH265 { + fn drop(&mut self) { + // SAFETY: all handles are this session's own on the (contract-live) device; + // the owning decoder drains GPU work before dropping state. The destroy + // entry points ignore NULL handles, covering half-built sessions. The + // ORDER is load-bearing, not stylistic: memory bound into a session may + // not be freed while the session lives, so the session is destroyed first + // — which is also why a failed bind hands its allocations back here + // instead of freeing them itself (`crate::session::BindFailure`). The Std + // backings are freed after both, by the `parameters` field's own drop, + // which Rust runs AFTER this body — the same reason `ensure_parameters` + // destroys before it replaces. + unsafe { + (self.video_queue.fp().destroy_video_session_parameters_khr)( + self.device.handle(), + self.parameters.object, + std::ptr::null(), + ); + (self.video_queue.fp().destroy_video_session_khr)( + self.device.handle(), + self.session, + std::ptr::null(), + ); + for memory in self.memory.drain(..) { + self.device.free_memory(memory, None); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The vendored H.265 parser has no set builders (unlike its H.264 half), and + /// `Pps` derives no `Default` — so fixtures are written out once here. Only + /// the fields the ledger keys or compares carry meaning. + fn authored_sps(sps_id: u8, vps_id: u8, width: u16) -> Rc { + Rc::new(Sps { + video_parameter_set_id: vps_id, + seq_parameter_set_id: sps_id, + chroma_format_idc: 1, + pic_width_in_luma_samples: width, + pic_height_in_luma_samples: 64, + ..Default::default() + }) + } + + /// The same SPS with the stream's VPS attached, as the parser would. + fn with_vps(sps: &Rc, vps_id: u8, max_layers_minus1: u8) -> Rc { + let vps = Vps { + video_parameter_set_id: vps_id, + max_layers_minus1, + ..Default::default() + }; + Rc::new(Sps { + vps: Some(Rc::new(vps)), + ..(**sps).clone() + }) + } + + fn authored_pps(sps: &Rc, pps_id: u8, init_qp_minus26: i8) -> Rc { + Rc::new(Pps { + pic_parameter_set_id: pps_id, + seq_parameter_set_id: sps.seq_parameter_set_id, + dependent_slice_segments_enabled_flag: false, + output_flag_present_flag: false, + num_extra_slice_header_bits: 0, + sign_data_hiding_enabled_flag: false, + cabac_init_present_flag: false, + num_ref_idx_l0_default_active_minus1: 0, + num_ref_idx_l1_default_active_minus1: 0, + init_qp_minus26, + constrained_intra_pred_flag: false, + transform_skip_enabled_flag: false, + cu_qp_delta_enabled_flag: false, + diff_cu_qp_delta_depth: 0, + cb_qp_offset: 0, + cr_qp_offset: 0, + slice_chroma_qp_offsets_present_flag: false, + weighted_pred_flag: false, + weighted_bipred_flag: false, + transquant_bypass_enabled_flag: false, + tiles_enabled_flag: false, + entropy_coding_sync_enabled_flag: false, + num_tile_columns_minus1: 0, + num_tile_rows_minus1: 0, + uniform_spacing_flag: true, + column_width_minus1: [0; 19], + row_height_minus1: [0; 21], + loop_filter_across_tiles_enabled_flag: true, + loop_filter_across_slices_enabled_flag: false, + deblocking_filter_control_present_flag: false, + deblocking_filter_override_enabled_flag: false, + deblocking_filter_disabled_flag: false, + beta_offset_div2: 0, + tc_offset_div2: 0, + scaling_list_data_present_flag: false, + scaling_list: Default::default(), + lists_modification_present_flag: false, + log2_parallel_merge_level_minus2: 0, + slice_segment_header_extension_present_flag: false, + extension_present_flag: false, + range_extension_flag: false, + range_extension: Default::default(), + scc_extension_flag: false, + scc_extension: Default::default(), + qp_bd_offset_y: 0, + sps: Rc::clone(sps), + }) + } + + /// An `Add` hands NEW Std sets to an EXISTING parameters object, so their heap + /// blocks must live as long as that OBJECT — not as long as the update call + /// that carried them. [`StoredParamsH265::adopt`] is where the transfer + /// happens, and this pins that it genuinely takes ownership: `ensure_parameters` + /// drops its local wrappers the instant this returns, and a driver holding + /// `pDecPicBufMgr` or `pProfileTierLevel` would be reading freed heap from the + /// next frame on ([`crate::session_av1`] for the measurement that made this + /// real). + #[test] + fn an_added_set_keeps_its_blocks_alive_past_the_update_call() { + // The ledger fixtures never convert, so they leave `general_profile_idc` + // at the unmappable 0; Std conversion needs a real one. + let mut base = (*authored_sps(0, 0, 64)).clone(); + base.profile_tier_level.general_profile_idc = 1; // Main + base.max_dec_pic_buffering_minus1 = [5, 0, 0, 0, 0, 0, 0]; + let sps = Rc::new(base); + let owned_vps = VpsSource::for_sps(&sps).to_std().expect("converts"); + let owned_sps = sps_to_std_h265(&sps).expect("converts"); + let vps_ptl = owned_vps.std().pProfileTierLevel; + let sps_dpb = owned_sps.std().pDecPicBufMgr; + assert!(!vps_ptl.is_null() && !sps_dpb.is_null()); + + let mut stored = StoredParamsH265::none(); + stored.adopt(Some(owned_vps), Some(owned_sps), None); + assert_eq!( + (stored.vps.len(), stored.sps.len(), stored.pps.len()), + (1, 1, 0), + "the object took the sets themselves, not borrows of them" + ); + assert_eq!(stored.vps[0].std().pProfileTierLevel, vps_ptl); + assert_eq!(stored.sps[0].std().pDecPicBufMgr, sps_dpb); + // SAFETY: `stored` owns both blocks — which is exactly the property here. + let dpb = unsafe { (*sps_dpb).max_dec_pic_buffering_minus1[0] }; + assert_eq!(dpb, 5, "the fixture's DPB sizing, read back live"); + } + + /// …and it keeps the ADDRESS too, not merely the blocks. + /// + /// The Add path hands `vkUpdateVideoSessionParametersKHR` a + /// `std::slice::from_ref(o.std())` — a one-element array that IS the wrapper's + /// own Std struct — and then moves the wrapper into [`StoredParamsH265`]. The + /// test above covers a driver retaining `pDecPicBufMgr` (an INNER pointer); + /// this covers one retaining `pStdVPSs`/`pStdSPSs`/`pStdPPSs`, which the same + /// wording in the spec permits just as much. + #[test] + fn an_added_set_keeps_the_address_the_update_call_was_given() { + let mut base = (*authored_sps(0, 0, 64)).clone(); + base.profile_tier_level.general_profile_idc = 1; // Main + let sps = Rc::new(base); + let pps = authored_pps(&sps, 0, 0); + let owned_vps = VpsSource::for_sps(&sps).to_std().expect("converts"); + let owned_sps = sps_to_std_h265(&sps).expect("converts"); + let owned_pps = pps_to_std_h265(&pps).expect("converts"); + // Exactly what `ensure_parameters` puts in `pStdVPSs`/`pStdSPSs`/`pStdPPSs`. + let handed_vps = std::ptr::from_ref(owned_vps.std()); + let handed_sps = std::ptr::from_ref(owned_sps.std()); + let handed_pps = std::ptr::from_ref(owned_pps.std()); + + let mut stored = StoredParamsH265::none(); + stored.adopt(Some(owned_vps), Some(owned_sps), Some(owned_pps)); + assert_eq!( + ( + std::ptr::from_ref(stored.vps[0].std()), + std::ptr::from_ref(stored.sps[0].std()), + std::ptr::from_ref(stored.pps[0].std()), + ), + (handed_vps, handed_sps, handed_pps), + "the addresses handed to Vulkan must be the ones the object keeps" + ); + // SAFETY: `stored` owns all three — which is exactly the property here. + let ids = unsafe { + ( + (*handed_vps).vps_video_parameter_set_id, + (*handed_sps).sps_seq_parameter_set_id, + (*handed_pps).pps_pic_parameter_set_id, + ) + }; + assert_eq!(ids, (0, 0, 0), "read back through the driver's pointers"); + } + + /// The create path's OUTER pointers: `pStdVPSs`/`pStdSPSs`/`pStdPPSs` address + /// contiguous COPIES of the wrappers' Std structs, and those arrays must + /// outlive the create call the same way the wrappers do. + /// + /// [`StoredParamsH265::assemble`] builds them at their final address — inside + /// the value the parameters object is returned in — so the pointer the driver + /// is given never moves. Before this they were function-local `Vec`s, dropped + /// the instant `create_parameters_object` returned. + #[test] + fn the_std_arrays_the_create_call_is_given_are_the_ones_the_object_keeps() { + let mut base = (*authored_sps(0, 0, 64)).clone(); + base.profile_tier_level.general_profile_idc = 1; // Main + let sps = Rc::new(base); + let pps = authored_pps(&sps, 0, 0); + let owned_vps = VpsSource::for_sps(&sps).to_std().expect("converts"); + let owned_sps = sps_to_std_h265(&sps).expect("converts"); + let owned_pps = pps_to_std_h265(&pps).expect("converts"); + + let stored = StoredParamsH265::assemble(vec![owned_vps], vec![owned_sps], vec![owned_pps]); + // Exactly what `create_parameters_object` hands the create call. + let handed = ( + stored.std_vps.as_ptr(), + stored.std_sps.as_ptr(), + stored.std_pps.as_ptr(), + ); + // The move `create_parameters_object` ends with: `Ok(stored)`. + let stored = std::hint::black_box(stored); + assert_eq!( + ( + stored.std_vps.len(), + stored.std_sps.len(), + stored.std_pps.len() + ), + (1, 1, 1) + ); + assert_eq!( + ( + stored.std_vps.as_ptr(), + stored.std_sps.as_ptr(), + stored.std_pps.as_ptr(), + ), + handed, + "the arrays handed to Vulkan must be the ones the object keeps" + ); + // And their COPIES still address the wrappers' own live blocks. + let (ptl, dpb) = ( + stored.std_vps[0].pProfileTierLevel, + stored.std_sps[0].pDecPicBufMgr, + ); + assert!(!ptl.is_null() && !dpb.is_null(), "both are always attached"); + assert_eq!(ptl, stored.vps[0].std().pProfileTierLevel); + assert_eq!(dpb, stored.sps[0].std().pDecPicBufMgr); + // SAFETY: `stored` owns both blocks — which is exactly the property here. + let profile_idc = unsafe { (*ptl).general_profile_idc }; + assert_eq!(profile_idc, 1, "the fixture's Main profile, read back live"); + assert_eq!( + stored.std_pps[0].pps_pic_parameter_set_id, + stored.pps[0].std().pps_pic_parameter_set_id, + "the PPS copy is the wrapper's, field for field" + ); + } + + #[test] + fn the_first_activation_adds_all_three_sets_in_one_update_call() { + let sps = with_vps(&authored_sps(0, 0, 64), 0, 0); + let vps = VpsSource::for_sps(&sps); + let pps = authored_pps(&sps, 0, 0); + + let mut ledger = ParamsLedgerH265::default(); + assert_eq!(ledger.next_update_seq(), 1); + let action = ledger.plan(&vps, &sps, &pps); + assert_eq!( + action, + ParamsActionH265::Add { + add_vps: true, + add_sps: true, + add_pps: true + } + ); + ledger.commit(action, &vps, &sps, &pps); + // ONE call carried all three: the counter moves by exactly one. + assert_eq!(ledger.next_update_seq(), 2); + assert_eq!(ledger.plan(&vps, &sps, &pps), ParamsActionH265::Current); + } + + #[test] + fn a_reactivated_identical_triple_is_current_even_across_reparses() { + // The parser re-parses in-band sets at every IRAP: same content, NEW Rcs. + let sps_a = with_vps(&authored_sps(0, 0, 64), 0, 0); + let sps_b = with_vps(&authored_sps(0, 0, 64), 0, 0); + assert!(!Rc::ptr_eq(&sps_a, &sps_b)); + let (vps_a, vps_b) = (VpsSource::for_sps(&sps_a), VpsSource::for_sps(&sps_b)); + assert_eq!(vps_a, vps_b, "identical content, distinct allocations"); + let pps_a = authored_pps(&sps_a, 0, 0); + let pps_b = authored_pps(&sps_b, 0, 0); + + let mut ledger = ParamsLedgerH265::default(); + let action = ledger.plan(&vps_a, &sps_a, &pps_a); + ledger.commit(action, &vps_a, &sps_a, &pps_a); + assert_eq!( + ledger.plan(&vps_b, &sps_b, &pps_b), + ParamsActionH265::Current + ); + } + + #[test] + fn a_new_pps_id_over_stored_vps_and_sps_adds_only_the_pps() { + let sps = with_vps(&authored_sps(0, 0, 64), 0, 0); + let vps = VpsSource::for_sps(&sps); + let pps0 = authored_pps(&sps, 0, 0); + let pps1 = authored_pps(&sps, 1, 0); + + let mut ledger = ParamsLedgerH265::default(); + let action = ledger.plan(&vps, &sps, &pps0); + ledger.commit(action, &vps, &sps, &pps0); + assert_eq!( + ledger.plan(&vps, &sps, &pps1), + ParamsActionH265::Add { + add_vps: false, + add_sps: false, + add_pps: true + } + ); + } + + #[test] + fn a_stream_with_no_vps_nalu_stores_the_fallback_and_recreates_when_the_real_one_arrives() { + // Joined mid-stream: the SPS arrived without its VPS, so the ledger's + // identity is the SPS the fallback would be synthesized from. + let sps_no_vps = authored_sps(0, 0, 64); + assert!(sps_no_vps.vps.is_none()); + let fallback = VpsSource::for_sps(&sps_no_vps); + assert!(matches!(fallback, VpsSource::FromSps(_))); + assert_eq!(fallback.id(), 0, "the id comes off the SPS's vps id"); + let pps = authored_pps(&sps_no_vps, 0, 0); + + let mut ledger = ParamsLedgerH265::default(); + let action = ledger.plan(&fallback, &sps_no_vps, &pps); + assert_eq!( + action, + ParamsActionH265::Add { + add_vps: true, + add_sps: true, + add_pps: true + } + ); + ledger.commit(action, &fallback, &sps_no_vps, &pps); + // Re-activating the same VPS-less SPS is Current, not a churn of Adds. + assert_eq!( + ledger.plan(&fallback, &sps_no_vps, &pps), + ParamsActionH265::Current + ); + + // The real VPS finally arrives (same id 0). Vulkan cannot replace a + // stored set, so the fallback→real transition RECREATES. (In a real + // stream the SPS changes with it — it now carries the VPS — so the + // recreate is over-determined; the VPS leg on its own is isolated in + // `changed_vps_content_under_a_stored_id_recreates_with_the_sps_and_pps_untouched`.) + let sps_with_vps = with_vps(&sps_no_vps, 0, 0); + let real = VpsSource::for_sps(&sps_with_vps); + assert!(matches!(real, VpsSource::Parsed(_))); + assert_ne!(real, fallback, "a synthesized VPS is not the parsed one"); + let pps = authored_pps(&sps_with_vps, 0, 0); + let action = ledger.plan(&real, &sps_with_vps, &pps); + assert_eq!(action, ParamsActionH265::Recreate); + ledger.commit(action, &real, &sps_with_vps, &pps); + assert_eq!( + ledger.next_update_seq(), + 1, + "a fresh object restarts its counter" + ); + assert_eq!( + ledger.plan(&real, &sps_with_vps, &pps), + ParamsActionH265::Current + ); + } + + /// A VPS built directly, decoupled from any SPS. The ledger keys its three + /// arrays independently, so this is how the VPS leg is exercised ALONE — in a + /// real stream an SPS carries its VPS, so any VPS change drags the SPS's + /// content along and the recreate becomes over-determined. + fn standalone_vps(vps_id: u8, max_layers_minus1: u8) -> VpsSource { + VpsSource::Parsed(Rc::new(Vps { + video_parameter_set_id: vps_id, + max_layers_minus1, + ..Default::default() + })) + } + + #[test] + fn changed_vps_content_under_a_stored_id_recreates_with_the_sps_and_pps_untouched() { + let sps = authored_sps(0, 0, 64); + let pps = authored_pps(&sps, 0, 0); + let vps = standalone_vps(0, 0); + let mut ledger = ParamsLedgerH265::default(); + let action = ledger.plan(&vps, &sps, &pps); + ledger.commit(action, &vps, &sps, &pps); + assert_eq!(ledger.plan(&vps, &sps, &pps), ParamsActionH265::Current); + + // Same VPS id, different content: SPS and PPS are byte-identical and + // would be `Current` on their own, yet the object still has to be + // rebuilt — Vulkan has no in-place replacement for a stored set. + let vps2 = standalone_vps(0, 1); + assert_eq!(vps2.id(), vps.id()); + assert_ne!(vps2, vps); + assert_eq!(ledger.plan(&vps2, &sps, &pps), ParamsActionH265::Recreate); + + // A NEW vps id over the same SPS/PPS is an Add of the VPS alone. + let vps3 = standalone_vps(1, 0); + assert_eq!( + ledger.plan(&vps3, &sps, &pps), + ParamsActionH265::Add { + add_vps: true, + add_sps: false, + add_pps: false + } + ); + } + + #[test] + fn changed_sps_or_pps_content_under_a_stored_id_recreates_and_resets_the_sequence() { + let sps = with_vps(&authored_sps(0, 0, 64), 0, 0); + let vps = VpsSource::for_sps(&sps); + let pps = authored_pps(&sps, 0, 0); + let mut ledger = ParamsLedgerH265::default(); + let action = ledger.plan(&vps, &sps, &pps); + ledger.commit(action, &vps, &sps, &pps); + assert_eq!(ledger.next_update_seq(), 2, "one Add happened"); + + // Same PPS id, different content (init qp changed). + let pps2 = authored_pps(&sps, 0, 4); + let action = ledger.plan(&vps, &sps, &pps2); + assert_eq!(action, ParamsActionH265::Recreate); + ledger.commit(action, &vps, &sps, &pps2); + assert_eq!(ledger.next_update_seq(), 1); + assert_eq!(ledger.plan(&vps, &sps, &pps2), ParamsActionH265::Current); + + // Same SPS id, different content (a resize the extent check would also + // catch — but the ledger must not depend on that). + let sps2 = with_vps(&authored_sps(0, 0, 128), 0, 0); + let vps2 = VpsSource::for_sps(&sps2); + let pps3 = authored_pps(&sps2, 0, 4); + assert_eq!(ledger.plan(&vps2, &sps2, &pps3), ParamsActionH265::Recreate); + } + + #[test] + fn capacity_overflow_on_any_of_the_three_arrays_recreates_with_just_the_current_triple() { + let sps = with_vps(&authored_sps(0, 0, 64), 0, 0); + let vps = VpsSource::for_sps(&sps); + let mut ledger = ParamsLedgerH265::default(); + + // Fill the PPS capacity under one VPS/SPS pair. + let first = authored_pps(&sps, 0, 0); + let action = ledger.plan(&vps, &sps, &first); + ledger.commit(action, &vps, &sps, &first); + for pps_id in 1..MAX_STD_PPS as u8 { + let pps = authored_pps(&sps, pps_id, 0); + let action = ledger.plan(&vps, &sps, &pps); + assert!(matches!(action, ParamsActionH265::Add { .. })); + ledger.commit(action, &vps, &sps, &pps); + } + assert_eq!(ledger.next_update_seq() - 1, MAX_STD_PPS as u32); + + // One past capacity: recreate; afterwards the evicted first PPS re-Adds + // (and the VPS/SPS with it — the recreate kept only the current triple). + let overflow = authored_pps(&sps, MAX_STD_PPS as u8, 0); + let action = ledger.plan(&vps, &sps, &overflow); + assert_eq!(action, ParamsActionH265::Recreate); + ledger.commit(action, &vps, &sps, &overflow); + assert_eq!( + ledger.plan(&vps, &sps, &first), + ParamsActionH265::Add { + add_vps: false, + add_sps: false, + add_pps: true + }, + "sets evicted by a recreate re-add on next activation" + ); + + // The VPS array overflows the same way, and on its own: MAX_STD_VPS + // distinct ids fit over one fixed SPS/PPS pair, the next one recreates. + let mut ledger = ParamsLedgerH265::default(); + let sps = authored_sps(0, 0, 64); + let pps = authored_pps(&sps, 0, 0); + for vps_id in 0..MAX_STD_VPS as u8 { + let vps = standalone_vps(vps_id, 0); + let action = ledger.plan(&vps, &sps, &pps); + assert!(matches!( + action, + ParamsActionH265::Add { add_vps: true, .. } + )); + ledger.commit(action, &vps, &sps, &pps); + } + let overflow_vps = standalone_vps(MAX_STD_VPS as u8, 0); + assert_eq!( + ledger.plan(&overflow_vps, &sps, &pps), + ParamsActionH265::Recreate, + "a fifth VPS id overflows the array even though SPS and PPS are current" + ); + } +} diff --git a/crates/pf-vkdecode/src/slots.rs b/crates/pf-vkdecode/src/slots.rs new file mode 100644 index 00000000..d7f1de21 --- /dev/null +++ b/crates/pf-vkdecode/src/slots.rs @@ -0,0 +1,284 @@ +//! The hardware DPB slot ledger: [`pf_bitstream::h264::PicId`]s mapped to the slot +//! indices a Vulkan Video session binds DPB images by. +//! +//! Division of labour: pf-bitstream's DPB runs the 8.2.5/C.4.5.3 processes and +//! DECIDES which pictures live and die — this map only translates its verdicts into +//! stable slot indices. It therefore never evicts on its own: running out of slots is +//! an error ([`SlotError::Full`]), because it can only mean removals were missed, and +//! a silent eviction would hide that bug behind corrupted output. + +use pf_bitstream::h264::DpbUpdate; +use pf_bitstream::h264::PicId; +use tracing::trace; + +/// The H.264 slot ceiling: 16 reference frames plus the picture being decoded. +const MAX_SLOTS: usize = 17; + +/// What went wrong with a slot operation. Both variants are caller bugs, not stream +/// conditions — pf-bitstream degrades stream damage to warnings long before here. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SlotError { + /// No free slot. The map is sized to `max_dpb_frames + 1`, which the planner's + /// DPB never exceeds; overflow means this map missed `removed` entries. + Full { capacity: usize }, + /// The id already holds a slot; ids are per-picture and never re-assigned. + AlreadyAssigned { id: PicId, slot: u8 }, +} + +impl std::fmt::Display for SlotError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SlotError::Full { capacity } => { + write!( + f, + "all {capacity} DPB slots are held — removals were missed" + ) + } + SlotError::AlreadyAssigned { id, slot } => { + write!(f, "picture {id} already holds slot {slot}") + } + } + } +} + +impl std::error::Error for SlotError {} + +/// The slot ledger. One per decode session; feed it every [`DpbUpdate`] in decode +/// order (via [`Self::apply`] or `plan_to_vk`, which applies internally). +/// +/// Invariants (unit-tested): +/// - a [`PicId`] keeps its slot from [`Self::assign`] until [`Self::release`]; +/// - a slot is reused only after its holder is released; +/// - assigning past capacity errors instead of evicting. +/// +/// Slots are pure planner bookkeeping: consumers never hold a SLOT (the decoder's +/// picture pool decouples IMAGES from slots — a re-activated slot binds a fresh +/// free image, so a delivered picture's image is never a decode target while the +/// consumer reads it). +#[derive(Debug, Clone)] +pub struct SlotMap { + /// `slots[i]` holds the id bound to slot `i`, `None` while the slot is free. + slots: Vec>, +} + +impl SlotMap { + /// Sized from [`pf_bitstream::h264::PicturePlan::max_dpb_frames`] plus one for + /// the picture being decoded (its setup slot coexists with a full reference + /// window). + /// + /// pf-bitstream's envelope gate rejects any SPS asking for a DPB deeper than the + /// spec's 16 frames before a plan exists, so a larger request here is a caller + /// bug — debug-asserted, never silently clamped (a clamp would turn the bug into + /// silent evictions later). + pub fn new(max_dpb_frames: usize) -> Self { + debug_assert!( + max_dpb_frames < MAX_SLOTS, + "a {max_dpb_frames}-frame DPB exceeds the H.264 ceiling pf-bitstream's \ + envelope gate enforces" + ); + Self { + slots: vec![None; max_dpb_frames + 1], + } + } + + /// Total slot count (fixed at construction). + pub fn capacity(&self) -> usize { + self.slots.len() + } + + /// Slots currently held. + pub fn active(&self) -> usize { + self.slots.iter().filter(|slot| slot.is_some()).count() + } + + /// The held slots as `(slot, id)` pairs, in slot order — WP-B walks this to + /// build `VkVideoReferenceSlotInfoKHR` bindings and to map slots back to their + /// images. + pub fn held(&self) -> impl Iterator + '_ { + self.slots + .iter() + .enumerate() + // The envelope-gated capacity (<= 17) keeps every index within u8. + .filter_map(|(index, slot)| slot.map(|id| (index as u8, id))) + } + + /// Bind `id` to the lowest free slot. + pub fn assign(&mut self, id: PicId) -> Result { + if let Some(slot) = self.slot_of(id) { + return Err(SlotError::AlreadyAssigned { id, slot }); + } + let free = self + .slots + .iter() + .position(Option::is_none) + .ok_or(SlotError::Full { + capacity: self.slots.len(), + })?; + self.slots[free] = Some(id); + // The envelope-gated capacity (<= 17) keeps every index within u8. + Ok(free as u8) + } + + /// The slot `id` holds, if any. + pub fn slot_of(&self, id: PicId) -> Option { + self.slots + .iter() + .position(|slot| *slot == Some(id)) + // The envelope-gated capacity (<= 17) keeps every index within u8. + .map(|index| index as u8) + } + + /// Free `id`'s slot. Returns whether the id held one. + /// + /// Slot lifetime is DPB RESIDENCY: a picture holds its slot for exactly as long + /// as the planner's DPB holds the picture — as a reference OR as a decoded + /// picture awaiting output — and that residency ends only when a + /// [`DpbUpdate::removed`] entry reports it. This method is that report's + /// primitive: `plan_to_vk` and [`Self::apply`] call it with the planner's + /// `removed` ids and nothing else may release a slot. + /// + /// Releasing is CPU-side bookkeeping (the slot becomes assignable to a later + /// picture); keeping the released slot's IMAGE out of reuse until in-flight + /// decodes complete is the backend's synchronization, not this ledger's. + pub fn release(&mut self, id: PicId) -> bool { + match self.slots.iter().position(|slot| *slot == Some(id)) { + Some(index) => { + self.slots[index] = None; + true + } + None => false, + } + } + + /// Apply one [`DpbUpdate`]: release every `removed` id. + /// + /// `outputs` is deliberately ignored: output-readiness is display sequencing, + /// not the end of DPB residency — a display-ready picture can still be a + /// reference (its slot stays), and only its later `removed` entry frees the + /// slot. + pub fn apply(&mut self, update: &DpbUpdate) { + for &id in &update.removed { + if !self.release(id) { + // Tolerated but never silent: reachable only when the caller skipped + // feeding an AU's plan through this map. + trace!(id, "DpbUpdate removed an id this SlotMap never assigned"); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_pic_id_keeps_its_slot_until_released_and_the_slot_is_then_reusable() { + let mut slots = SlotMap::new(3); // capacity 4 + let s0 = slots.assign(10).unwrap(); + let s1 = slots.assign(11).unwrap(); + assert_ne!(s0, s1); + + // Stable across unrelated churn. + assert_eq!(slots.slot_of(10), Some(s0)); + slots.release(11); + assert_eq!(slots.slot_of(10), Some(s0)); + assert_eq!(slots.slot_of(11), None); + + // The freed slot is reusable; the held one is not. + let s2 = slots.assign(12).unwrap(); + assert_eq!(s2, s1, "the lowest free slot is the released one"); + assert_eq!(slots.slot_of(10), Some(s0)); + assert_eq!(slots.active(), 2); + } + + #[test] + fn assigning_past_capacity_is_an_error_never_a_silent_eviction() { + let mut slots = SlotMap::new(1); // capacity 2 + slots.assign(1).unwrap(); + slots.assign(2).unwrap(); + assert_eq!(slots.assign(3), Err(SlotError::Full { capacity: 2 })); + // The failed assign evicted nothing. + assert_eq!(slots.slot_of(1), Some(0)); + assert_eq!(slots.slot_of(2), Some(1)); + } + + #[test] + fn re_assigning_a_held_id_is_an_error_not_a_move() { + let mut slots = SlotMap::new(2); + let s = slots.assign(7).unwrap(); + assert_eq!( + slots.assign(7), + Err(SlotError::AlreadyAssigned { id: 7, slot: s }) + ); + assert_eq!(slots.active(), 1); + } + + #[test] + fn capacity_is_dpb_frames_plus_one_for_the_setup_slot() { + assert_eq!(SlotMap::new(16).capacity(), 17); + assert_eq!(SlotMap::new(4).capacity(), 5); + } + + #[test] + #[should_panic(expected = "envelope")] + fn a_dpb_past_the_h264_ceiling_is_a_debug_panic_not_a_clamp() { + // pf-bitstream's envelope gate makes this unreachable from a real stream; + // reaching it means a caller bypassed the planner. + let _ = SlotMap::new(17); + } + + #[test] + fn held_lists_slot_id_pairs_in_slot_order() { + let mut slots = SlotMap::new(3); + slots.assign(10).unwrap(); + slots.assign(11).unwrap(); + slots.assign(12).unwrap(); + slots.release(11); + assert_eq!(slots.held().collect::>(), vec![(0, 10), (2, 12)]); + } + + #[test] + fn apply_releases_removed_ids_and_ignores_outputs() { + let mut slots = SlotMap::new(3); + slots.assign(1).unwrap(); + slots.assign(2).unwrap(); + slots.apply(&DpbUpdate { + stored: None, + outputs: vec![1], // display-ready, still a reference: must keep its slot + removed: vec![2], + }); + assert_eq!(slots.slot_of(1), Some(0)); + assert_eq!(slots.slot_of(2), None); + } + + #[test] + fn a_hundred_synthetic_dpb_updates_churn_without_aliasing_a_slot() { + // A sliding window of 4 references over 100 pictures: each id's slot must + // stay fixed while it lives, and no two live ids may ever share a slot. + let mut slots = SlotMap::new(4); + let mut recorded: Vec<(PicId, u8)> = Vec::new(); + for id in 0u64..100 { + let slot = slots.assign(id).unwrap(); + assert!( + recorded.iter().all(|&(_, held)| held != slot), + "assign handed out a slot a live picture still holds" + ); + recorded.push((id, slot)); + + let removed = if id >= 4 { vec![id - 4] } else { Vec::new() }; + slots.apply(&DpbUpdate { + stored: Some(id), + outputs: vec![id], + removed: removed.clone(), + }); + for gone in removed { + recorded.retain(|&(held_id, _)| held_id != gone); + } + // Every live picture still holds exactly the slot it was assigned. + for &(live, slot) in &recorded { + assert_eq!(slots.slot_of(live), Some(slot)); + } + assert!(slots.active() <= 5); + } + } +} diff --git a/crates/pf-vkdecode/tests/common/mod.rs b/crates/pf-vkdecode/tests/common/mod.rs new file mode 100644 index 00000000..3cd04d03 --- /dev/null +++ b/crates/pf-vkdecode/tests/common/mod.rs @@ -0,0 +1,636 @@ +//! Shared Vulkan Video bring-up for the `#[ignore]`d GPU legs. +//! +//! `tests/gpu_smoke.rs` and `tests/gpu_parity.rs` each drive THREE codecs, and the +//! path from "a Vulkan loader exists" to "a [`DeviceHandles`] a decoder can be +//! constructed on" is the same ~150 unsafe lines every time: loader → instance → +//! pick a physical device whose queue families carry the codec's decode ops → +//! logical device with the decode extensions plus `timelineSemaphore` and +//! `synchronization2`. Six copies of that would be six places for a +//! fleet-only failure to hide, so it lives here once, parameterised by the one +//! thing that genuinely differs between the callers ([`Graphics`]: the parity +//! legs read back on a graphics queue and so REQUIRE one, while the smoke legs +//! accept a decode-only device and fall back to the decode family — which also +//! decides whether pool images end up EXCLUSIVE or CONCURRENT, so it is not +//! cosmetic). [`Request::report_families`] exists so a caller CAN suppress the +//! per-family table, but every leg currently asks for it: it is the first +//! thing a fleet failure report needs, and it is a physical-device property +//! query, never a recorded RESULT_STATUS query, so it cannot trip the RADV VCN +//! hang. +//! +//! Cargo does not treat `tests/common/mod.rs` as a test target of its own (it +//! auto-discovers `tests/*.rs` and `tests/*/main.rs` only), so this file is +//! compiled purely as a `mod common;` of each test binary — and therefore under +//! each one's `#![deny(clippy::undocumented_unsafe_blocks)]`. +//! +//! Environment knobs, honoured exactly as they were before this module existed: +//! - `PF_VKD_SMOKE_VENDOR` (hex `0x1002`/`0x10de`, or decimal): pin a PCI vendor +//! on a multi-GPU box, so a run is attributable to one driver instead of +//! whichever device enumerated first. +//! - RADV additionally needs `RADV_PERFTEST=video_decode` in the environment; +//! without it no device advertises the decode extensions and [`bring_up`] +//! panics with "no physical device with VK_KHR_video_decode_*", which is the +//! correct report rather than a confusing later failure. + +// Each of the two test binaries drives a different subset of this module (the +// smoke legs never touch `Setup::pd`, the parity legs never pass +// `Graphics::DecodeFamilyIsFine`), and a test binary gets no `pub` exemption +// from dead-code analysis. +#![allow(dead_code)] + +use std::io::Cursor; + +use ash::vk; +use ash::vk::Handle; +use pf_vkdecode::DecodeStatus; +use pf_vkdecode::DecodedVkFrame; +use pf_vkdecode::DeviceHandles; +use pf_vkdecode::VkDecodeError; + +/// The vendored H.264 vector both GPU legs decode: 250 AUs of real encoder +/// output, 320x240 — the same file pf-bitstream's WP-A tests plan, at the same +/// relative path. It is **two slice NALUs per picture** (500 slice NALs over 250 +/// AUs, with 4 IDRs), which is why the splitter's `first_mb_in_slice == 0` branch +/// is load-bearing here rather than decorative — and, per libavcodec's own DXVA +/// slice-control descriptors captured on hardware, why its slice-control buffer +/// is two records wide where the HEVC vector's is one. +pub const TEST_25FPS_H264: &[u8] = include_bytes!( + "../../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264" +); + +/// The vendored H.265 twin: 250 AUs, 320x240 Main 8-bit 4:2:0, one IDR_N_LP then +/// 249 TRAIL pictures (verified by pf-bitstream's +/// `the_full_25fps_vector_plans_every_picture_and_every_pic_id_reaches_output`). +pub const TEST_25FPS_H265: &[u8] = include_bytes!( + "../../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265" +); + +/// The vendored AV1 twin: 320x240 Main 4:2:0 8-bit, no film grain, **250 temporal +/// units carrying 274 coded frames** — 24 units carry two frames each, and those 24 +/// extras are HIDDEN (decoded, referenced, never shown; the vector contains no +/// `show_existing_frame` at all). 250 frames are displayed, which is what the rung +/// delivers and what `data/test-25fps-av1.nv12.sha256` hashes. +/// +/// Note the file name: it is `test-25fps.ivf.av1`, not `test-25fps.av1.ivf` — the +/// directory holds BOTH, byte-identical, and only the former has the `.md5`/`.crc` +/// reference hashes beside it. pf-bitstream's planner tests include this one. +pub const TEST_25FPS_AV1: &[u8] = include_bytes!( + "../../../pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1" +); + +/// Split the AV1 vector into access units — one IVF packet per TEMPORAL UNIT. +/// +/// Unlike the two Annex-B splitters below there is nothing to re-derive here: AV1 +/// carries no start codes, so an access unit is not something a scan recovers from +/// the elementary stream — it is the container's framing, and the vector's container +/// is IVF. `IvfIterator` is the vendored parser's own reader (the same one +/// pf-bitstream's AV1 planner tests walk), so this is a rename for symmetry with +/// [`split_h264_aus`]/[`split_h265_aus`] rather than a second implementation that +/// could disagree with production. +/// +/// The consequence for the parity legs is worth stating: AV1 has **no prefix-width +/// leg** and needs none. The four-byte-start-code hazard that made HEVC unplayable +/// on every driver (see [`h265_four_byte_start_codes`]) simply has no AV1 +/// counterpart — OBUs are length-delimited, the host hands whole temporal units +/// across, and there is no prefix for a driver to mis-skip. Its absence here is +/// deliberate, not an omission. +pub fn split_av1_aus(stream: &[u8]) -> Vec<&[u8]> { + cros_codecs::bitstream_utils::IvfIterator::new(stream).collect() +} + +/// Test-only H.264 AU splitter, mirroring pf-bitstream's +/// (`#[cfg(test)]`-private there): a new AU starts at a non-VCL NALU following +/// slices, or at a slice whose `first_mb_in_slice` is 0 (the first bit of the byte +/// after the 1-byte NAL header) when the current AU already has slices. +pub fn split_h264_aus(stream: &[u8]) -> Vec<&[u8]> { + use cros_codecs::codec::h264::parser::Nalu; + use cros_codecs::codec::h264::parser::NaluType; + + let mut aus = Vec::new(); + let mut cursor = Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + + while let Ok(nalu) = Nalu::next(&mut cursor) { + let nalu_offset = cursor.position() as usize; + let start = nalu_offset - nalu.offset; + let is_slice = matches!(nalu.header.type_, NaluType::Slice | NaluType::SliceIdr); + let first_mb_zero = is_slice && stream.get(nalu_offset + 1).is_some_and(|b| b & 0x80 != 0); + + if au_has_slice && (!is_slice || first_mb_zero) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus +} + +/// Test-only H.265 AU splitter, a verbatim copy of pf-bitstream's +/// (`#[cfg(test)]`-private in `h265.rs`, and the same one `pic_h265`'s and +/// `fault_detection`'s tests carry). +/// +/// The two differences from [`split_h264_aus`] are the whole point and are why +/// this is copied rather than re-derived: HEVC's NAL header is TWO bytes, so +/// `first_slice_segment_in_pic_flag` is the top bit of `stream[header_start + 2]` +/// (H.264 reads `+ 1`), and "is a slice" is the numeric range `nal_unit_type < 32` +/// rather than an enum pair. Getting either wrong silently merges or splits AUs, +/// which shows up as a frame-count mismatch a long way from its cause. +pub fn split_h265_aus(stream: &[u8]) -> Vec<&[u8]> { + use cros_codecs::codec::h265::parser::Nalu; + + let mut aus = Vec::new(); + let mut cursor = Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + + while let Ok(nalu) = Nalu::next(&mut cursor) { + let header_start = cursor.position() as usize; + let start = header_start - nalu.offset; + let is_slice = (nalu.header.type_ as u32) < 32; + let first_slice_flag = + is_slice && stream.get(header_start + 2).is_some_and(|b| b & 0x80 != 0); + + if au_has_slice && (!is_slice || first_slice_flag) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus +} + +/// Rewrite every Annex-B start code in `stream` to the FOUR-byte form +/// (`00 00 00 01`), leaving each NAL's payload bytes untouched. +/// +/// # Why the suite needs this +/// +/// Both vendored vectors use THREE-byte start codes throughout, while the real +/// host emits FOUR-byte ones on **100% of access units, both codecs** — measured +/// off the M0 NVENC corpus through the capture hook's `.idx` offsets: 1514/1514 +/// H.264 AUs and 1133/1133 HEVC. So without this, every parity verdict this +/// program has recorded was taken on a prefix form that never ships. +/// +/// That gap was not theoretical. Submitting four-byte start codes to +/// `vkCmdDecodeVideoKHR` is exactly what made HEVC unplayable on every driver +/// tested: drivers are validated on the three-byte form, and a fixed `+3 + 2` +/// skip into a four-byte-prefixed slice lands a byte early and reads a nonsense +/// `pps_id`. The cure lives in `ring::pack_slices`, which trims the leading zero +/// byte and derives the slice offsets from the trimmed lengths in one call. +/// H.264 was safe here only by its vendored encoder's convention, never by +/// structure — which is why the normalisation is shared and so is this helper. +/// +/// # What it preserves +/// +/// The payload copied is `nalu.data[nalu.offset..]`: exactly the `nal_size` +/// bytes the parser itself hands the planner. `Nalu::next` already discards +/// `trailing_zero_8bits` before the following start code, so the NALs in the +/// output are the NALs the production parser sees in the input, and the ONLY +/// difference between the two streams is the width of every prefix. +/// +/// Generic over the NAL header because both codecs share one `Nalu` type. The +/// AU splitters above cannot be shared for the opposite reason: their AU +/// boundary rules genuinely differ. +fn four_byte_start_codes(stream: &[u8]) -> Vec +where + H: cros_codecs::codec::h264::nalu::Header + std::fmt::Debug, +{ + use cros_codecs::codec::h264::nalu::Nalu; + + // A lower bound, not the answer: the output gains a byte per three-byte + // prefix and loses any trailing zeroes. + let mut out = Vec::with_capacity(stream.len()); + let mut cursor = Cursor::new(stream); + while let Ok(nalu) = Nalu::::next(&mut cursor) { + out.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); + out.extend_from_slice(&nalu.data[nalu.offset..]); + } + out +} + +/// [`four_byte_start_codes`] bound to H.264's NAL header. +pub fn h264_four_byte_start_codes(stream: &[u8]) -> Vec { + four_byte_start_codes::(stream) +} + +/// [`four_byte_start_codes`] bound to H.265's NAL header. +pub fn h265_four_byte_start_codes(stream: &[u8]) -> Vec { + four_byte_start_codes::(stream) +} + +/// The slice of a decoder's surface the GPU legs drive. +/// +/// `VkH264Decoder`, `VkH265Decoder` and `VkAv1Decoder` expose it method-for-method +/// (the crate docs say so deliberately) but share no trait — codec DISPATCH is the +/// client wiring's job, not this crate's. Binding it here lets each GPU leg run ONE +/// body against all three codecs, which is the only way "the AV1 leg proves the same +/// thing the H.264 leg does" can be a fact instead of a claim about three hand-copied +/// functions. +pub trait TestDecoder { + fn decode(&mut self, au: &[u8]) -> Result, VkDecodeError>; + fn take_ready(&mut self) -> Option; + fn wait_status(&mut self, frame: &DecodedVkFrame) -> DecodeStatus; + fn release_frame( + &mut self, + frame: &DecodedVkFrame, + presenter_signaled: bool, + ) -> Result<(), VkDecodeError>; + fn flush(&mut self); + fn status_queries(&self) -> bool; + fn debug_snapshot(&self) -> String; +} + +/// Forwarding impl — one macro so the three decoders can never drift into being +/// driven differently by accident. +macro_rules! impl_test_decoder { + ($ty:ty) => { + impl TestDecoder for $ty { + fn decode(&mut self, au: &[u8]) -> Result, VkDecodeError> { + <$ty>::decode(self, au) + } + fn take_ready(&mut self) -> Option { + <$ty>::take_ready(self) + } + fn wait_status(&mut self, frame: &DecodedVkFrame) -> DecodeStatus { + <$ty>::wait_status(self, frame) + } + fn release_frame( + &mut self, + frame: &DecodedVkFrame, + presenter_signaled: bool, + ) -> Result<(), VkDecodeError> { + <$ty>::release_frame(self, frame, presenter_signaled) + } + fn flush(&mut self) { + <$ty>::flush(self); + } + fn status_queries(&self) -> bool { + <$ty>::status_queries(self) + } + fn debug_snapshot(&self) -> String { + <$ty>::debug_snapshot(self) + } + } + }; +} + +impl_test_decoder!(pf_vkdecode::VkH264Decoder); +impl_test_decoder!(pf_vkdecode::VkH265Decoder); +impl_test_decoder!(pf_vkdecode::VkAv1Decoder); + +/// Serializes the GPU legs within one test binary. Hold it for the whole leg. +/// +/// Cargo runs a binary's tests on PARALLEL threads by default. While each GPU test +/// file held exactly one test that never mattered; with one leg per codec it +/// matters twice over: +/// +/// - two decoders would contend for the same decode queue and double peak video +/// memory on a device that may not have it, turning an attribution run into a +/// race and any failure into something nobody can pin on a codec; +/// - the parity legs set `PF_VKD_TEST_READBACK` through `std::env::set_var`, which +/// is not thread-safe and would be racing a second leg's reads of it. +/// +/// Poisoning is deliberately ignored: if the first leg panics, the second must +/// still run and report its own codec's verdict rather than fail as a casualty. +pub fn gpu_lock() -> std::sync::MutexGuard<'static, ()> { + static GPU: std::sync::Mutex<()> = std::sync::Mutex::new(()); + GPU.lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +/// Which codec a bring-up must find silicon (and an extension) for. +#[derive(Clone, Copy)] +pub struct Codec { + /// The video codec operation the chosen decode queue family must advertise. + /// Checked per FAMILY, not per device: a device can advertise the extension + /// while only one of its families carries the op. + pub op: vk::VideoCodecOperationFlagsKHR, + /// The codec's device extension — required on the physical device AND enabled + /// on the logical one, per [`DeviceHandles`]' contract (a decoder whose + /// extension was not enabled reaches `vkCreateVideoSessionKHR` on an + /// unenabled codec). + pub extension: &'static std::ffi::CStr, +} + +/// H.264 decode (`VkH264Decoder`). +pub const H264: Codec = Codec { + op: vk::VideoCodecOperationFlagsKHR::DECODE_H264, + extension: ash::khr::video_decode_h264::NAME, +}; + +/// H.265 decode (`VkH265Decoder`). +pub const H265: Codec = Codec { + op: vk::VideoCodecOperationFlagsKHR::DECODE_H265, + extension: ash::khr::video_decode_h265::NAME, +}; + +/// AV1 decode (`VkAv1Decoder`). +/// +/// The narrowest of the three on the fleet: `VK_KHR_video_decode_av1` only reached +/// core drivers in 2024, so a box that decodes both H.26x codecs may still report +/// "no physical device with VK_KHR_video_decode_av1", which is a fact about the box. +/// On RADV the extension is additionally behind `RADV_PERFTEST=video_decode`, the +/// same knob the other two need. +pub const AV1: Codec = Codec { + op: vk::VideoCodecOperationFlagsKHR::DECODE_AV1, + extension: ash::khr::video_decode_av1::NAME, +}; + +/// What a caller needs from the GRAPHICS queue family — the one behavioural +/// difference between the smoke and parity bring-ups, an explicit parameter so +/// it cannot drift back into being an accident of two copied loops. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Graphics { + /// A device with no graphics family is still usable: `graphics_qf` falls back + /// to the decode family. The smoke legs want this — they submit nothing + /// outside the decoder. The fallback is not cosmetic: `decode_qf == + /// graphics_qf` makes the picture pool's images EXCLUSIVE rather than + /// CONCURRENT (`DecodeDevice::sharing_families`), which is exactly the + /// arrangement a decode-only device has to run. + DecodeFamilyIsFine, + /// A device without a graphics family is SKIPPED, not defaulted. The parity + /// legs want this — their readback records `vkCmdCopyImageToBuffer` on the + /// graphics queue. + Required, +} + +/// One bring-up request. A struct rather than positional arguments because the +/// two fields below are precisely where the callers disagree, and a bare +/// `bring_up(H265, true, false)` at a call site is how that disagreement becomes +/// invisible again. +pub struct Request { + /// The codec whose decode ops and extension are required. + pub codec: Codec, + /// Whether a graphics queue family is required or merely preferred. + pub graphics: Graphics, + /// Print each candidate device's per-family `flags / video_ops / + /// query_result_status` table. It is the first thing a fleet failure report + /// needs: which families exist, which of them decode this codec, and whether + /// per-op status verdicts exist on this box at all (RADV: they do not, and + /// recording one hangs the VCN — the 2026-08 .25 lesson). + pub report_families: bool, +} + +/// A live instance + logical device a decoder can be constructed on. +/// +/// Torn down explicitly through [`Setup::destroy`] rather than `Drop`, so the +/// ordering against the decoder — which must be gone FIRST — stays visible in the +/// test body, exactly as it was when each test carried its own teardown. +pub struct Setup { + /// Kept because [`Setup::handles`] hands the loader's + /// `vkGetInstanceProcAddr` to the decoder, which resolves everything through + /// it. + pub entry: ash::Entry, + pub instance: ash::Instance, + pub pd: vk::PhysicalDevice, + pub device: ash::Device, + pub decode_qf: u32, + /// The graphics family, or `decode_qf` under + /// [`Graphics::DecodeFamilyIsFine`] when the device has none. + pub graphics_qf: u32, +} + +impl Setup { + /// The borrowed-handle bundle both decoders are constructed from. Valid only + /// while `self` is alive and un-destroyed ([`DeviceHandles`]' contract). + pub fn handles(&self) -> DeviceHandles { + DeviceHandles { + get_instance_proc_addr: self.entry.static_fn().get_instance_proc_addr as usize, + instance: self.instance.handle().as_raw() as usize, + physical_device: self.pd.as_raw() as usize, + device: self.device.handle().as_raw() as usize, + decode_qf: self.decode_qf, + decode_queue_index: 0, + graphics_qf: self.graphics_qf, + } + } + + /// # Safety + /// + /// Every object created from this device — the decoder's session/pools, any + /// readback handles — is already destroyed, and no [`DeviceHandles`] taken + /// from [`Setup::handles`] is still in use. + pub unsafe fn destroy(self) { + // SAFETY: fn contract — nothing derived from these handles survives, so + // the device can be destroyed and then the instance it came from. + unsafe { + self.device.destroy_device(None); + self.instance.destroy_instance(None); + } + // Deliberately LEAK the loader. `ash::Entry` owns an `Arc`, so + // dropping it `dlclose`/`FreeLibrary`s the Vulkan loader together with + // every ICD and implicit layer. That was harmless while each test binary + // held a single GPU leg (the unload was immediately followed by process + // exit), but each binary now holds two legs serialized by `gpu_lock`, so + // the second leg would re-`dlopen` a loader the first just tore down — + // a documented way to fault inside `Entry::load` or to take a signal at + // exit AFTER both legs reported ok, which reads as a decoder defect. + // The process is about to end regardless, so leaking is free. + std::mem::forget(self.entry); + } +} + +/// The optional `PF_VKD_SMOKE_VENDOR` pin: multi-GPU boxes enumerate several +/// decode-capable devices and first-match hides all but one — the pin makes a run +/// attributable to a specific vendor's driver. +fn vendor_pin() -> Option { + std::env::var("PF_VKD_SMOKE_VENDOR").ok().map(|raw| { + let trimmed = raw.trim(); + trimmed + .strip_prefix("0x") + .or_else(|| trimmed.strip_prefix("0X")) + .map_or_else(|| trimmed.parse(), |hex| u32::from_str_radix(hex, 16)) + .unwrap_or_else(|_| panic!("PF_VKD_SMOKE_VENDOR is not a PCI vendor id: {raw:?}")) + }) +} + +/// Loader → instance → a physical device that can decode `request.codec` → +/// logical device with the decode extensions and `timelineSemaphore` + +/// `synchronization2`. +/// +/// Panics (the test harness's only failure channel) with the reason when the box +/// cannot host the request; the message names the codec's OWN extension, so a box +/// with H.264 silicon but no H.265 says exactly that rather than "no Vulkan +/// Video". +pub fn bring_up(request: &Request) -> Setup { + // ---- instance ---- + // SAFETY: loads the system Vulkan loader; no Vulkan objects exist yet. + let entry = unsafe { ash::Entry::load() }.expect("a Vulkan loader on this box"); + let app = vk::ApplicationInfo::default().api_version(vk::make_api_version(0, 1, 3, 0)); + let instance_ci = vk::InstanceCreateInfo::default().application_info(&app); + // SAFETY: valid create info rooted in locals; the instance is destroyed by + // `Setup::destroy` after everything created from it. + let instance = + unsafe { entry.create_instance(&instance_ci, None) }.expect("create a Vulkan 1.3 instance"); + + let vendor_filter = vendor_pin(); + + // ---- physical device with a decode queue family for this codec ---- + // SAFETY: live instance. + let physical_devices = + unsafe { instance.enumerate_physical_devices() }.expect("enumerate physical devices"); + let mut picked: Option<(vk::PhysicalDevice, u32, u32)> = None; + for pd in physical_devices { + // SAFETY: `pd` was just enumerated from this instance. + let props = unsafe { instance.get_physical_device_properties(pd) }; + if vendor_filter.is_some_and(|vendor| props.vendor_id != vendor) { + continue; + } + // SAFETY: `pd` was just enumerated from this instance. + let ext_props = + unsafe { instance.enumerate_device_extension_properties(pd) }.unwrap_or_default(); + let has = |name: &std::ffi::CStr| { + ext_props.iter().any(|e| { + e.extension_name_as_c_str() + .is_ok_and(|extension| extension == name) + }) + }; + if !(has(ash::khr::video_queue::NAME) + && has(ash::khr::video_decode_queue::NAME) + && has(request.codec.extension)) + { + continue; + } + // SAFETY: live physical device; the two-call form fills the chained video + // properties for each family. + let family_count = unsafe { instance.get_physical_device_queue_family_properties2_len(pd) }; + let mut video_props = vec![vk::QueueFamilyVideoPropertiesKHR::default(); family_count]; + let mut families: Vec> = video_props + .iter_mut() + .map(|v| vk::QueueFamilyProperties2::default().push_next(v)) + .collect(); + // SAFETY: as above, arrays sized to the reported count. + unsafe { instance.get_physical_device_queue_family_properties2(pd, &mut families) }; + let flags_per_family: Vec = families + .iter() + .map(|f| f.queue_family_properties.queue_flags) + .collect(); + drop(families); // release the &mut borrows so video_props is readable + + // Each family's video ops + RESULT_STATUS query support (see + // `Request::report_families`) — printed per CANDIDATE device, so a + // multi-GPU box reports every device it considered. + if request.report_families { + let mut status_props = + vec![vk::QueueFamilyQueryResultStatusPropertiesKHR::default(); family_count]; + let mut families2: Vec> = status_props + .iter_mut() + .map(|s| vk::QueueFamilyProperties2::default().push_next(s)) + .collect(); + // SAFETY: as the query above. + unsafe { instance.get_physical_device_queue_family_properties2(pd, &mut families2) }; + drop(families2); + for (i, s) in status_props.iter().enumerate() { + eprintln!( + "family {i}: flags={:?} video_ops={:?} query_result_status={}", + flags_per_family[i], + video_props[i].video_codec_operations, + s.query_result_status_support != vk::FALSE, + ); + } + } + + let mut decode_qf = None; + let mut graphics_qf = None; + for (index, flags) in flags_per_family.iter().enumerate() { + if flags.contains(vk::QueueFlags::GRAPHICS) && graphics_qf.is_none() { + graphics_qf = Some(index as u32); + } + if flags.contains(vk::QueueFlags::VIDEO_DECODE_KHR) + && video_props[index] + .video_codec_operations + .contains(request.codec.op) + && decode_qf.is_none() + { + decode_qf = Some(index as u32); + } + } + match (request.graphics, decode_qf, graphics_qf) { + // A graphics queue is required and present. + (Graphics::Required, Some(decode), Some(graphics)) => { + picked = Some((pd, decode, graphics)); + break; + } + // Not required: fall back to the decode family (see the variant docs). + (Graphics::DecodeFamilyIsFine, Some(decode), graphics) => { + picked = Some((pd, decode, graphics.unwrap_or(decode))); + break; + } + // No decode family for this codec, or none of the graphics kind + // required — keep looking. + (Graphics::Required, _, None) | (_, None, _) => {} + } + } + let (pd, decode_qf, graphics_qf) = picked.unwrap_or_else(|| { + panic!( + "no physical device with {} and a decode queue{}{}", + request.codec.extension.to_string_lossy(), + match request.graphics { + Graphics::Required => " and a graphics queue", + Graphics::DecodeFamilyIsFine => "", + }, + match vendor_filter { + Some(vendor) => format!(" (PF_VKD_SMOKE_VENDOR pinned vendor 0x{vendor:04x})"), + None => String::new(), + }, + ) + }); + + // Attribution header: which device (and driver) this run actually exercised. + { + let mut driver_props = vk::PhysicalDeviceDriverProperties::default(); + let mut props2 = vk::PhysicalDeviceProperties2::default().push_next(&mut driver_props); + // SAFETY: live physical device; the chain fills the Vulkan 1.2 core + // driver-identity struct. + unsafe { instance.get_physical_device_properties2(pd, &mut props2) }; + let props = props2.properties; + eprintln!( + "picked: {:?} vendor=0x{:04x} driver={:?} info={:?}", + props.device_name_as_c_str().unwrap_or(c"?"), + props.vendor_id, + driver_props.driver_name_as_c_str().unwrap_or(c"?"), + driver_props.driver_info_as_c_str().unwrap_or(c"?"), + ); + } + + // ---- logical device: decode (+ graphics) queues, video + sync features ---- + let priorities = [1.0f32]; + let mut queue_infos = vec![vk::DeviceQueueCreateInfo::default() + .queue_family_index(decode_qf) + .queue_priorities(&priorities)]; + if graphics_qf != decode_qf { + queue_infos.push( + vk::DeviceQueueCreateInfo::default() + .queue_family_index(graphics_qf) + .queue_priorities(&priorities), + ); + } + let extensions = [ + ash::khr::video_queue::NAME.as_ptr(), + ash::khr::video_decode_queue::NAME.as_ptr(), + request.codec.extension.as_ptr(), + ]; + let mut features12 = vk::PhysicalDeviceVulkan12Features::default().timeline_semaphore(true); + let mut features13 = vk::PhysicalDeviceVulkan13Features::default().synchronization2(true); + let device_ci = vk::DeviceCreateInfo::default() + .queue_create_infos(&queue_infos) + .enabled_extension_names(&extensions) + .push_next(&mut features12) + .push_next(&mut features13); + // SAFETY: live physical device, valid create info rooted in locals; destroyed + // by `Setup::destroy` after the decoder drops. + let device = + unsafe { instance.create_device(pd, &device_ci, None) }.expect("create the decode device"); + + Setup { + entry, + instance, + pd, + device, + decode_qf, + graphics_qf, + } +} diff --git a/crates/pf-vkdecode/tests/data/test-25fps-av1.frame0.nv12 b/crates/pf-vkdecode/tests/data/test-25fps-av1.frame0.nv12 new file mode 100644 index 00000000..1f526a2b Binary files /dev/null and b/crates/pf-vkdecode/tests/data/test-25fps-av1.frame0.nv12 differ diff --git a/crates/pf-vkdecode/tests/data/test-25fps-av1.nv12.sha256 b/crates/pf-vkdecode/tests/data/test-25fps-av1.nv12.sha256 new file mode 100644 index 00000000..a333918d --- /dev/null +++ b/crates/pf-vkdecode/tests/data/test-25fps-av1.nv12.sha256 @@ -0,0 +1,304 @@ +# SHA-256 per DELIVERED frame of test-25fps.ivf.av1, DISPLAY order — 250 frames. +# +# Each frame is the 320x240 render region as tightly packed NV12: +# Y plane 320*240 bytes, then interleaved UV 320*120 bytes = 115200 bytes/frame. +# +# EIGHT-BIT NV12, not P010. The vector's sequence header carries +# `high_bitdepth = 0` / `mono_chrome = 0` / `seq_profile = 0`, i.e. Main 4:2:0 +# 8-bit, so the Vulkan pool is VK_FORMAT_G8_B8R8_2PLANE_420_UNORM and one byte +# per sample is the whole story here. (The ten-bit scar the sibling +# data/test-main10.p010.sha256 header records — P010's ten bits sit in the HIGH +# end of each 16-bit word, NOT yuv420p10le's low end — does not arise for this +# vector, but it is the first thing to check if an AV1 Main-10 golden is ever +# added beside this one.) +# +# NO FILM GRAIN. `film_grain_params_present = 0` in the sequence header, so the +# golden is grain-free and the Vulkan decode profile this is compared against is +# the film-grain-DISABLED one (`Av1ProfileKey::film_grain == false`; grain +# synthesis is part of the Vulkan decode PROFILE, not a per-frame toggle). A +# vector that gained grain would need its own golden AND a device that offers the +# grain-enabled profile — it would not merely change these hashes. +# +# 250, NOT 274. The vector is 250 temporal units carrying 274 coded frames: 24 +# units carry two frames each, and those 24 extras are HIDDEN frames (decoded, +# referenced later, never shown — the vector contains no `show_existing_frame` at +# all, so they are never displayed by any route). The rung delivers DISPLAYED +# frames, one per `dpb.outputs` id, so the golden is 250 entries in display order +# and NOT one per coded frame. pf-bitstream's +# `the_whole_vendored_vector_plans_and_the_frame_count_is_the_parsers` pins all +# four numbers (250 / 274 / 24 / 250) on CPU, and `gpu_parity.rs`'s +# `av1_goldens_and_the_ivf_split_agree_with_the_planner` re-derives the 250 from +# the planner beside this file's line count so the two can never drift apart +# silently. +# +# Generated 2026-08-06 from libavcodec's SOFTWARE decoder (AV1 decoding is exactly +# specified — every conformant decoder is bit-identical), and CROSS-CHECKED between +# two independent builds on two architectures whose 28,800,000-byte raw outputs are +# byte-identical (not merely equal per frame): +# ffmpeg 8.1.1 (Homebrew, macOS arm64, libdav1d) +# ffmpeg 8.0.1-3ubuntu2 (Ubuntu x86_64 in the pf-lxcheck2 image via +# `apt-get install -y ffmpeg`, libdav1d) +# +# ffmpeg -i crates/pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1 \ +# -f rawvideo -pix_fmt nv12 -fps_mode passthrough ref.yuv +# # then split ref.yuv into 115200-byte frames and sha256 each +# +# THIRD, INDEPENDENT CORROBORATION: the vendored vector ships upstream per-frame +# MD5s beside it (test-25fps.ivf.av1.md5, generated by cros-codecs' own gen_crcs.sh +# with `-pix_fmt nv12 -f framehash`), produced on other machines by another +# toolchain at another time. Re-running that command on BOTH builds above +# reproduces all 250 of those MD5s exactly, so this golden is not just two builds +# of one opinion — it agrees with a decode nobody here performed. +# +# Sibling of data/test-25fps.nv12.sha256 (H.264) and data/test-25fps-h265.nv12.sha256 +# (H.265) and consumed the same way, by the AV1 parity leg in tests/gpu_parity.rs. +04a9916b634632172d1e21d1bf1f083305e847225d361784a1d4fb46c49d7b1f +51172eeef06a23063aa881bc6652e43e319c86f954eb21f3cf80d31c3832f365 +9db9cc170509db22fad2436de8afea4bc3601cd645706f21a9a786491ffa4714 +f589376bb225817e337da827ff08d55aec024148691c16589ef135dc74f364d2 +c0ae27f37ccea7405e5508c768c95a96ab44f1e916625f000410f1d82a43912e +a8cc856dd4c2b4d558871d26fb5501385ce900a9eab4f66efbc7be8f8597912b +155a58840b546a6c65ae6bdc16523e723dcf50788b92baf813186026e7b39fb3 +48b6c37599f073ad7f6d61f65756d85d82b5b35a1fecc22cd1491f4c3366bffe +99846bf7c69f3c709621f3c6bbbdf7d385dc5a3736c1702933ea30441c5a2352 +a2f970fc8d340e6978b327f89366660635aec6782b2845c4a5f9319cee79d340 +a3245f047ebff7cacba1baeee09f9eb34977dd578f4a4ef3e4fe40c3601c56be +39ebba702c84e7a342792a85ff922130335d724597fd48e41f32a471fa76333d +8540fe45480763d891673ecf99b0dc4ff23fa1f7b1f8e25c7532805141b6ed85 +88a37ad837a0778ec7465405416e06487e2aeebb1307d550a07e5f66498a0721 +9678d42aca9aaf32d7aad049988ba6ec530babfcd547da545b9d8be4a5e2763f +1f608b7af5d30bdb8e1786af9549b27f216656cb34175f3ab1538c07fe32a91e +29341aeeac9d91760a3e1aa45a0a91b69a8505a125e236d35476e438b2d8b5f3 +18254332b0d8455bff2edff3c6421beabdebe45ef45a1e4da6155190ffdd8c37 +e731deed91bb66157e2c51eb2ca351801205397922fe99936defe6fe53252daa +e52cef7ee6f7cb335350743ef4acb5eb2f1e5a1bf5db030f58afd827499be877 +f46832050dec356423266289871b6e1e48e1a8551ab03e879548f7b05f679f1c +a9505b4fad6f60176131196b37d8d435bb079090e24402b33e96d0ad18676abb +d41c9853a927e9c215c795050a4eb017fdecf8aee3da5e92ec8a09328ebeff4a +73dc659f8c749f92acddf83df17deb23c55b81bdaaefe5efe3444de4eab5d6d0 +51728e28c25190da1812d15bd3652a6ff6f07eb1ebffc80a15819913446d7b5c +861cb38a660b83663dc12da2d806e00a97ea9a17d637285c6f1b66ad66fad16e +8f05f727c2d601fee4c46230ddea131b06d252acdadb6985ec85665e4452b6ec +cbb46e9c3ebb5de26151c90c094bf903c1b0e64827610480e891e0857454be86 +16102b7bb0a6188c5a81ca165d160086a7e1235bda2a318bab0492119aa8c494 +bfc34108b13be73ac0d8ffac1414076f5dd79bd4de686b457a95023bbb14f8ed +6142990f104c14f0f3095f291c9566eeedf650070d722e6da143f594b80931ec +3b7e155c3862dfa85c2326f643617578094fcc2b9dfb2d655e8cc6b014a4da63 +66e2f3d97f3ece05e56ca37159d9586f08c7034a291fc4b003bfd4195c404fef +e1cd61fb1b9b0378027a737ab4affaa590830d71fda2c6d8e93e82af7f0e3d19 +c141b82b6f7b20b4928f715819da5c6c4576d7228202a331a1953676295498bc +28c2b7f4b9fc9653e4ab10cf40b270b14d92e7725370cd3203f92b5ce5531458 +bb4015bd6cf672cd51eb5d30095419b2f4c37de4e299daaec423b5c4c6d3bb92 +bd898e002d41d1539510cdf0820f6757506a781db541e90f793010ca61e633be +7a2e6dafcc16aab285e11f763871f8d06f653aa437d5a874347fa4f3e2902036 +52389449d67fc312137ab62db2ebfd3a6550eab707bd03c04754b2b69ea0c68a +c9e44fd4cc038e9ebaad163effdd7e1fa1fb564a7226680418744266eb7beb13 +cc66d3cc8a0ac53b360695ea9f3851a218f659a2bac5a75569c65fb663a2963a +32bc728bd387325bfc383ff5266e790f59d10cd79f1379da7292e4b5313fac26 +50901e7012c4549cea01c9039568a002b26f0d4861acdda6e1353363fc907b2e +d7d1c8c9f3d8533119164292583ef7f06490ebfc42f39bfa04100eb13f8dc572 +e861a7eec4472574229a22819f4c5312570eb671babf394b5d71250fbebbd963 +ae4f62b2cb9454d16729cc475fc77717d7863dde2b90295983ee83b7c23945dc +cc1f95cba3fdf71afe9f4a17f185aea83059091cfeb217dcd83764a02537c98e +2f01b1a190556fec2760c1bc6b9abd94839e332b6b229178aa8caaad809a76a9 +e7214ac73576798426c38f27936f5222a8f12ff046e4bef4fd7db56765b5cc1b +cf6c88ec0eac4461d28a3544af15271a960ccc19573ef6cb300cfb9800112f88 +559d96b0d160a10581969136b4abc0d653a39ee45b03fbe1dcb04b5a98d245d8 +15631d150c3215a1fabcf3bf0035314d898a546a2b6a4b2255957202c55bca71 +44bfe0041bf7b233e2c9f39023d617b85ffd227b1922d06eee3e3d158db94426 +c520a61755e0f5e0fd78d2791431b220e131a8b7a095b5a29fed2ff66bf3ef4e +c9b176c0d8fb92056f6d29e64a0e6f899b646bc5ea397ca09bffde796314d790 +feb41aaa9c0afa2cd36ae45318b70118aec59ac8ef694bef014d224fd4d9970f +9f00fb66da1f277f88ba31610203e78d9cecdc750bfaf4f773b29a36c3d156b5 +225728f96f584f7a019d2a47b4cc104332d92892ae1f0ff1db2a384f11f8e6e3 +ffaccd50aaed93791506e4db411673303804e3bffd6dce69f1ab0ac9527c26fa +dd889709e85b4f655365111efb007fe05564548898cbdb4c9328c24508a82eb0 +00c2f8613d319adb52e3fcc59014de4204aac015f549e01013c40f6a74b3af9c +84f4f2a6b403a2c255979368d0e5ba4840e1647e3bc20c57f88506701c3adab7 +27bd83a287a03570572f1d70f823841802dccac6e92a718dd29d321b36bf4882 +5829642335c09a3d86f280a674e7d559957f40a9327917e5864f8dba5a30819f +235aa9abe1c41aca79f4a052821a20b3687c6aad26a8921bee251d84645be5bc +83ea45720f61cd77e6998465929e338a5bba0d94b188441570d76d8b5cd7fc42 +612d69a24cd95badafdc4315e1992f08170a212e1a8d20dbe0f54e73142d64aa +5130f8cd836552a00e5f296b03bfe511dc3469ec8484b23fe31f787b445aaad0 +b8e25ead0212886654eb57e4949f3e00c3995360019a8270448031648949f7a2 +b370f67f98a36a98b0080beabdac6b0503646e6971f02c971e1de506d414a4c9 +af95d0cd2bb7191275681e284fb85003831b10884c610e84c55860a2497169d2 +f4a1db148944256991a51531620fb107637f2e6ef298ef100a2dde3694c7c7c2 +a1717c8df1ece7821c7202afafa951c184c097f4c0231105a1380ff9cffd3d72 +695261e9aadc259d2a05926b8591235c35939ef89ec4de0e9dcc858988ff7d95 +cdbbc29d212ce0f66ad991615b2fb01d7d3c9c2caa761a9e11f744ed2a4c0115 +e29fa67c173cbc04bf0a3a6d6fbfb2a6c6208fd71f386b8fca5c6adc0adea614 +481b12556e2a0c019a3dae5cece32af4089e077106cd672ceb79630c76f5190d +848f032dd30b84a12383381a2e689c034377630458651d27f5d3ab946d632389 +7a9a535f138d5a966edb4f27d59faabfbe39d33d92e19c99d1ce58d3b0391d95 +7ccc23f5ee19dbac8be8c6a49145fac7b03d6f3dd19496a4236961098ded7920 +bb4c430d52662df7f2ffaef738c067d489073231834d40b819d58e38c6a44cb4 +0204f817e8e6891657bfcec7b830c244dc9dc028ed753529cc021709a6ac49c7 +4484238b52c1775e3a5436976b00d83f5398e8be0d019c491b383f169e343754 +f78c775cf0da2138af3174b9e36335bdf689af55288002ef64fc5a0cfdce05ea +ad2934c43eb8e088c1f5f51518fa0254e418b941d1c7218f4611246e0981023c +d83c3eefe9437fdebdb111401a3622ca6c33a36490079b7a77dea14e04cb01fa +0b3a39e42415f1181f47f6a9df7e2083a234499a0eecdeb7a073a1421beda8a3 +1ba58406e0e81a9d283b414f3e0cf4c143d2d2b0dbccd6378d1973af864cfedc +2097812bf91b0cad050ddf2a62cae3eb6713f191bed44d52c1ec594706365268 +90bb9b77ad84af847c7915702755b2e28805f1597d65bb04aac805a7135f4468 +c3a7b4d956edce520afaca9510f206982380617f1328f7b37e94aeb3f6827a5c +ccc19ee8a386a1fdadf674a87ce519ed490d98b6dd071810f3286a91cf97fd61 +a0dfdd093e1bebc1c3735bd166b9f63bb52373aae85e3ed7cef0207826ed4dfb +0319dc031150b2f2773743291ca5d9a91f756e0f6224dd6e4da7eb4d88c1bd40 +0c2edd8861e6d3397983c3bdff9dc13d2cae4522197db10401379a8d13898f38 +3393e5f4b352df95d11ad00b70dfd96802c25ea23b9d710623c175a40ad54afc +1c2e35c9db9c862ba64ff1c7b5086b7178e347c5c68e39c916ddad03c55b9418 +95bbab9230052ba329b42fc4ab2bbd4db437e9cd65bb137b0ef604ec196335f4 +b00716a8f666a443345c03b302a90b6f56cbeac378c03c9b70d92c15f038b4f7 +c9f818bf8f49e67945410573eade3b7efb66cbda16eee10d678ff7cb9bce4b0c +28aed1c9e7b11922920b407f5b2cf2c7df363e9f793b3961a0142691cb5a20d1 +8dd178787e662d7aade20307db2b089c09f930788ae1a0bf53ff085060c5e8d2 +48336f5699128d39f91dd9de306ebbdf05ff9f8c0e5ed94692fb75454940b388 +6c67cf9ac541919e8330f736922738f38c63adae4b19f3049f76c724bf5acfbe +56b3569f6c2e7b52b3d87633e1295dd1ea18907e536875643ac07cb262434de0 +316979d5edd9f59ae6d46e75f527a099686ce8e7474009b2962c514479ea8c58 +c24ec25ab6b37e3115ce9e529f6e696270fea37971565543eb93e780da09d905 +9c42b6ef210397982cf52cd2915a687d39e99495cf1276643f270644d2bff2ed +99135c0c59e8e12487b77325af4151be2cc37a77f06b7912ec3d20bc5526ddb3 +82eff222c286981bf376b18ea879cbf48db197d2fc5e5c2143b8cd6c1122c001 +19bf0af707de5933b49d84595e6e9d0c312ef2479f8eda74cf838099328de201 +bbc769d24c42c68fe33d0a3d0cca2ded31091f9430434f66f74d731a8740ecd6 +f7fafac3d6f77b639528bc29222a3ef495ffa2281ff83071edd6978849138e00 +0f3fda4e040e6ea9431eb659cfb31780afe6b59606dd839e6a1ba42e63e90ccd +822d68d0a791832aab0cf32ee5db1681d6061bff00ead8803c1cb3beb38b47e2 +3861804dd6152ebf876be7e24868b7e8f38ae4c4f8a35de9cc4db2caff680ade +fc0827486353b291004ef921f859fed4e158b570dda3c00006f3f654a9c94d46 +88c08db2b7aac161c53c53e10b1a7955c9f34ff7fdb63bdaa018231638d53c80 +e652c08d1ef97bfc4036d24013079e4536b0ab897ce71fee9cdd1aacf873cfe7 +9aa766897bd50812c5ad944f1cbbf85abef921dc177bcbbdb8d5af395729ebbd +d5aa42f129cf03c4404f63875d6500dae74f142b53f90c493f65f4ea6cd52e29 +f046d91a8520f79b82b6c8127eccc10bdfa8dc33c23de610cb88e685a46dcc9d +2e5795b34aaca15982aa068acfbf61d6a91adec487e8e58579d04c03504df010 +1feb946111faaf40be12cec1e8d9e3749241a529978fb16d84a024f8363f14a0 +58a18c5b6c68cd7e966404622e9176c6ba03d7a2b351327dd32114d24eedab6c +a6b592f74134be10420ee0d26a80ce61d6a22b8c858b1f03551f127d469eeb35 +f0aeb9eff65e9a45937d6089ece2d75e2cf08d9723869dd629a85b83f1daf858 +3311498180e377211ebd282c3daea28675d8616c560d233aeb2990f4df3a3157 +bad41313546387ae30b83aad7aedc854fa2761fb18db2f62b8b4f060c84a6bac +444ef76a1b0fc74a410b5bce137d05e073a4461ca10f29406ae47bdcbd6f0b9d +9b45c7c24a19132a9fb2f9833801319ec6fca92dab61c18ca6c8eb6c52b5cbb5 +88f4546ddedf5363d5d267db4a5659986f59c0f559563b11b164fb2e67156467 +a3597d945ee641d1016f42b02db41f93e12a41d49008103119a479fc1a1d64d3 +79c3e3c8aed31550eb0d2fb8762aa0350f7aaeb107fe5c2eac577b39c2cd8d72 +ff483d3e8ba9af42c26233ef67e4d755d64245a68d580c2c1903cbbb9746dfb5 +8572121897bd614f3efa03ef6767f3e21695f2cf48b2476560dfaea16c281762 +fbd251b057a3156b32d9d5565da046dadc8ac51892d9c77b74851b31c7c08053 +f34de916154676ceda653796b53fb2fe9f892094759dd3939279b4c7d512f9a0 +eaccd92e77fcc29f32f202d93e7dae85382397098377e23051f9e0dbba31b34d +fe83002ddfdaedc72986f35f7703cff06a67a4ffa034b7aedddd394b731dccfc +cd23524ffe622557dc33e36b40af70d8726e124bcfe2e26d54d4e2c3b3b19417 +5a858bbe9916be0e0c5e1460ed6e7bb227a6c4a939005dba78a0f6f3a4029215 +e04e68822bc3a196507df77f4a206918438eddef2404e608cfa5c7ee8922c57b +c3c8f51b5b2b2374316c9b2ac39c0d9464f01a220c5d8c5ff6b455aaaf45ccde +146cd43a435e663d6eaf36975e5bb2ea5019891aeaaa762fffada09df61c848b +d2d4b3246c184c3b219abfbd48634107aa0aa426fb190c23aae3f088867371e6 +be86426ad5e44ab73ed6f058625ab920836b532b9634aca82a4a1343342372b2 +34114cecdd6017b74adb6701f9622bc7fba27aa3410c2373479ba5e0540d267d +35017238c99551d827d7dacbef35ed6076630049b165ae83180eb71d81e7823e +058069a3afb141b4158815205f6a74ff1c94318cd4177abbe0c540e5ce70653d +d6b3d6d433ed65df97f9c33a761a31507e5d98db07c632c052518efc1956a099 +e3c8746bbf9ae6f2c620bbcfb07e854d6e75741350df8cf37b57699c000c687c +73d5cf36367ba4e506e2ad34891202da22fe77980ebc4b5d4a4f081f5616726c +abdf3028d25ba750b9295bd1aea2686a15c8f8ba464eb60710b40053279e1bb6 +bc324cc77981f01c72f620c5cc1dd85334f781eafdb8a89b012d8e08216b62a4 +5108ff93f95752c540859a23c31822107398a38f7629f9805cde7d0e826471d6 +0b82dc51c686bd6c062ee38ba028d2abb5d7bd3fe66611294d281b3a2afe581e +efad4f5d6f244eeabaaef316640b123b591297bbc84c3b3fade3fc18d8a35df9 +b034a1c107559c8f444e1791dc5e3c73664d9f05c945b3ed0e237c94fee5cabd +f6423105f012a404af935e7a7385095579fe417a27b932372bc357d01bd0ec5e +ef421cd426de02948ff7cf456e9a69f3665e6be7d1a3f0ea42821e62a3faa44d +8c5f37b28e980057d5f168514f4031283c1bc45e0f5d28ef97640507156e4999 +a40ffe4ad42e22f4e79ab1d4565848957bb4526d8dd68016796c86eb7006c4a4 +7c193d2d343bfdc36dae5e3f49b2f73b1c77d9b41a1493fb88d1df5a8ed2f95c +7b316fb7c9be2138fd04cc55ed2ecd4158e46239062f2890d3262dbb73996f86 +511cc99acfd3c66096d01e1225d3eab1f9ff1a14fcc84a5e8606813da3a4d5bb +2f3799815a546dced31bdf70749462fdc81642142b393f893856afe40f1ca764 +1bb241bcbd388fe6ca5c6d6070d0526dac14abb6569dec2eb036cfe3bba694bd +4267b825cb3a692eda0d1bd75d48f6d1f74de5c256434192c9ae27220d552ab8 +294a8e9277ae256c7e519515deaef45a2c41ce223864c9468db54c8e44233045 +ce0842add29867140313f607508076ae9fd4b7f3ed7117a0c969cdfdcda47aca +15b4e0600d0cef8591e53681c92aedaaec419b202094533935cf52fd3419282d +2a2f74a4d066a09c26677867345d64edf41dc5f419e0fec3b2f4618708164585 +9852a0524833dc3e60d68924558ad424b9d6fd498351b926db7c3b9501b79e97 +6fd26a7b2c5fded3688130e003560a8ddd58d0f9bc2906ec1b679842d1cf16c5 +7bf43d1d54f813057713c66ed1ac69060c26ee9dbfa10de7bee70f1272e4c4cd +ae3dd2cfb8d7ebf28fbee064b9ee95e8ebc555e479c48a6b64e1e453f4951bf7 +93850ae80180f782c311c182d21207991df3db3b17d9f76af910ac655ae5c1fa +e8dc4065580d4e764de7efc30498824cd2e1a3bc32db46211c8f8128182d41d7 +d154299c13772040f936fc48ac1c84001baad3ba97f42c935f34e49eb5f647c1 +daf8fe6bb8f25a1c4e07829fd7bd282ece7ac184c4671465036c102814c03ddf +fc73ca6be0515992d070467b0f099c075f733d85d3d2202e333c20dcde11e5cf +f1d624e05f1d4bbd19595e52d33c6ee9cf655bd7a830078a5d989f88d5e7fcff +9457a3d05a76811600fae446c1391963aa411ac2ce4e53784af7b5a688625fdc +b153bd9cccfd998ca9bfe291fe12f3c234fdfa342e4124c3575d1e35443ea669 +274f46071fda32c474cddbd234b837a0aacb3c57638eb6d5079cf1ffbea1651c +874d47f84440c15fdd0c37d2c20d471e1675de27671208e007c8507ea0d7a313 +77c47246000c7168b87adeca3b5c5d80e8a5e9d723086a73ed99a14d01ac272c +ec77435e3a8469c287bffecfac6c0e08505a467da7f4b6d09ab3248f1c1d47a8 +013c86ff9eed9007d8d44fd15fd4b050958e7737a65add691c126cbba7ad10f7 +b7adacbe6bbb4a33f0346215f67566fe4e6adcd43c221bcdf6cc112b07323396 +d37d05cebb4b4145b1934555a064069659c8a6c91854c5e207ff4e065c4df4ce +6c377b3e07d0b16a73ff58ea7cb50fc50c49aa38cccc7dba6f8c14db16139655 +1988b08348836747be71e0828dd0ef306c437043eda757eb5baa0cf3af860dc0 +f49648f9d652552863ac10227bbfe5c29d43c8c7e2dd436d30cca8adcdcaa40f +c8e47679b7d12b5335e24f86ee4f4a37d9bfae6ff70c99b92cba16960169fe82 +e1c353d3fd564e699359b4d5046b26a5e26323192a7a9b43be685ffbc60959ef +c87c2abb582a2969f68b46a98815f4e0bc8d003fc2fec32a529126fb07e63be0 +d88be32ee96a5b0eb436042b3ad8017c48abf3d1fdf8cda290fb3ba3fcae5618 +cab86bce00ce96e259056298408e2bf51026801b3985be7187b927ba1433e5b1 +93b729c6995d3576c286de166850681592876dfa30bd7d2cd979aa510de00964 +cd610cb405436263464bf116632a2dd71ae379efe1f38d66238308c82941a8e9 +1925666c067fada0dc9dcb9019198829071229aa7ffadd6aca98c1b918a88a3c +c975916319c5b80581908e88483d301c977f2b463a71f2554597bcb0e133924d +2f28cc1210626c92c5720c9013ad4e610383911f5a8f8786dd7ae85e70ae11db +9c826990635030face0b4016d139bab00be992debbf2cc5ae17d55ba6c3c2bc8 +7710fdb6e25f11880b021afe45244f128df67b750f634598e3902edb184dd2f7 +59d739493f8d60822d323b4236dc38073e3c231b5c1ce90ef2b2744afe503c3d +e8b7bcf58d95f3d7ff5b26ea7c5bf82583f4cfe752569ae97904c668dd2880cc +b537e54d9368c2a70ae6bd37c16d7d55476c1aa25247b191db34c03723d7cc89 +9d5b75f00298e2d088f4a47b8422c31f793e3c7be0093ba696c9234e213b1f45 +70cab03ae1fb6d1d8d4f090dab142ed3bb8079b924564513cd65ad2d1ff50ebe +2699bb193449fb1b85afc49e0504cd84cab903b4329a15fd3cb423ec27eca250 +5f23520761f8ac1b09e9da8dd3cc85753f12921320b76e3bb534e8f375d71e17 +746f5bd533e793d2604bebd0e1514bec7865e92c58ea1abe554e30b27abdaf0f +d9bd51b37c524ef653c108be5a4c5e38c4bf16da7328a0e3f88a520298975bf5 +45a7ea96e52ab5344bb5b9b69088123861b4f29d94b918b3fc895280a258022b +29c2b08cae2f068b818792e27b5ec01e2fded550a531ecbc9385db925e9269b6 +7b6169487d371b695f2e3837cc2a2e4d6fe52a6bc6f6949ee9ee36212de897c3 +021b8dd7a86360029f7028571c9e3226057a61909b67b1471610d354eb49e270 +18c55b4d764148e3052f1232d068a542697b30f0b522d5e544ffd6e41fb62314 +f374d94bb22cf9cfdb7ace086f4e9c4e9e30bdcfab544eaefa2d23c1a39995d4 +ddb1802bc2645c54f916ba144fd96141f1705fab084ff645e11ecdabe4788d48 +5cde86b5f8a25c323774e8ba27a5cb4158f1bad5da8b75d9a530f476ddd9cb72 +197389c50e97878feaf583df1d5ef5b0f48154207c40dba89c6a72f9ed367924 +5459967d1c6b9f1dfa95e7b4564907fa1e9f42a31d77856402fb0af27c81e5aa +647006388c6fa8f6a69b432172b31ee56835b53ba9a4d2a8d7646eba8d699424 +6d4e9ef701148328c0ba14ac0a62b41ae96babe384542731ff8b7c5ef46fe63c +b6d3de098262e4d24011095345bf3bac97433401a1d96f0233b79c7e48ba55a7 +f4a81172ff9454e221b669c822ab3b5f76cdd8e62d3fa923e42eaaa58e60e6b2 +053ee6f10d5d37d451656c6e25c33110296f02bd5e2461ea710cd049006b224b +b2ac14a086577e381e80865a66cce66300b72e961a1d7b8a817b9f4f5189d71d +ceebdccc9606c9d6d59f0b8b27b8f7da95107d27864e9e1fef6d4d5379039e7a +431569f73856c9478390635049ef07fc73a7b9ad2f0e58e71b806068e93fd844 +be4474bcf98e5112f903d54799dec8f6e8b738a6e9d901e6cbe2c1e99f886c7a +9be9d34ab68928b3fccb05e1863fcd2a8302400b748f3d4de0411c614fca0673 +de433edf968140c2f32a4ad3cedb5153cc4e65a7d846cee1d621ef7cd5e1ce25 +1fc5889fa4fc6aa5111cb3c816a75feea5fd7159a0090d950a50935045cb2de2 +df8ad0344cc492f244ef63288517116db1404f5bac56f362ee214ff41062f6ba +4c7a7ad1a5ebe5c7fa2c075a95b13e67418ddd3ee95c72fbca66b36150daeb63 +07c6d9e6128d4844d7665e3a12d8d9d67a104cc4bb0a7a14a34c8355aebaa486 +5583161f61a665f7d03ceca5b878621942659303c856245887bca36adab7f834 +59687559c0c38a27cc79c6d72e77d238569427a040d992d28c22a70d34dd918b +a3485384bb20822e4b0e67f95764cd765884912149c2cd3b89d5072023044b08 +21b3d28c3324e7c2e97106e8459eb8bc56946a326fd584049e8ff93007efc368 +207bba7349318b67dc97af31762acb76a8718ef4749dbbbe51240abea6d03c72 +c60a83c60c28e3b173a6a2e4548ce11e4760f99ea740b1900697d946136d86bd +36c83d820c97b467810e84727782fbc64b355a0692acac00621bb8817dd08080 +2917e0ad1745f53b79359df1be6886138d54219e01ddcca28525d6187ef7cf40 diff --git a/crates/pf-vkdecode/tests/data/test-25fps-h265.nv12.sha256 b/crates/pf-vkdecode/tests/data/test-25fps-h265.nv12.sha256 new file mode 100644 index 00000000..4863aaf4 --- /dev/null +++ b/crates/pf-vkdecode/tests/data/test-25fps-h265.nv12.sha256 @@ -0,0 +1,268 @@ +# SHA-256 per decoded frame of test-25fps.h265, DISPLAY order — 250 frames. +# Each frame is the 320x240 picture as tightly packed NV12: +# Y plane 320*240 bytes, then interleaved UV 320*120 bytes = 115200 bytes/frame. +# (This vector carries no conformance window — coded size IS display size.) +# +# Generated from libavcodec's software decoder (H.265 decoding is exactly +# specified — every conformant decoder is bit-identical), 2026-08-06, and +# CROSS-CHECKED between two independent FFmpeg builds that agreed on all 250 +# frames: 8.0.1-3ubuntu2 inside the pf-lxcheck2 image (docker run --rm +# --platform linux/amd64, ffmpeg via `apt-get install -y ffmpeg`) and 8.1.1 +# from Homebrew on macOS/arm64: +# +# ffmpeg -i crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265 \ +# -f rawvideo -pix_fmt nv12 -fps_mode passthrough ref.yuv +# # then split ref.yuv into 115200-byte frames and sha256 each +# +# Sibling of data/test-25fps.nv12.sha256 (the H.264 goldens) and consumed the +# same way by the HEVC parity test. +ef4900effa72cbc00cff38938bc558cc5d70b31ca268aaf3c01c4268a9d8066c +fdbae4473d24c3adcef59a51c1449304c2fa271412622efceeba5ee7437b5529 +9aab5493a8fe3dfbd3f1eab89bf67a4964998bf8f1229c672ddf2d0d5fe26edb +52797a59351b0a02069a3d8b2022ad7e06be4e03a7ab9483e09c0b03e4dbfec9 +9c68407853977ddd95dda04b77676e983fd7ad4665bbbe65497ea5e91f5b19d9 +1d7e6adc6a225e1bcd2c5cc598c0342472edd2d7dcc162c38872fc665b57f3d5 +4633e66a0ec77f26ab955bea209b1e976947ae87d8405e4a70be2d8e6ab69625 +6cce1546ee5c1d414353a990aeb6cd3035199196cc6bd527d1f7985247c30cfc +6e5eb5c2286156d4c7406c9bba159348618799fdf2d46d470ad4f0fe2c6671dc +25b0af55a492eaa503349ab45f1d6449df832361ac60d2910eade5380c5b0789 +dcae9606d93f9405c95e5a03699beeae1442240303df2d457bb3b5fd5e2cbd18 +d7bcf9616f062b9cbdaa076db0aa8fe0e60ef83b51c1336276183fa4d6f2f0cb +003e43b5d1e731ae234146763c1f65b4c4e2a67fdab3d938b7ba442be360a520 +3bbf1a62250deeae73fdf77ff34c2c850886557bc79815a2c968a5d8968a67ba +d8fff092b581e7807b77df684e74f97013b6c69d4610ad17f08855585c3ef5d6 +52d3d01f8f147d8574295859ba1f7437ff11d57eafb5c5325b55c7533abf922e +e75eef989830cbd6c8dc3957fcd96e1061f861c5f12eaefadd21f600ce0998a0 +9ffd2496189a6384eddda2e752a719e27c6c69e5fc74330b75fa7e4c928cfd55 +ff304806867abf9d6d683c0d3bce95473cbad3252e815463e9eb5a1b5fabfd6d +9320a5e1fd33ec1c8b3d2964af8050bbcec219b3287b83c2cbe6f61e8d4f3cd7 +9816da3390b510218fc8605398f9cee8d0c8561e2933f2e2a1f771023a7116ea +198b078fda1f6f1402a53e8c73b99544abb8c4091efab68e7221af73732fc10c +fb13ab23a5e012e4ff0cdbba1af7261a636940831c1cbbe1065fdd4eeb4754f4 +73fca4a423291a3ff6c787a7660085732a687d7e061dff235a29d83ceb96e95b +195ce227acdcfa1a4cd4fe35eff3291ebdd5f3fe7545509e32e17c744eb98b4a +706aee7736f9c599b48dcd156cb27faaaae51f3fe85b14dc12402207033aed17 +e061a0ab22441f9b2be0fd111ca67248ea40bb3f5d3b280b3c52648eed8444b1 +93be6f5a81550ecca250ee0c145e5c4135d05ef0f4a78095f94e85070cae9db9 +465723432c68de41e96fc32e562812343a9fdecd53b9699fcc6770b5f9e4b002 +874b39b128db5b47160cf0d8bcc0040063e23dc5b7f0172a8149f78e7ab57195 +3cee067706084dd8740ba200e3eb1955c93e1df4aaecc298453560e61f658271 +7ea42e3f7c7a6488057d4cb7db1f7dd61f505cdcf8e71bcaa33eb00b734018bf +55b00704e979ac7bf2e4961dfe12f1ef5c559a421e24e3243f943a013e679fe0 +e91f2096b0bde6bef5762f2765d2ce30c77f9e1a9adc151bd07277fb715c8131 +08d1391f8876e67cd086d79a703401da70abdd37e3522445830e40cd820cf3ba +87531fd1641407d8eee5888068c9655920976deaaace01b0c6b6766dace53d62 +1993c8add817ed50882c865eff2c82f93f1c8f050e1ee3171d9518e3a6ac0563 +f682098c0cfa38af29ecb40572aae48c4663e11c5ba9ae256871a1fc62111dd7 +dca4d5cad38a35d15ecd4ef02e2a2effc6c103ac4f27ed61518416bfb8084f72 +33a6d608d4dcb2cb47d648ff4637b62e65b4a769093ba0bbf39ef151e174cf5f +814d05ff585e21676fe08a867ca4593c65870b81d5bb7ae51d8fb795371bf9e5 +91339266713bb79af8d547ad888a010b7ad56debc938e4ecd9f3542a8d148e40 +945258f865bb6b795d06609d57ac77ee5d863bf19fb35785ac7578d46e229cb8 +2b2619f2814b08675b263e38ae8a50d816fca71d6f014f6058cea2b4dae74aee +89b8983416633c225519b3621bf65601a0f6c0f46de5ca3250a9b0c34edff73f +0a007d2eacaf06f629ef854450fca7d83d6f6e1bab60a1145466dc157082f877 +e8550a8a431f623ccfc8457c4f171a6c9bfdc1c67807fe1fb45cf4bdbc14f7d1 +aafa9b366c4d560b9994366b1ec3a2f71c861191316b84e6b46ba7ddf79c19ee +4ec7b250bb49ec17b34a090f36f8cd07692829dcba1ee939a4526e89366ba1a5 +c00e70ec1b43cea1b3d2909d61cd53239f428085b6769913307c9006bfeae83c +b8744028170518a8542d4f37ff05990230ea1ab87d71896b0fb41866b3859e57 +cc9b833a5725bae1c6e9d198c148ef45fced9b34a36fcf043cfd2774baf9ac55 +81f3666280e765cc8641fcfb0a65fac70175e18c7ab701534a5499ffe3fa8543 +f92faf17adfb611afa432c2c4f3c192ca24246a9613601ca109bcec0d8a18c56 +6acda837d6691b80b11c9c83fb41108b0aaefd1a90b5bdae28429542ae998f5c +94db3458a1fac5e9cd5a9ec07de06c659a20051b80bf71b2435d174604e502bd +cdbb03c8d67aeef96c7073346a87b1a41e866c2fec75a5003ab43d10ba953457 +2f05ec023ce09ce00b3fdd277e0da4aabe1e1a073e9612632fb5f5c95103caa7 +5587ef3923591036daf4d471a8a131d04fff2a6612e2e7788ac4662a8a3e16eb +19c7c1534c40e8a4dd7a6461fcd5d8695951dca24d7d2923b5ca12cb4160afe1 +95bb6aefb8ac74e52e3ef64e224a64eecb08d82ff3449d1cc1b8b44888a2ca38 +1278e669f16f311582ea9456e7361c57d4b4748c9902c780c4d9d62c7e603f1c +0d77145396334c2f3008b72441d594f7c3e110da95aef11d6bede46f49c79be9 +099ea10831ceda528df779d1c7b679cdc1d251f10cfc39f4950f7fb3a3cebd36 +8cb5a50ee01d37f8501884be3cd5239f5e57892901afedc4ddc3a9fecf435b0a +5c4f777977b786ab5e80f6bd7ea9880aae7fd8d8cf9e4d79016020d4fc916c22 +d635fab89355529ee6661f8e867e5361bd28773422f8478939dd806162292346 +baebab9b93c6dbb63ea19468538d9663a2f9fef5bae21dba46161454ca05c770 +22f4c44eec9c70d32b05cf0eccfbdf5b477b0f93bc61a17f9b5b4c5a7dbc2ebf +fecadfb3ec25bd00929059e9849da387ca0cee8eec9f0792128f3256eddd5bd7 +48065431ce0b3f8f0ad5f77fc30fa29eb961b1ba5e6a97c0bf41c5d3c0ea8518 +b575ccc73436b7937f98ec1847fd54d7c7a3a086d08adcff9d226613efd5bac6 +be5d094c2a2ab5feb6c988c68e9f2d6eee73bc69e8c51ed24786325d185ad5f6 +6c16634620280926af758f013b66c2a75a79ea67a90184965ea1bf91b9f51696 +0b63b9eedc87d047474f9c46e71dee75ca1e3a5db5cb0c9d7ea0627fa8c82876 +02052e7622fa96d6a0696f5c27d632ee49caedc5ac908a5c9f6a8d6d475be6e3 +ceeff7e73cca36a48a775f2e6b773d7c968c20de18bef4460dca582e6ba4b894 +693663aaf34e534a4f6d2ae7846aa26aa1d73b4bd9f854dd70bdb6a41b0cbb65 +474a10f6eefba5c7d16efd99fee59e0b762380b0f3acbb91e76bdc18a6dc9c02 +4c005f6dbb86ba5ef0001a551af8b4e4aa996e5f27c23658ea3002d319e3c53a +9968c5c0966a246dad5e0b49668d3d1caccc12a7722aaff677a6ad63c32b961e +a1d57c958b92e6a8d557893c62cea08d5959dd34a1421bf1fba02adf7acf0244 +f885bb6f13840be75c88223d07105f0ccbd4aa678ac2e76ca347739b0e3158b0 +644526f73200746ed78f8d0a35bacf486b17c6bb890f58e12c433ccdad59a37f +2beef1de03e18970f81792b0d247fdc9112d603dca852507b48d3585c86a7fa6 +b76fe733ccb80eb233ccd263dbfe264b6c298a93a3847159ab19fd8d9566d63d +cc7cfa0dfd02fec28ddc9504cf0e4a2656c2b3e7e2f13207b9979f9930408ea6 +fa6546a8828e3e3c75b1a60c0681db322d521fd4dd3fc9b005ccfb42d58fd9ba +d1b2527753951c11ff69e4db8323c058e40f411bfa4e23915f5675e30cd43b77 +5eaf026663142af2ba32628a4589aaa143977ecba28181730172099c6f749abb +9d18abd96a6548cb643b39a2499b1f718713dbd92f66f35392fa0f9fcdf4a81b +38ce5c4261c740ab054dac484af6afe58bf5eacd95fd322113cf532f362e0ea2 +55852ee3a2f92db0bdfc64c2693ff5a43ed7348a6ea947aaf9646b964c1842e7 +ba7488d71bf856cebde72d96245a582efae3d3392dc75615fdce2146b123cb0f +3e4c55e47febd86861cf4a7b56cb2e6fdcb1d9542d521ab04f57643d844d2f81 +8df056cf2f0d1e13c1d1a419251b09a9e1edd164e5c9ace7525be2c08d7cb84f +2e7fc74a622da56c8ad17d66f7fa2bd5a466dfe4147db80b7a58dbc93158a0ea +ffac7a103ab051aad9ff90fa43f9d98c149aaa6057d59660a70256bc9ab3951a +3725bd25b394076327a8f6ff010fb9e4b7662d67b48c80693997d434eb541399 +fd807e07cd0f003ac3d834487bd7526b000d3ffd79295ba92fc10caf3e5bd6ad +071469b129dc9ef55e67f773ad5c2ef6498b728169e98910dc5923b4181e3ad2 +512ced1859982b5bbb58f48a5ae8006c56bd28e165f0a761c38bf214d2636983 +8a663c0abe78913974d29f4380b7addc46612d55fb431c11042bb7985f22d501 +92ea5506c94d885aedb9dfa7f884a43ed8289b49542b67861e576cfea4d62b11 +a29dde155a03511735f9370ce23b95bfd98868434ddf6e9a3669c2edd0ad1d8f +1688fd2fe7b5f7e4cfcc40d3afff903bd0a5cb8536c0d7da4d53b90544b03227 +96f593ab7339f0cb02bec07b6f5bcc11d2494ced35a90d4037c961a2e284d914 +9450d79524043cc3f2afe0dda19b6a5a06de7128ad9826802b798255f72aaf00 +4dee54c01448ed0595fe1c0108c97031c48de96ea425c8f8c2e5b3382e862a3e +24bb1d429aee55c9d2c719777d15a3e8116fd2074a569a5e44a8d5de6d11fe8f +fe0255625438d6403fd663f60eab05ff990780054eb530dbfaa3552a978cfdc5 +452961eb29a947dd2818409c8d5e283ebf47281eebeb9c368164b22dadb315a5 +e49ad550b30140b2b99b6edd0e1a7873f3ba0145829037c2490467ddfcfc439a +f02dd0ac342330223e9c0ae56ed6a9efc3a0c69d8bb4c036943dd16f654f10bf +60352cf761e2cd8ba2cc0025c45e715a8719fabe78d5de7a382da4b648704ea7 +3b79506fc18e01a1168f880712c539f0a520712dd62d2f824b6b754ce034f1f2 +3c16234ed4606128fca31196a81f769ac69a4261aa176bf8c8d86b8699177926 +6a98fcfcef5d1ed813ba3f2f66f5b1bd11729430187a7366dcbe9617f2914646 +5683c679c3c0fb24b740ef4582c787591f10dcffc9e46037422829f5b58a1501 +9b4c0ca82d8440b630a569a8d6525384beb26ca7d54b7a1ddb1b83a00e134701 +5693915c011d2ad738b84c8a04cfb8081006b8835ec655e76d829e5dbcbe0688 +1ddbeb8cbe7d0d8ce61a94e3ca35730a07e59bdbb692cc82dfa3ea0ff9ba5979 +ca10afdd882627e9a6cb5d389c71fa015cb0015124c7445295ac42660a01c491 +7aa406047f3fdc2a07d6d228b30535734689c99d93e8c5c7865044187ca0d3b8 +8e3f8d26612b58f985ed778f1ad5f6cca0be49acee5b7d4d09ab2b886a3327bd +9edb22ab753a8cb66392d301144df20c7e67e3afe6bc9d1c3143087ecf26120f +fc4d040db9172642d710b4edabca7e5444fdc9099f9867b5942e200da1d6373a +1cfd8b95c000cc30892061db6dac6323f2f7a654c30d0ed024368997ac65e673 +f9ebda67091c2b26e7205bdab431299d982dace99cedfec13c80cfd3195286c0 +5cea0e5febf079803e85b57bf45d3d563d0e8fa1e45844af18609077497fbd3c +2c4c74504660b5222f2135407805000fe1a41df48c47b28685e57020c6d21061 +d49cffec62c461a04292556bb21f78861556b058db006143696e53cee077d26b +9d7ad96746eef142fec236232c0d275943a5258a40fe3fa384aaa5614856cbc2 +958cde369e11df3a7bc534ac549fc11b4a6e325fc02942f49c68c47e99ed65bc +a05043d7198834dd8da3d544dd7c11d3f03a3389a422fd0a0be732824b587047 +700a4641d8ee0a7ab2b0a09421755ee6223ef1f7b6436a96eaa479140361b40b +16d487df87dc05fa10373f139558597f0c82b9a22828de0ec462fae72ac09124 +73a8de0a8f0652405aad48cb13765767c80286ef190dfc1a10234fae0ee0dc71 +19f30498e93062ddf30e284bb44b9c6b6ee5c2ef6e143d40cf7dd76b669bf670 +93de878f03e69fce3ec00963df3bb941713bf225b665b0baede46e4a9559b567 +fc55a32f7fc41925a5bdf7fcf34513b90b6acc326771fd77db7fc0f4572db395 +e84bffc862d0d017903ec43414b0659b9720d0a4d646be3d1576a168a4333ab3 +ef655d0f2ee29f23409d1a8689710e024aae636c1ecfe8bc1915774a94732f8d +95c63e9092ba3a5887e9ba2044aea96315b25e55af2fbadbad50345fb98629fa +15501a54f24943a19a663263c5459d31c45441147cd58eba4935ab67d1bed291 +bedcf37b2101e06e9f76f85df28e70574bff094ddee96fb3b4733da63551e22a +e06487f3691cb56df61d13e838d62a403e7cf4cbf399c89e98a7d04597e91653 +483596c621e2646ffe668743eb0e55269bc7dde6188ffb80c086b215d73a9717 +a84fcc692a5ae998f082fb21e429076d59c48fa6880309bbe692e6f0b5c0ab86 +4aace8b7ea4891aa80f8df369689e0dd587d88d55160d40ff1fc03dd4fb7fe44 +b0b13ef2e6684489fe0af1ddcc2dc2e04e833d372b7c4838d1cd173da993265d +6ca96a7be457ba2bf06aba287fdad92bf356d31723d392e654ff352d480b5da0 +69554f33448ca613f466adbb14748ce2001518c861397100579340b9d9c71dee +baebb6de906b34a88a890274e491ffe00ce28b003b0ef1331c484740c036a30d +841a7b7efd05b4f8aadf6bb0074067a64b118c20505f317babcdebcdac05b9aa +d2eeff202d639a4c0ff451caad9cc7a7f4bb8aa62e6659671fadc4ba85a7eb29 +4ebc9035f1db8feb6e56f47a180b4108ee374c7442ea5cb2ba4fa59427d88fc6 +b7e2988ff896ed2618a17766dff44839b23b934108a1d75948ed0826c8129d42 +9eb4b2490eabc51f7fcc75c5aad419208238ed6807ff109ec02cf0f6ac1e12dc +4ba2d5fc6b9b27c53b4217b04f70dc219073a9c1cb0863e040d9973c88899084 +e26701bcba0f2014ca93eee23893e020bf681e071c9e62d0fb75619de7de715c +ec829d9898ba7eccb51af41e5412255bfe93e61137f56a21d4999911fc154bfe +ad47b88c0fc7750dd1a5cc27cf134638a9270342e957aef81e517a5a519933fd +1d39f1ebd301526344a29c8d07049a389ce157e8a0184fb34e38ad3c2a851afd +20d4699ce3a43154a62ef5cbf8f14bc47cfc344eff92ec78e4e6108d2827ad33 +79ceadfa9d591934dc9d87125772ae7cf3a0f26fe91ee77f27eb11a23924981f +3d481e4dcd5f0e849323822d687f4cbc7570474b985251fc13cb019ac5f72a81 +80a680a0054e1b0968a3ade98a8ccb42789082584d3a72917d2a1857b348b5e6 +fd031ad921c6dad12473e6fa148c74e05660e98be91cb68d3c5b43da423d06d5 +7f5fa38b7ce6401ff353a9b680fccadc138af24cd66369a05a796c363bcdeb45 +8a6ab1effe3736620a7d21ff943e7c50bcd244d9ebd26dd0f613c86b1e866ad2 +8020cb196e1830ce33c5242cebae2c13ca64e36b0a1186cda71b3b19fcdcddef +8c8ad9f8f7fd75798017937e117699485711220f2b529f0b4e62c1765461538c +66cad5c567e55c73b672f710a95c05825e26786ba1c69551f0d1c6ef92887d70 +02379f8b49c069e1a14e4fc371b84aa46e0589d6b77050a51fa888b20de13a7f +f7bc1ca4d9c8a3ac7dd2c53f3829c459bf656fef033a1c2e07942e573f1488f8 +fafd7c0ef34504324578f48f22bdcca6b87d3a57a80ac6a9359b9559ba1ba599 +e0f5d057bc2ea677d16506f272387801e5717afad3c27d29c56e6f491496d399 +951dbfd636ec1d0a164350cad1f0a69f097ae28447520e6d3ce4dd18cb7ce33e +b1707faa90c8d2853c98f3deb845283b119a3f626ff8733d1ca23367b617e24e +6835f35622b8a7f0a3393cc95aae42dd81d51cc31de0fd70aa0a2c4d9f7dc540 +6038a51c740da0a594613a563e0290493e1cea0f45b98334711137ecb309ce1d +9426f1fb57ce97b8a50cdfc98da236cd6c12404e0d9a27dbecfe53bd4e70ecc8 +55d02d7484a0e86dd43b28722b91ee46e47af72cddb478692b0569cf0ea19f8d +48a809c673c51d8acd8c8de0877bf5addd82ebd7ae39260dee46627b99e5f35e +61d251ca5cd28071a241e6e31dd1cca9d6c92aeab9bfb705c90fc02fa59d0721 +2e129f22a54b5a80cd9993eb6b10c0a8f3eb7abe2b47f5d2cc64c9365a083864 +c7acbe0e490196575368ffe3cdb08a12cb99466c0c77423fd1592033591e04ba +193d41b5c3ef5c85851f4ab1b3b5fb8a8f90575e2377362e89e35915de9166c3 +eb4c463ca22eac43632f8f5048343457e9bb0bbce83a67c4f3c34a74341622a1 +daff5bbe42272e24b29056ea9ab29b5dd70b9aa985370989ff1502d20b715890 +22b400207ffd843e7aec2ec2cbd6d53159e7252f1c60314ad5164f903862972b +d4f3bf876293ee9de770ae8a940458d75ed0f392a7fc93682223cee31d257d32 +a0ed984f000cf909cba32fddc9c9ac78c626105f2c0bc8abbf3f3f50d312a342 +825ca691eeaf5c2373892742050620e26ce1c1e81fb88c8ef089a1fbabb66448 +08e2a544c9756812b03513048e7ae109e9989aa87facd534ad633c15e440db49 +538d67388d4f47b8ee58da756dfb913f2762610044fc375849a4aeddd9cf8adc +7b11c5921e69b8544ad1fa6ff1f7da57912f1473392a64312b0132e657bf788e +87b0b1cb7b95066f491834c1f3f01afb6bc9e6e95a8a3a7b4cd3177ff0d52f4f +a6746ac64f664e61e3d1d041fc71ca5379c831925b714461b3203187e44a2542 +a88bdd638069672b79a708aeea9a12ab17fd7baebfc898182d4df64404dce9ca +0db7d0fb2b4262b93d1ec21bf00a428fafc96fb410cf1d5d2c30a8cefd374e93 +8e2218f82bc6b653705ff81ff03dc98dc90dd7249183521e1dae62429fe6e4ad +2d48037c916139c029f0d9ff9bb47ecb5dd4c0e3af9ce746077cb4a16759c62f +a03d1c762fea0edb027741537fbdad74b2aee95828218c8c6358ebe019f14782 +711ea5dd1cd0884c949066bc35af77e89a6760fc7812feaf34ff34c14b6cd124 +1802816711243b32c875a8b0c5fe04212f9dac427dfaa04a4ae614111cc9d490 +11421e2188d4b46d484edc1f346f22fefeba6ed504952fa53b33878512b7f364 +a54098f99bf02a7a143ae21a0e05809ee66ba2f712fa9a270c307fde43587f14 +0c3fdc1a5c92bba34cc7bd69f84771e7a979601e71428b9e8ff91e7a104552ff +023b3bf5fac6a8518ee80de4b3a41e443cee431eaa6e701b839b2ab5b42420b5 +cc073fa309af6fd9023c642316785bbd56d49f4ba60f96248a8991d3287455f3 +a24b3cf2b9e1c2639e8dc4a33dcc4f652afe949e8dfe50d3dbe6fb075c63e5ef +454e182ce3b29aa1942470b0a223d6070dfdfad119fbd9cd86d52d34d0261bd1 +14287adcbbd45689d25c04cc29edb30f8f2bc8e40499436fa09b33558a8f3c5e +ae957a764471b46c353396802418c1a13f3701b7847ed3749bcb619a95ec5546 +500832b21119374cec8b831a06969cb0534892707b3c8b8d49354bc2c9e87262 +29a45c4d1a1240f45efb271b7710418501030ece1a526557364f65591d34daf8 +2ecc2c67e41643ee4d3a1b1c27d2d608396b68de4293d2df2e061faca97456e6 +051f0a7a60641592f2a83846c157bb42538bf0be3f31b27e24f6c69ca6575485 +c108938b16d30b2a13bbaa2fea82ddecefd85b3c0e52a0a200f63a7c5c6526c9 +5b5c1d60dcef4e90866a824a1dbf3e0cb67eb95661dee5f17aaa146d04c99c8e +eb8517229cccf5d0a5f3f43f21ef18ca68520684f4761fed62894555e05fb132 +227566441065679a17fcaae3a15b019606f3ea941b0b78e2fbcff48c0de253ea +61618f5db64098bfbfe1f4a6e05ff647c703a00b74854bc4a5a60dd1672a7e66 +44add97c02f2d128a3b15363ccf61c126438aa96c826b2c0577402fa56de3cb7 +00feb43366991297b770fe2929bde6d1844e6b984662db62323138662c9b51db +43dac961a766a410c13eb45ff6422d6f9fd69944e1d81801b19bb38a2f30fb54 +e8deb99a10dcf354e4065c587c51feaba35d45b6a3cd3333a5d8657ce0e7bc34 +f0729cfcd090b1ad620934a99d06adc5705a4bd2d2d57ac2e82a35f5e030eb1d +9cb217fd3190461954921f1b5be6e6d7337cd694ad30d7a3e10f672b2f74a717 +346cf7fd86aab5ab941b926a243c6aa7b65fdca52aaddfd6884cc1fd7e0f4a2e +d8ce9c4921d6fdd4c57dc64a250c2dc886e74e9bff336b8c7030ba4a539ff79a +eb67d21bf9fec6d9ee9af47b3dfe95a44c201836d17e7e08a3bfe561908b7008 +bdb306831f8bbcd7e675f7db95ef50d66ecff44dabdfe6e89972cf016b915b6b +e3d1aed962e48afacd57007efcfcafb0b4d245ae4c579676d98c1b21855fbf9d +c092a99f788b140c59d0288560c53c6522cffcaa9223c2ce88b8fb8a7ad5ee45 +6d0dc8953f306f6913d3be67c54a113eee14ca9600b7d6b5cd99f2ea18cf4984 +94bd0d5ee0cc10c95c836337733f1730be33151383d7946afbc24e2e2fabd346 +99ffdc3d1b432890b662126c09dc56a45c6491b580dd5bc4b2a7f464fd7b00ed +fb8d38ac9ebfb8f196a6549161931d723151d3aa180f77fe9130e059e12d2481 +fca6ae50a370e226998e785ec9c5b7dcfcb23263ee7bb621da05eaa6fcb806e7 +f3d0c20bd10b0b2fae3ea4bb4180ae2174310a283c03f5c7733ac937a07198fc +f4cdac0c218c483533fdd15627cb3c93296b714159bbb1594c3c3ea59ba09185 +db3b2227ac0212da4c2a01ba24c41aa1a075e42117d70bd220b8f03c27f6602f +9561dc8fad0e5e16c1a035da4edaa1c8255cd36340a0d3ee2731ffa65be2037d +53dc9c6bc9462c30449f4df70c19981d5d144895518bee4258934399f91fc181 +ab0bf02e7debd2d7fc43f9e0d3cf5d3c816a9c10398b3336eb17cf6c3ef20f3f +d1666cdfb8645f4f2e5f554a4036e789dc98eadc0d06bf99c7daa4203a0a3e77 +30044bd22f2ed02193191fae5ece9a623c7c9b22442fc2f2274da08a5708f0d6 diff --git a/crates/pf-vkdecode/tests/data/test-25fps.nv12.sha256 b/crates/pf-vkdecode/tests/data/test-25fps.nv12.sha256 new file mode 100644 index 00000000..c2607b9f --- /dev/null +++ b/crates/pf-vkdecode/tests/data/test-25fps.nv12.sha256 @@ -0,0 +1,267 @@ +# SHA-256 per decoded frame of test-25fps.h264, DISPLAY order — 250 frames. +# Each frame is the 320x240 conformance-window crop as tightly packed NV12: +# Y plane 320*240 bytes, then interleaved UV 320*120 bytes = 115200 bytes/frame. +# +# Generated ONCE from libavcodec's software decoder (H.264 decoding is exactly +# specified — every conformant decoder is bit-identical), inside the +# pf-lxcheck2 image (docker run --rm --platform linux/amd64, ffmpeg via +# `apt-get install -y ffmpeg`), 2026-08-05: +# +# ffmpeg -i crates/pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264 \ +# -f rawvideo -pix_fmt nv12 -fps_mode passthrough ref.yuv +# # then split ref.yuv into 115200-byte frames and sha256 each +# +# ffmpeg version 8.0.1-3ubuntu2 Copyright (c) 2000-2025 the FFmpeg developers +# (cross-checked bit-identical against ffmpeg 8.1.1 on macOS arm64; ffmpeg's +# two "non-existing SPS 0 referenced in buffering period" warnings are an SEI +# preceding the SPS in the vector — harmless, no frame is affected) +0b90656d0073d0f5ef80c7a8f19e08e55fb7d5d964cec920e010472c2bce2a4f +426edeacc9a3bc7d155ef638b9d83f76c06718494b68c679b86773a1db4b0369 +103384f7b1da90c8a4207195388514955df75b5aa0c6cf4c3c0117bff4bfe65c +b43e92c17d32801461344bb9727b6b508ab3abd937b592522fa13efb254d5582 +64d69ce1f948367761a48ee1bcceeabdcc58b4052ae74f8cc46b2c3064874c3b +ef3c87d5812386cd391e0b41baa887bea57607a5781e8de5b2efef6e27c1f33b +641d0c4b9ca37198fbf1294eee0205b03392fc872f0f0856335a4c797169c0f9 +fb496f343c222e2551e72440af276fb5f6876ef0397a16d59ac8403f484749b6 +6c79338dd34b4316cbbcc27b274b96d60ad7f9697b3ebf883697a5f3b792bf1e +b36896f1f70c8971824ecbf247ca4bae402e041ef87a8f8e3d975a2f183f3e68 +2052d220bf06fa1701217806b5cd9bf33f3eee536731916ce3e6af87f62f839d +2f6d10ec3c7359f60b0b87718a66d935ebe735e03d2f2e6548873d2b0ba26400 +0556a84be0d929432712c1e5b43e0bde63222a7168300a5cf41705e7278188fb +652c512e9156521d7cfd3c235eccb2c3f35138104523cc3142ffb87399e14e5a +0a670d6fd129aaa946f9b3f514d741f12d6a764627e676f9a0ccc8914904f145 +ad04fbb191208944069ce66b8ef7d522d4c9600127296f64a1894f5488faff6c +357fad51f7a789022e20b6f3b34594643b85cfae180a164a8eb80737c441ff4e +244e8f5b209082a7ceb943552fde977673dfa806b1b4cf6e8e748a97eaeddff3 +f7b904b81ecdbc25db60b5952b9610da446467874a8e20e702d559b828929824 +e733ed049d880fbf0d620d6e2add3bbac1ce91c38f22c1c23730ad3a2e3501c8 +bc092845dbcb9e7a21aead8e2567db94a2ae6a6495897bdabc515c6c647bbad0 +45f101681599e69e0adfdd71efd7985711d22d36b3d89292cb9d6eab0939c2cf +3859c9ba68a834279ff0feba9df74f6055b148a7bdc557cdd486429d50dfcf89 +8bfbd3b025bfc984090dba2d70d76d01e3413846bab9fc58f9fd14ab366f54bd +6d5de7d54c939210410cd4a6bdfe2ec5c1f5b45a236030f6f0da708507c8af88 +e91e07d21ab7541513083b65d6515139c77229a4291ddf595f37c1624784cb88 +c9f8af29896ba9c9dc4644058861e35add58b7076306263e6ea702c39ec8211f +43698f8c167d9712a56505df7fbb7e7dbb72dd475b8c350bbd1aa8b7d8bd617e +0e0dee1d1cb9a21f2d3a541a6f2c59dbb1891ee32a07a6484a3b2405415bb0b4 +8e0610370c4c76bfcc7d7265c47b3ed1a362b59ebfe06bd7b6ae156028e28b84 +c41b96d91b8a0381d0fe9d38cf75fd9476ebc391323407425b2faa9cd3e27f1e +ad9e3b8c6f77d7007ba3ec783f1a97bb5b20fae9f38392f36b72d4eddcbf3d13 +ce8da4c829ba77a6126b4b0a5fe844ab9b6af17e295477bb8990d1d1764fc712 +c2750f8725482d14122acd3b11101d574c0962d4069c8b3a6e124ec44ddc49cb +6097344d5b3bec003f118386a5b002e1aac8e2af25a1fee2ef1d5a1f84a82afb +514e8b635a72344834f73929c86d1f3977160f74234e1185d94a6c639983b0e5 +70e247589ec800cae250fcbd20c20d6a89265961e64fce7a6a2b0193d5f54db4 +7fa9e6c2dfb413840525e7f9cb9314156705b9c68d3eea234fd940a2745be81b +7503bbe71d90d54ba169caea94a381cdc33311f4075a4803188531762d68f8d3 +152bf0bfc1139c878dc156ca1cccd7c4deba8776f5daa84281ea158ddb373d9c +d94a8273bd427ec794cdd4314cf89c494c55845e534b49a9607a3d3cca19c23b +ee7e890a13c62f68fbaa22ffaa42e8ee4eccf54ee67c8098c4dc0026d988a40f +f9a23a95b8833a65e75e8ccc84f273e58d0f902a2948f9c8855c90c214186881 +542fde8cb98ccbb13cb42b2fcb3cbb92a4d998142b5a32da1cffa241f444f1eb +54ff4bd22812bb7089fcc4e2df8cd8a3397151cf090dc6e5a7e2185c177d7619 +be1d1daa788a71906e9860faa7803522f9e01f462c05cb9c2aa1fa741df12d17 +fe369395a5ead4741ae5a3e8a470b2c2f1f637bfb1e3bdf2fcd5361aa7f564ca +faa51b4103b04885225ef65ebdf79215c4733c9b7f645ac6bff9eee6646e712f +6f69723828dc2244af8f5443417aece5e4839e56ac0760d0d5f823bc9b125d15 +71989cb65be483ada531009f3b8a16bc842573fc95e3199e9a9e826eaffe24db +da740a8a5dc37df5044c0c573c4ca398175a229a591c3ec6832f9fb274ba1103 +30fb23368a1922fec1626f73fd5c1623f14288b4e678472e2318bdebecddeaf6 +b95204fe9ac70f4664b631a651e95cd963ed76e1df4d32b1891c8d4af6a509db +e162aad9b481bc038cb987a89a0e024f37a389f3331c3bf6dbd0b6462e81eafa +808562bda2cd82d490c3485f9daf815c4a5dc7fd1fcc0c6d411dd226d2f0d9e3 +01fbe98468e7d810caab30fa24da03d03c8cf0d8d9ebb2a474a2d01536b4b1a6 +ef2e100fd95b7a6c562cea6c6055355f96cafc7a99d8fd9a77142fafe318e57d +a4d2f9a3e6e0d131e7fb99475af549790a8f6770926f97e89c7afbdf0e3ac0a2 +9c5a074ce591cd6f043b7284df35c2ae06d4befecb57622c0ab0dbccf32f6dbf +c8e67952dc7f7a1ca1cdc9432ea2ec20c361d5f6dceb4ac5a307908e7da6abe8 +a22c0d543bbbd72d5eecfe5994d9c7c096d358e60b708190408038b96e137736 +383367dd08ed34208e5ff74efdcdbea9b19eb7687751950cb419f08086892008 +70776046dbe080eec944c3170ba37e9a236ae1fe569874efd0ad298307127c3e +8e3a48cb61f5ec8dd5a965ceae8974f0bb65bfa6b5cfe21dba08ef3236d0d199 +452d178a5f88b320e0d351cce506c4b51ededacbc4a9eaf80af22c2b11d8a3c1 +86b41c15ea413828f58580ed11c2a0444d2f8d15ec6a59a5fc183ac4821b1f9e +6b9fb776d125d421221f9949ff6f0b0f240c1f0a85c51a69e060023ead53203f +6544223a981420acf6e34dc357716fc9da6b9a0497cacb6cc8fe0c8aa12a7bc1 +3e081407ebda818323a9346e3a7c061b12976d047b642619cf8625b8d6a65ad9 +716c7d891909fd42aebb6ce1c133b7713e433d276c74874b56cd0efa2c05a762 +1002b6644c41b51cda605d36a74b199dea056efab04473aa9c2df6d895c17977 +fdd01c9cb5bd9d5a0ebdd5b30fd33f15274f38a778f9cd870e1f24154fcb2d51 +2cc968f5af71e3fca3a539a1d98abc2ff5e3e107ca136c8f903abd62f8588ab5 +90f752b9b747af113b6a2eceb85a23fe132e777f63b0a66a6b9b31e815c761c4 +1176f465739905e32e07fd3eaf42721ee6557530df277faf9d2ee65065291261 +70ee1d5922bb80958d0d0e548056ec3ad52553a1ceeb05fd453fb80b8cb52f37 +47500975b2477ad17a32b88c5713939b3bbb72b943f77df64af9d26788c15633 +677bf3b75e5bbc91c7c861234cdd69b78d454bc7d60cc4ef4466ca1aaeb0cda0 +7340dae25917ef0e9455470b71ee9828bb0257522de0b69df0261707601abaae +66366a42398f8e2fc33b5c398c637fc8599f1c9095a591ec5fedb245b65d8259 +6b10f4704a9305ada1a188cb1344810f7f3ca860daa654d99fc089071d9efd07 +2841355a13f0078892bf187a3e9a26bf933b0f5b9e44ddeefdfcd9b5e4eb52d4 +f7eefab9d7850165142fb013e38f06c8ded9343e3de7c99ecf625d7eee49df1c +eeda3ca26958e168696869fd60295b91d36f245e75fa2c1dba96eb015d0c5ff6 +9116997bc75478e08064b06e564d908098b7de0df8c48c1b3f85129099cc2284 +33e7d7e773adbc6c90a1e2fe31064cf40c55de325e605c39ada460ee5030dcf6 +03df8278481af8c050bed6be2a20d40321f7d5a1660875f3bbb13967b77f7dd2 +cb0226f67ec08a8f774efcd98ffe0af370ed08166c6b56faa2e530b0ee476953 +dc00284e63283d9f3f1767f306b0cd1b95cef54c48a669b8566f887fa27b4c07 +343c133465107f0180f7f0a91096d4415bfe35bab6fb973331791ddd359418a6 +24a39ead65655210805a4e24febea47fe193a063925914fde70c72409391f399 +c4a50d150fe1160bc355b535c5d869a6a9f3707939339cfca0fb1073fe367ef6 +7599b2bba47f8a79a44174dd711003f1431ff944e0347c6496ef81030ffe97e6 +e2e824df2e1ee1b45921c44f132b62a4259d2ec904e92216d0872229991fc223 +6b3d6e6e268badc21cb8d890ad545bef9524b9660ef3a42b3c7d122276476930 +12db06cc6fb3d84453e9f15f73d682ba1d0caa5a27784a386abd753b65c27f4e +41740504395cd80144ae1a2530fcc7dd73c5db46a8e47f75478bfc9fb2c47366 +8a64ec8fa1c6a5a0dbadd11f6775b5e0ef4f455a33425db2c7d95481d8b1a599 +9a1b795bd182ea5ed3cdd7d62f4573687893f8955447f90eb23caaa0c4228955 +67f1a041201eab6471c8e3166220ab523ff1ccd67c049278555e47385bbbe333 +d868eb283905efe040a252eebd84079763285b195e4fe045ae700950ec7b3b21 +42edd6516283a6687a1de7e8fb98ce39240af44b0bc9a49b1b5786b5a8f0ca8e +1ff6ca0cebc0bffc1d6748b4a137d75d2f8181855e4ed14dce0f600b21360e26 +e9a5d3c6415771b054396ab56624764cda12ee0c3a2d79b300d72c2a74171e6a +343e81d501c8abc228fa19d8c2069dbac9f2efd576a8cc09ea82d6b18664a79c +883329828ec3a2804d49afee9b701d36a479b3975276767919ce3c691c677f91 +e379590cbc0f04bb49ac1911964e072aebe04352973869e017d65bbb147b5763 +04727f24b045201d199437046ea84d32312af19d9962539465e9574e3398fe52 +5b11ef561f154c89332dfa9781ed58b70dd6445b8ee6cda542fb3c208ad9a1fc +8f37f1006041e928599f4cdb34bb4661c91d0931057de16328fcc4b0dce159b5 +413c97ba67c2487a4266801f1553c18d4d0ae9164d41117a31ce33888e10cee6 +dc09400dc2a5e441319acad4f1056a922ce43eb3977f7cf746452f5fb95ae833 +72eb2c8c57cc5d66a8529965542142bfdb6daed539b2d70d674272358e1aa144 +6dec84a209d299e48de98efbeebaff005d01bdede477bd17b4dab28053ffab22 +e41eb98c56fef112632c21e11fdb5c200ce7d233ef02f5105687912bd7c73489 +0d2b44581f52f17b5bb34f63dc9bb3d8595606dcf22bc92b2fa7a2701f330bd8 +ae805bb2be43dde5a21132391a8ec4db4e5cbfea9bc8a252e1517f5d6880556a +a12d311502b0163241d02199fce8dc178fb3c78fda94bf30b0011f87debaf8b4 +db218d273e3ae407fdbebf9a461b74e41febc233f95ea586a17ed5fc63ad0ea6 +b3ec4e7f77292ee0a222cb4a894542afdf0204a18301a1a92a22d5900d2c5e3a +4b4d5e2e64059efe43f33a50d35bab01bea69b5c59920a903a337078453257c1 +9a3768e9179be20683bf0145e6ed3c831c86cc283034fdba8d836924eab32229 +df593ff95f4ce6400bb0725e4d1a37eb563d513a7e79efde923b7fcfdb418010 +84d26ec9fc8b3d64e298d5f37eb1b2a5d0ec480a7247c00710cf6e8b6a3d3503 +efef3e26f95fbe1ccfb1e1d8a2e6aaa3aaf0e978725ce755a3e26f01e16e645c +2bbc737a1758d223a2faeae1881abf8c21caf3db2e8f99f1b741dadaf79b9420 +a2ece09eaa2c14ceb75d8fe9b73bd9e5ecfb2a3a15a986a4441823e990455523 +01e2a5b57b232b8908f72b0329bfe341498fbf6f4d0e3538259cb69f8cf861af +250a6d33f7ef0de3b4aa7e6c05a337bf5afb0eb09f34cbeaf1a581a5deb15b3f +d2af6504ec674c17ab0fe17a74af59dd7ac80f6f98f07b9d29fe75af9554dfd5 +1685a738835865cb0ad9ee40b1648be021ad542c0c204f6ac17e9f5b30af4b6a +0844e227c0320b3e59082c5a7c1c2f0b7ed1128974f5076f790e6f316425cdd7 +2023bd089470332e859e5bab766408bd8a5faa4abb74a5f454df4a66fbc067ed +276c9f17585c5b84872d26a212f0a25191aee4ce69febffc4e1ea0ef3954858a +60dece81f32a1a7aee0e5a1016c8e8c3eb2fef7e08f04b560015b97bdffc596f +ba9b621cac58968944b23825202b83ed9945de2983768d09d1b69b278c0cbe6f +6e90989c6314749661d53c5f2fa4f02e342a80207a0930be6424879a9086ac6f +905d02b8c2de44a29b68772ce6a3d117b90f13d2379f1159d3e73bc0ba9a25e3 +1668df55d8f04d41aff1996dfbffcdaccb220ae625a04dccbb4515b90ae8f28f +bef8b50baebfa3eea65e3f0edd98ef185321c27816a09e9575169495f4581c7f +824ff1ef4f93aaa6e568b4eb32c724bb69b350171230745ebc47740a75472519 +37e42144279ff5899f646a5de9896bc2fb887a88918e7b5b78bc207a56e52d4d +f64d103958e08551e06a5f72f22b76aee671ce76b4f986c666697e08a3fc4e97 +b9647e2405c3068c3a0743bd41e30d554d4be6db91183805c424b43c0e904801 +c2bb17fea55efa58210ab3ab2636aa6f828bbba1d37d4906c2da57ecb5715c93 +8a71fb38b1fb2074a8a1f68967749201b7661e4e97ab7919a25f6f145929aaf3 +9af8cc8034eada25787b6427e9d24ee7f618d962cf3624dabed91776d79193ef +4679e1a74d207bb95fbc0605a999e939ef4844f1a1038c39f59ff855f35621b6 +8b8859f4cc9e8ed67feafa17a1c7e8b66c760b5eca9c104ad33e6bd8c5d08a37 +c1e427ff9ec73be4e6e64d3fcdd58a452014d69aea16f6b72ffd93027ee7a533 +35f09c7808be14418de04ac833ac846778efdff42c28ceef54e06fabb94ae9e4 +e5ef08f78698d441b519d53880b5aa366f8eae04d6512e267b57bd7c9996fd1e +38a0a28c91eac581647ff745d6e497be18c31fe374f61ed1d8502bf299572693 +6d451d866460c108bbc258c29c2de596813d88b9835b35ef92f495265b64e302 +1a9c67bbcb45c71a5ec97976c7a54a647beb3c445257022a4da71eb2a14667e3 +c43195cc687a785db43e64feee841c512b6a4c8aef2bf6420972700dc8f925c8 +b2d0a6b3f6785c28ff537908f9a7fbc30c3025223d76ba2a3bad622f2313ea29 +a2f6b2f9455449a32ec8df50fc6ad2dd7a0ab04888baf52779f2d84560b9d15a +e2b0e08324465c9489cbcfde87267a046e0d30a409e29dc32d4fe2e62dfac432 +8ce434e02fc4a79fa83993cb19f808f159ebb06d342b0723e3ddb3c2e4c05f62 +89786221bea32a38e6dbc27c04443e47da40173b3474b15d860e49606ced09b2 +258b6d4c01e0e3efc81da1e1a50aa607fa23cc1f2584210ff4317aadfa4babde +4732a7ff59f63199be7661519d8c38c9c401c8bcd04bfe94a437613a1aa10150 +6b174a5a8c75d0c6b30464d6924be0670e89f5dc6cd665849b63520f05d126da +b496050efeb03af2976a90dc8344a4042261b668114171d6b825e1eeace3b7cd +4997d172ef5f06d03dcb13e4d6b0d44efb3612bee00572d02755b5552b7acd2e +7abc1388e80a48b98ab494f5e679c3afd6ed48d93bb83c87d5d1bdc77315cb93 +7bbd1c5daa4cf65ae29065938fc9a89477900875d2f4a3083cfb11d5485289aa +fd2bdd77848ce41da3a1a05bfa34f7d05c2c0c1eddf9f32c3f99f58879c12492 +355f35cb2e8abc77f01db2d9dfe0a87d62005a3ac8cd13d401853e55c2b69b61 +26a6f4a4904d479bcdb09b3a8edd459e618886cfeddcdfcb543cf46c7d10c76d +b5dbb623aa2876bdc1d13cc439da72d9ce7de68ee6873a57c4af18f28e65dbd6 +09fa44af5bf92dd67362dde07a49ca4abd75afe1fd0b08bbdba651264db26aa2 +52c4e4eea32a5ecb6b9d5b958ad78ae732b1d821c82b9c82450543c998b849d5 +ebfed9b549643aac86d0bea6a520ed704e046b5bae859766e768a9f378aedfbd +c80e01ad78922a195593ab0531e6ee11600ed43f9567e548c5a7fbb8b15c3de2 +a4b0a3eaee44d1ed069b6b2a99c6584aa1c004b8c8d3af3519709eb8c6caec65 +f1f7b5be4b662303f43d46fdcf6718f4631b81af67a563637db4727bf4462a83 +8dc72ec654ced2a029b475c90d36e36bff03a1447946c573f8a109b8937f802a +198bf80c83d29db20a7e72589d4b7150cce4753a22048c54cf19aeac62ff8f88 +f8f483b40379e562632a4ff062d9605b2dffa4dd9b4d889cbe169fbaa969bf99 +226c197633ecc8d28d4b8849df8d108480f2e865c98210ca06b12c10ab3ec086 +0997fea45eb7180ebfd766cd9de64e6a1186be152d4e085da6eacd93590c8ed5 +84f4d784723e1160580245d8184fc0be827821f9c37342e9b405bd9cef161e7f +20aede1edf420efb978355c6c436b3d4baf8bdb2caada92b4adfbf1629fc79a0 +e6ec12769e54cc3025dd3c05ddc61d10bba59fa23acea9e513d1d5c8793f107a +e6011532e1fc39801b922983e395c6de403329d2214d202d156911336f8f86a8 +f685605a0bcff5eae9c984f2d01157d9c745adc2d070393c4e49648145fc8287 +438b1ed285dbcb8e1dd5572cb38e48f703ea054cc486f849d55add0f9bb80115 +846ecd0ec2b9d89dc1784f483bf8a151bc5712b4bac5b6e335b0a0c4ff56df83 +cdcf4f1db20116cc11f195d62cb27ec29b50c2967e1ea9745857d19fa2b3e3f3 +7bd94902f85dc9afd07577cfbc4409db0e43b09e9ca5b408a8a8e83e09406c5e +9edf5a32cb1da57fef79718bb37ff22b7a2c23d0c19dce567cab9a8829246c2c +6961a882ea08aa2957d93f8d70ecf80989f6b2045e232362fdd88e1fa6e2a8eb +8f3db233155aad751018b98a55a0d823068b6ef981691b8173ee4dc63add943c +884ed0f0baa64ccf52519e0564ff5fb42205799aa558d9b96ebe5f1d292bf24e +eb25d5728dbb19f2f9c7902a56f70dbe34d8f59c214cbf67316221b6d134472d +d43c2c580fd009720ff3896cd835e1f0de3bed6f765936fc691661e6ef78fce3 +66984354174c7faf0cf6919ff27a745e4d04cbd00a71fe6fb10a9b22c061f692 +2ae17293c2b8e3ab8492f2df93cc5df458ad5f2084a99a25d65498a4c3ac5915 +c4ebaee307f1bac9679eb52b716d6cca1934c51d4213b83520c87f1a4d3dc354 +c6a4f0d671ac4aa25acd87ef1f912a8ad56f40542b3edf55331b5971734db2bd +08c554594139a6f5dda9c9e62279fea24100423c151370952f84ae0278c08e2e +f6a8404804d821a105c048c58afba9baa123cc1430a40461410e6405351554ba +86b5113ac2b5b3541e32db3bd0420e4724d3bb6069ad9071577c65796da2f56b +91fec67eed692e5d1bbdc97bc58ae4541ea21decbd79543feec575cfffe8920f +9bb81e4d42cf106ae74a5670d1e6eb47ab0e90f3fdd22fbbe2a3096919dff8dc +98546509149cccafb0e304fa8394e7d16b4cca97d9481ea0c5604932f6fe4a9f +2f59aa4bd67f83d2f247026af5940758c9b7a8a91903ae6d6d09aeff8699eb72 +d49793a2d3e4dd81680b3356f1b807870dbbf01d145764d0cee2667a105dda0a +da6db2186f97a7595f8c8651c9df2f3002a29e5bc1d4b45a4da47aa3b0a902c0 +b2b198faa26f1928030f0e612d8a447b276e33214e6d461f487bd1668ffcebb1 +6616b7da478711919a1135527f0d3fc3466d4ddfea020966c6ff5c86d76c29a8 +9fdb7f7812019991c0fc2a94e43c9a72ea37ef0f51103aa69b470da581952ad1 +f26e2001c3b3fb417e595934886bf577391ff78e45362df0202cafc059840bc3 +ddb6e82617370b947a01ed35a97f0b222a69cc82205db763e313d31313afcd90 +cca0fb8c2f22f5bc6c72b44c8ceff3a65ef0aebc76b5c9df444e00822d9f95ac +3f1a3829ea9a2cd6aaa16a88482e4bf48462cdf2f09e5882a8f1060836fe6c02 +264f5c0bdfa9bac5cd12204ec220d5825a199c226fb02d129d163b0e5e127075 +b51b71e1a6f14a3cccdc7962b42ac8e66b3513dbd241ea441dda5c50fd907a69 +68bfee7e76375143067494dba0056e091bef2c0d16091f9e64a60b377f074888 +e3c8d0c23a36df5d8cffed0e1348bea745dd3f80aa21bcdb2ba13c36ae63b2e2 +03bfbbaad233af3690fded6cd3d8ccbd1807e6cf5bcaa2f3e7da3488225d953e +507e46d9471718243f2086542b7cfe43c9cbd2cfc1dac516bb85180bbeea8c1c +a86053a40337cbb6199b02546e7ccd1ea9f9823102b7bcd2a847788afd2bf265 +1d5e5ea6fa92890a6ce6566929094b68758f12ef9b405c9e2927ce3cf1b90a76 +3bd7bc3ceb7ffa284adbd644d09c1d3b298b4b28b71fbc254f7140278f18a5f0 +4a5f25c3d9f88fbf86844d9fd29b3687320866ee9968d1115ecbf9d2c397b3da +f7167f09784700563d3b314046c7341efbfca80a48b4c830b8d1d07034d19d59 +9871a9e37d3c2c51844e3a07a6b75b8ed553d7fd4d6e2af1250e91366ebc174b +cba6d7b129a290fc15c2f85c180702725d0f9abb82d0c658f339e9f973512494 +41dc29241aee4b80346e5df268806daf34d6606d831ee8088bba9365c115b7f0 +c8232ee1f0a86dfb9101d968a06361ba2ba8af5b864517b3653100bfb70de3b0 +be3968003b5f580ed732414420882633e22c683811e253689220a21a48987c23 +d952bed0f72c7d84deea9fcaad7d64555004114ea3ea3d9d945440e3ebd6286e +962976cd6403603d59e71a1ecaccd75a8e3f2e1e090f96415b9b51e5ed1e0c08 +ca1160ee7670730d9f413dd199db1d9424ac03ebc3b9504f1ad0e5874fa65c29 +716d40bdc4b850d3d101150863d1d1547dd629648632e53f8de856e5c0c94367 +65510bc206a782477fc102584341525b5e71d33dedc67a61a7c4d031eb003b09 +a1c8dd0345d5adbab643f7b463933e0ae18daec04c5aeaad7bf8d2ab81634316 +e6ca742bc83b756bf25fdaec471bcf75d04d19313d758db7d315fb37fa7c6d4f +54efd66ffd75ede95ba9485a68aada5fdffe46ffac8cda030f6ff0c8af0ff0cd +1567cb38f2df05407c5b2b03ac1b6abdb3805d52aaf2b949406450a9231c762e +0b70603bc497317b0395c3e084f7c49ac5f749a84dbeb38f23bf5b2a7e3e632d +a22b607fed5e95281d1060ac50a9def45233247d99ae72838c7caa9bf977533f +7d920490373e40445ac28d9f1e19821b19d2da2196e5343b7ec3a660a41119e1 +cda6c02cdb38a68346d9303a99c921b9b6253c5ed795c9ee4927530f53fb2ca2 +31c41a44c6802cf7880ab64c7d5774c4c6adb5a80bfa2d9a1066a5f7bc170097 +cf8aa6ff1b51cc5066aff47668d58fe2dcbddc3a2cce07c3050c3a9f3cc00808 +688ed8a607cc26680d24a7f3183dcfab7ed465ef00b5a9eee15d0d4f5aecc3eb diff --git a/crates/pf-vkdecode/tests/data/test-main10.h265 b/crates/pf-vkdecode/tests/data/test-main10.h265 new file mode 100644 index 00000000..a065be5b Binary files /dev/null and b/crates/pf-vkdecode/tests/data/test-main10.h265 differ diff --git a/crates/pf-vkdecode/tests/data/test-main10.p010.sha256 b/crates/pf-vkdecode/tests/data/test-main10.p010.sha256 new file mode 100644 index 00000000..98e0c075 --- /dev/null +++ b/crates/pf-vkdecode/tests/data/test-main10.p010.sha256 @@ -0,0 +1,83 @@ +# SHA-256 per decoded frame of test-main10.h265, DISPLAY order - 50 frames. +# +# Each frame is the 320x240 picture as tightly packed P010: +# Y plane 320*240 16-bit words = 153600 bytes +# UV plane 160*120 interleaved (U,V) 16-bit pairs = 76800 bytes +# total 230400 bytes/frame +# +# P010, NOT yuv420p10le: the ten bits sit in the HIGH bits of each little-endian +# 16-bit word (15..6) with the low six zeroed, which is what a D3D11 P010 surface +# and a Vulkan G10X6_B10X6R10X6 image both contain. Hashing yuv420p10le instead +# would compare LSB-aligned samples against MSB-aligned ones and fail everywhere +# for a reason that has nothing to do with the decode. +# +# This vector carries no conformance window - coded size IS display size. +# +# Why it exists: every other golden set in this program is 8-bit, so no rung had +# pixel evidence for its ten-bit path. The HDR legs proved a Main10 session BUILDS +# and runs clean, which is not the same claim - D3D11VA has no per-picture status +# query, so a Main10 stream decoding to garbage would log exactly as cleanly. +# +# Generated 2026-08-06 from libavcodec's SOFTWARE decoder (HEVC decoding is exactly +# specified - every conformant decoder is bit-identical), and CROSS-CHECKED between +# two independent builds on two architectures that agreed on all 50 frames: +# ffmpeg 8.1.1 (Homebrew, macOS arm64) +# ffmpeg 8.0.1-3ubuntu2 (Ubuntu, x86_64) +# +# Vector generation (libx265, Main 10, 4:2:0, 2 s at 25 fps): +# ffmpeg -f lavfi -i testsrc2=size=320x240:rate=25:duration=2 \ +# -c:v libx265 -pix_fmt yuv420p10le -x265-params "log-level=none:profile=main10" \ +# -f hevc test-main10.h265 +# +# Goldens: +# ffmpeg -i test-main10.h265 -f rawvideo -pix_fmt p010le - | +fe40d5f2aac672155dd200cc64ae3c6c47dc90e6dfcc59f634db0603ea553068 +ba62cb199082881c76eca0bccdd41201fd9c1142e97ce89747ef64a836c31f4f +2494e3695cb7be56aabedbfae3dddd10ba0c2e0ad08b51bbe03d7e2d26baccb8 +8195990eeb8475d01abd2f0c27cbf20a3abe8d2fc273996aa66528277070aec1 +ff45876b191c83688a37898e0fa9757b03215f2d2cf1a1a50f9899c837873a04 +a684b50bf67c59052c138cda318c83f85d55132c8f9e8e28a430ea6731fa59c5 +65e4dea6aaab1e73a942b3c3a9334cd2a215c36709a4338f3dfc71bf8dca5819 +9d8deda227c79a931be366370093c42c5e8ce417044069c883d144197eb089be +48dbfe1702045aa13e3d6f7fa3f95aae2e863c0b792f77a444504c373bc28313 +f700d0bb43c2df17e49b036125f313ae41f31ee75f1d6ad895e36e7145a402ca +ec351982a1e14ef5edaba99d578e9d77fa3644ffdd7101d5d3d95b484313b9f7 +0526ca0453f5eb05b9cfb694d5a0d30cdd3fdd9e7914b4a527b33fd0c2ad3cef +07fdeeaeb8de894030c60a4fcfccf116488e9f9dbf19de59963f279c9b4bf3de +ef15d30d0bbd9f2bb454a10d30451151ae4300b0b5844f8dda2c06f4feb41d40 +f8e7bf24d3d8c3938c4a3b0bf7f2d81af457ecedee22cbf0cfeed8e4e67baa95 +fb48d15f9462cf359acab5ae48c91176b7b2931a57755815d4539bc4f3e8ad4b +a8bec00e24b39b55fcca51fde360fce130400202c1ac9370657c60578536acc4 +f3fe62e6ed17d4e59dc16b7c8627d494047163e14e5a0521792d3d9680a06fe1 +b811ebb99d9339e3fce4a2db070a315385ea7570f01a3d18275bc5a97a359850 +b38cd7adda00d5f8e33b7cb8e0d18fdc94686ab16e72bbc67ecb6521d19ab412 +cf7946c7be96da66e256e2235ee0f7b2a35e1e344fea2214e2045e8bd5b548c7 +635916c2a10c1086efccb71c505a71f4a77e2007f5df165f0be15ea1359eec5b +e70cc81406480acfd686091d023ddb0b5315230fa2449a5d73ff403ea0f6d532 +825301a5c79ed4b28f503af92ae569fdb66c53de60c661b93d96467cc2ed6b8d +9739d731f87ad1dd21d26331bb4f2d28857815ed4c2b1d45dde2744da1efa60a +313d001c0c2203eef18a2386c73e63fdf9ac0417dba4339fce95b11c73b68b84 +74597aa7d898c4ba79f88fff2c37cfb54dda2f9e9834cbc92b1b090ae730efa3 +ef22cd7812608173266162af239e845c66ef71d025ce17a4f815af33a8617760 +c6f5f5ce376f4b5dd82f9d01cc16081a352736051eb00b352cf3f4d15ca5de17 +a854b63bd8d288734548ddc792c3d67c5e3ec744264038b5f0a319038b129112 +f3a14d11678dbd8e4dcfbc8ea0b1cd3503cb2e695858f03cac1e9613744308b2 +8d6abe1632b2586ba830a69ea509c72645c1e7cb939adffba05ec846716d2ba9 +f809f1e5cff7402bbc281ef4651760858dc5e8feed6e815bdbe780972f03fe0d +43c64be95cfd7512a96f8d82e48198766f7eab292023c2b84780075cae4d0544 +4e9e8f2336bafb17267c2deb18b9fd4fc561b2df3cc0ab63a6654d8946a0362a +1003635166c1d34360bca984e31a06dea1583859d0cb515eac6c65f13629ffd2 +31ce8f6f38026317ed31bafc11c8ecdc5fa6c14cf72835126a6def695c4934d2 +eaeb25f2e4f6a881fc3d707a3076eab45c5605880c953f552591d199ba48ab0b +ec6aa73e67516f2e6053cfb3ab6df304ee4093190ef6b52442b2ad2cf598f287 +f07f7d2d5e5d48d37804e33234a0fa029cc7abf60ca19d21663ef8435d9c3ad8 +a0f5d8dab08fe6764eb46c46eb4320a105329b805ab6dd0f4ce0758b4fc10409 +3f98a8dc1bd2c7eca8fc39945195b16de868db1ff0445d9ac2aa74dbcc370b8a +0ca2d9f7251aa020c14a6548922e55cbf010d14c2c3a2a27db3eb8a9a4c185b8 +79b6e0898dfdddca8a878ace9742aa3607b3a968e56b95d3aef03dea3b11cf71 +59487538cdcd63a48a28e2682fd56cc6dbe67ca00c696ddf06b21f605dac91dd +dc98f561f0ecc58824123fd8cfa5f0c2ce96bf4fafa56ca1d062b00df12f2d7f +394804f6a890eeecf6ffbd7a54015cc248ced7e0a951e921a329d615522d3102 +7f8d56e2e57099e1cd456c9cb67748de521a398180b86f52e06bb6ef6ca8d816 +a860ede88b1f66e2d08a1755f6d49e70b553270c61e709ca0b1ca5adac3e1594 +c6b7df04a6292863041fe141f6a4cb4eaf0d27f267e733e1ba75a6fe3f8f05cc diff --git a/crates/pf-vkdecode/tests/fault_detection.rs b/crates/pf-vkdecode/tests/fault_detection.rs new file mode 100644 index 00000000..5a776c3b --- /dev/null +++ b/crates/pf-vkdecode/tests/fault_detection.rs @@ -0,0 +1,447 @@ +//! Fault injection proves detection — M4's exit criterion, on the CPU. +//! +//! The native-decode program exists because a field corruption was +//! ARCHITECTURALLY undetectable: FFmpeg's Vulkan decoder creates no status queries, +//! never sets `AV_FRAME_FLAG_CORRUPT`, and reports trouble only as log lines. The +//! native lane now has the signals. This harness fires them on purpose, so +//! "detection works" is a test result rather than a belief. +//! +//! It drives the REAL detection path minus the GPU: [`pf_vkdecode::AuFault`] — the +//! very injector the client's `PUNKTFUNK_AU_FAULT` knob arms — damages a real +//! 250-AU host-shaped stream, the damaged AUs go through the planner exactly as +//! `VkH264Decoder::decode` / `VkH265Decoder::decode` feed it, and the verdict is +//! read with [`pf_vkdecode::is_integrity_warning`] / +//! [`pf_vkdecode::is_integrity_warning_h265`] — the same predicates the client +//! conceals on. Nothing is mocked; what is absent is only the Vulkan submission +//! below the planner, which cannot change a plan's warnings. +//! +//! **Both codecs**, because they detect a lost AU through genuinely different +//! predicates and a harness covering one proves nothing about the other. H.264 has +//! a `frame_num` gap — an explicit, cheap counter break that fires whether or not +//! the missing picture is ever referenced. HEVC has no analogue at all (POC is +//! coded per picture and simply jumps), so its entire drop detection rests on the +//! RPS resolving a reference the DPB does not hold: `MissingReference` or nothing. +//! HEVC ships in this milestone, so it is tested here on the same terms. +//! +//! What it therefore proves, and — just as deliberately — what it proves is +//! IMPOSSIBLE here: +//! +//! * **Proved**: a dropped AU is DETECTED as *integrity* damage within a bounded +//! number of AUs, on both codecs — the class that makes the client release the +//! picture unshown and ask for a re-anchor. That is M4's exit criterion for the +//! parser-visible half. +//! * **Proved**: the clean stream produces NO integrity warning anywhere, so the +//! detector cannot be passing by firing on everything (the failure mode that +//! would cost a keyframe round trip per second on healthy links). +//! * **Proved impossible**: truncation and payload flips are INVISIBLE to the +//! parser. Annex-B carries no NALU length, so a cut slice is just a shorter +//! slice, and a flipped payload byte is syntactically perfect. Both decode to a +//! wrong picture with nothing in the bitstream to object to. The only detector +//! left is the driver's per-op `RESULT_STATUS` verdict, which needs real hardware +//! — the GPU smoke/parity tests' ground. These assertions are the negative space +//! that justifies the status ring existing at all, and the reason a session on a +//! driver without `queryResultStatusSupport` must be reported as unmeasured +//! rather than clean. +//! * **Proved impossible, and correctly so**: a dropped SUB-LAYER NON-REFERENCE +//! picture is invisible to the planner on either codec, because nothing ever +//! references it. That is not a hole in detection — no later picture is damaged +//! — it is a missing OUTPUT frame, which is the wire's frame-index gap detector's +//! job (`punktfunk_core::reanchor::index_gap`), not the bitstream's. The HEVC +//! vector below contains both kinds and the test asserts both verdicts, so the +//! distinction cannot quietly become "HEVC misses drops". + +use pf_bitstream::h264::H264Planner; +use pf_bitstream::h265::H265Planner; +use pf_vkdecode::{ + is_integrity_warning, is_integrity_warning_h265, AuFault, FaultAction, FaultMode, +}; +use std::io::Cursor; + +/// The same vendored vectors the WP-A conversion tests and the GPU tests use: 250 +/// AUs of real encoder output each, an IDR then P-frames — the punktfunk +/// envelope's shape, in both codecs. +const TEST_25FPS_H264: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264" +); +const TEST_25FPS_H265: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265" +); + +/// Test-only H.264 AU splitter, mirroring pf-bitstream's (`#[cfg(test)]`-private +/// there) and the GPU tests'. +fn split_h264(stream: &[u8]) -> Vec<&[u8]> { + use cros_codecs::codec::h264::parser::{Nalu, NaluType}; + let mut aus = Vec::new(); + let mut cursor = Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + + while let Ok(nalu) = Nalu::next(&mut cursor) { + let nalu_offset = cursor.position() as usize; + let start = nalu_offset - nalu.offset; + let is_slice = matches!(nalu.header.type_, NaluType::Slice | NaluType::SliceIdr); + let first_mb_zero = is_slice && stream.get(nalu_offset + 1).is_some_and(|b| b & 0x80 != 0); + + if au_has_slice && (!is_slice || first_mb_zero) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus +} + +/// The H.265 twin, mirroring `pic_h265`'s test splitter: a new AU starts at a +/// non-VCL NALU following slices, or at a slice segment whose +/// `first_slice_segment_in_pic_flag` is set (the first bit of the byte after the +/// 2-byte NAL header) when the current AU already has slices. +fn split_h265(stream: &[u8]) -> Vec<&[u8]> { + use cros_codecs::codec::h265::parser::Nalu; + let mut aus = Vec::new(); + let mut cursor = Cursor::new(stream); + let mut au_start = 0usize; + let mut au_has_slice = false; + + while let Ok(nalu) = Nalu::next(&mut cursor) { + let header_start = cursor.position() as usize; + let start = header_start - nalu.offset; + let is_slice = (nalu.header.type_ as u32) < 32; + let first_slice_flag = + is_slice && stream.get(header_start + 2).is_some_and(|b| b & 0x80 != 0); + + if au_has_slice && (!is_slice || first_slice_flag) { + aus.push(&stream[au_start..start]); + au_start = start; + au_has_slice = false; + } + au_has_slice |= is_slice; + } + aus.push(&stream[au_start..]); + aus +} + +/// What the client would do with each AU, condensed to the one bit that matters: +/// did THIS AU make the client conceal and ask for recovery? +/// +/// Mirrors `video_vk_native::NativeVulkanDecoder::decode`'s CPU half exactly — a +/// plan that fails outright is trouble, and a plan whose warnings include an +/// integrity warning is damage. (The third source, a driver `Failed` verdict on a +/// prior frame, has no CPU analogue and is the GPU tests' business.) +fn damaged_h264(planner: &mut H264Planner, au: &[u8]) -> bool { + match planner.plan_au(au) { + Ok(plan) => plan.warnings.iter().any(is_integrity_warning), + // A refusal is the loudest possible detection: the client turns it into an + // Err, releases nothing, and asks. Counting it as detected is what the + // client does. + Err(_) => true, + } +} + +/// The H.265 twin — with the one arm that is NOT damage. +/// +/// `RaslSkipped` is the spec's own answer (8.1.3 NOTE) for a leading picture whose +/// references precede an open-GOP join: `VkH265Decoder::decode` turns it into +/// `Ok(None)` and clears the warning ledger, so the client neither drops a frame +/// nor asks for anything. Counting it here would let this harness "detect" a fault +/// through a path production treats as a non-event. +fn damaged_h265(planner: &mut H265Planner, au: &[u8]) -> bool { + use pf_bitstream::h265::PlanError; + match planner.plan_au(au) { + Ok(plan) => plan.warnings.iter().any(is_integrity_warning_h265), + Err(PlanError::RaslSkipped { .. }) => false, + Err(_) => true, + } +} + +/// Replay a vector with `fault` armed, returning for each AU index whether the +/// client would have flagged damage — plus how many AUs were actually faulted, so +/// a test can prove the injector fired at all rather than passing vacuously. +/// +/// Generic over the codec through the split + verdict pair, so both legs replay +/// through ONE driver: the injector, the AU cadence and the "a dropped AU has no +/// verdict of its own" rule are codec-independent, and forking them per codec is +/// how the two legs would quietly stop testing the same thing. +fn replay<'a>( + stream: &'a [u8], + split: fn(&'a [u8]) -> Vec<&'a [u8]>, + mut verdict: impl FnMut(&[u8]) -> bool, + fault: Option, +) -> (Vec, usize) { + let aus = split(stream); + let mut fault = fault; + let mut flags = Vec::with_capacity(aus.len()); + let mut faulted = 0usize; + for au in aus { + let action = match &mut fault { + Some(f) => f.apply(au), + None => FaultAction::Pass, + }; + match action { + FaultAction::Pass => flags.push(verdict(au)), + FaultAction::Drop => { + faulted += 1; + // The AU never reaches the decoder — exactly what the client does + // for a dropped AU, and exactly what the network does for a lost + // one. No verdict for this index. + flags.push(false); + } + FaultAction::Corrupt(bytes) => { + faulted += 1; + flags.push(verdict(&bytes)); + } + } + } + (flags, faulted) +} + +fn replay_h264(fault: Option) -> (Vec, usize) { + let mut planner = H264Planner::new(); + replay( + TEST_25FPS_H264, + split_h264, + move |au| damaged_h264(&mut planner, au), + fault, + ) +} + +fn replay_h265(fault: Option) -> (Vec, usize) { + let mut planner = H265Planner::new(); + replay( + TEST_25FPS_H265, + split_h265, + move |au| damaged_h265(&mut planner, au), + fault, + ) +} + +/// How many AUs after the fault detection is allowed to take on H.264. ONE: a +/// dropped reference is visible to the planner on the very next AU that references +/// it. The bound is stated as a number rather than "eventually" because +/// "eventually" is what the 500 ms freeze backstop already provides — the whole +/// point of local detection is that it is immediate. +const DETECT_WITHIN: usize = 1; + +/// The fault period the tests replay at: five faults over the 250-AU vector, far +/// enough apart that each one's detection is unambiguously about ITS fault. +const PERIOD: u32 = 50; + +/// The 0-based indices [`AuFault`] faults at `PERIOD`, over `total` AUs. The +/// injector counts the AUs it is OFFERED, 1-based, so the first fault lands on +/// index `PERIOD - 1` — which is also why any period above 1 leaves a session's +/// opening parameter sets and IDR untouched. +fn fault_indices(total: usize) -> Vec { + (PERIOD as usize - 1..total) + .step_by(PERIOD as usize) + .collect() +} + +/// Every faulted AU is followed within [`DETECT_WITHIN`] AUs by a damage verdict. +/// Returns how many faults actually had a successor window to check, so the caller +/// can refuse a vacuous pass. +fn assert_detected_after_each_fault(flags: &[bool], what: &str) -> usize { + let mut checked = 0usize; + for dropped in fault_indices(flags.len()) { + // The last AU of the vector has no successor to detect on — the stream + // simply ends there. Skipping it is honest; asserting on it would be a + // statement about the fixture's length, not about detection. + let Some(window) = flags.get(dropped + 1..(dropped + 1 + DETECT_WITHIN).min(flags.len())) + else { + continue; + }; + if window.is_empty() { + continue; + } + checked += 1; + assert!( + window.iter().any(|&d| d), + "{what}: the AU(s) after dropped AU {dropped} must read as damaged — \ + the reference it needs was never decoded" + ); + } + checked +} + +/// The indices that read as damaged — the message a failing assertion needs. +fn flagged(flags: &[bool]) -> Vec { + flags + .iter() + .enumerate() + .filter(|(_, &d)| d) + .map(|(i, _)| i) + .collect() +} + +/// The control: a healthy stream must produce NO damage verdict anywhere, on +/// either codec. Without this the fault tests below prove nothing — a detector +/// that fires on every AU would pass them and cost a keyframe round trip per +/// second in the field. +#[test] +fn a_clean_stream_never_reads_as_damaged() { + for (codec, (flags, faulted)) in [("h264", replay_h264(None)), ("h265", replay_h265(None))] { + assert_eq!(faulted, 0, "{codec}: no fault armed"); + assert_eq!(flags.len(), 250, "{codec}: the whole vector replayed"); + assert!( + flagged(&flags).is_empty(), + "{codec}: the clean vector must plan without a single integrity \ + warning — flagged AUs: {:?}", + flagged(&flags) + ); + } +} + +/// A DROPPED access unit — the everyday network-loss shape — is detected on the +/// next AU, because that AU references a picture the DPB never received. This is +/// the exit criterion's first half: deliberately corrupted input, detection within +/// a bounded number of frames. +#[test] +fn a_dropped_access_unit_is_detected_on_the_very_next_one() { + let (flags, faulted) = replay_h264(Some(AuFault::new(FaultMode::Drop, PERIOD))); + assert!(faulted >= 4, "the injector fired ({faulted} AUs dropped)"); + let checked = assert_detected_after_each_fault(&flags, "h264"); + assert!( + checked >= 4, + "{checked} drops actually had a successor to check" + ); +} + +/// The H.265 leg of the same criterion, and the reason it is a separate test +/// rather than a loop over both codecs: HEVC detects a dropped AU through a +/// DIFFERENT predicate, and it has a class of AU whose loss is legitimately +/// invisible. +/// +/// There is no `frame_num` gap to notice — POC is coded per picture and a jump in +/// it is legal — so everything rests on the RPS asking for a picture the DPB does +/// not hold (`MissingReference`). If that ever stopped firing, HEVC would lose +/// drop detection entirely while the H.264 leg above stayed green. +/// +/// And the RPS can only speak for pictures something REFERENCES. A sub-layer +/// non-reference picture (`TRAIL_N` and friends — 3 of the 5 faults this vector +/// takes) is referenced by nothing, so its loss damages no later picture and the +/// planner is right to stay silent: it is a missing output frame, caught by the +/// wire's frame-index gap, not by the bitstream. Asserting BOTH verdicts is what +/// stops that correct silence from being mistaken for a detection hole — or a +/// detection hole from hiding behind it. +#[test] +fn a_dropped_hevc_reference_picture_is_detected_through_the_rps() { + // Which AUs carry a picture something can reference? Read off a CLEAN replay, + // so the classification is the stream's own and not this test's guess. + let mut planner = H265Planner::new(); + let referenced: Vec = split_h265(TEST_25FPS_H265) + .into_iter() + .map(|au| { + planner + .plan_au(au) + .map(|plan| !plan.picture.nalu_type.is_slnr()) + .unwrap_or(false) + }) + .collect(); + + let (flags, faulted) = replay_h265(Some(AuFault::new(FaultMode::Drop, PERIOD))); + assert!(faulted >= 4, "the injector fired ({faulted} AUs dropped)"); + + let (mut checked_refs, mut checked_slnr) = (0usize, 0usize); + for dropped in fault_indices(flags.len()) { + let Some(window) = flags.get(dropped + 1..(dropped + 1 + DETECT_WITHIN).min(flags.len())) + else { + continue; + }; + if window.is_empty() { + continue; + } + if referenced[dropped] { + checked_refs += 1; + assert!( + window.iter().any(|&d| d), + "h265: the AU after dropped REFERENCE picture {dropped} must read \ + as damaged — its RPS names a picture the DPB never received" + ); + } else { + checked_slnr += 1; + assert!( + !window.iter().any(|&d| d), + "h265: dropping sub-layer non-reference picture {dropped} damages \ + nothing — if the planner starts complaining here it is reporting \ + damage that did not happen, and every such report costs a frame \ + and a keyframe round trip" + ); + } + } + assert!( + checked_refs >= 2 && checked_slnr >= 2, + "the vector must exercise BOTH classes ({checked_refs} reference drops, \ + {checked_slnr} non-reference drops) or this test proves only half of what \ + it claims" + ); +} + +/// A TRUNCATED access unit is NOT parser-visible, and the assertion is that it +/// stays that way — on both codecs. +/// +/// Annex-B has no NALU length field: a slice cut at a byte boundary is +/// indistinguishable from a shorter slice. Its header parses, the picture plans, +/// it enters the DPB, and every later AU resolves its reference against an entry +/// that exists — so nothing in the syntax is ever wrong. (pf-bitstream's +/// `TruncatedAu` warning is a narrower thing entirely: a NALU whose HEADER is +/// malformed with real data still behind it.) The hardware, meanwhile, is handed a +/// slice whose bitstream ends mid-picture, which is a decode error it can report — +/// so this mode is how a lab run fires the driver's `RESULT_STATUS` detector +/// deterministically, and it is useless without one. +#[test] +fn a_truncated_access_unit_is_invisible_to_the_parser_and_needs_the_driver_verdict() { + for (codec, (flags, faulted)) in [ + ( + "h264", + replay_h264(Some(AuFault::new(FaultMode::Truncate, PERIOD))), + ), + ( + "h265", + replay_h265(Some(AuFault::new(FaultMode::Truncate, PERIOD))), + ), + ] { + assert!( + faulted >= 4, + "{codec}: the injector fired ({faulted} AUs truncated)" + ); + assert!( + flagged(&flags).is_empty(), + "{codec}: a mid-slice cut carries no syntax error — if this starts \ + firing, the cut is landing on a NALU header and the mode has stopped \ + exercising the driver-only path it exists for (flagged AUs: {:?})", + flagged(&flags) + ); + } +} + +/// The mode that shows what the DRIVER's status query is for: a byte flipped deep +/// in a slice payload leaves a bitstream that parses perfectly, resolves every +/// reference, and decodes to a wrong picture. The planner is silent — as it should +/// be, since nothing about the syntax is wrong — and on the FFmpeg rungs that +/// silence is the end of the story (`nb_queries = 0`, no `AV_FRAME_FLAG_CORRUPT`). +/// This is the Xbox Ally X class exactly, on both codecs. +#[test] +fn a_payload_bit_flip_is_invisible_to_the_parser_which_is_why_the_status_query_exists() { + for (codec, (flags, faulted)) in [ + ( + "h264", + replay_h264(Some(AuFault::new(FaultMode::Flip, PERIOD))), + ), + ( + "h265", + replay_h265(Some(AuFault::new(FaultMode::Flip, PERIOD))), + ), + ] { + assert!( + faulted >= 4, + "{codec}: the injector fired ({faulted} AUs flipped)" + ); + assert!( + flagged(&flags).is_empty(), + "{codec}: a payload flip must not be parser-visible — if this ever \ + starts firing, the flip is landing in syntax rather than payload and \ + the mode has stopped testing what it claims (flagged AUs: {:?})", + flagged(&flags) + ); + } +} diff --git a/crates/pf-vkdecode/tests/gpu_parity.rs b/crates/pf-vkdecode/tests/gpu_parity.rs new file mode 100644 index 00000000..3b1724a8 --- /dev/null +++ b/crates/pf-vkdecode/tests/gpu_parity.rs @@ -0,0 +1,1975 @@ +//! GPU frame-hash parity tests (WP-D) — the decode legs are `#[ignore]`d +//! because they need real Vulkan Video hardware; the coherence guards at the +//! bottom of this file are not, and run in ordinary CI. +//! +//! Run on a Vulkan-Video box with: +//! +//! ```text +//! cargo test -p pf-vkdecode --test gpu_parity -- --ignored --nocapture +//! ``` +//! +//! (RADV boxes additionally need `RADV_PERFTEST=video_decode` — for AV1 as much as +//! for the other two, and without it `bring_up` reports "no physical device with +//! VK_KHR_video_decode_av1", which reads like missing silicon; multi-GPU boxes +//! pin the vendor with `PF_VKD_SMOKE_VENDOR=0x1002` / `0x10de`, same knob as +//! the smoke tests. Device bring-up lives in `tests/common/mod.rs`.) +//! +//! What they prove: H.264, H.265 and AV1 decoding are all exactly specified — every +//! conformant decoder must produce bit-identical output — so the vendored 25fps +//! vector of each codec is decoded through [`VkH264Decoder`] / [`VkH265Decoder`] / +//! [`VkAv1Decoder`], +//! every output frame's NV12 planes are read back (`vkCmdCopyImageToBuffer` on the +//! graphics queue — GPU→CPU is fine in a test; the pool grows TRANSFER_SRC via the +//! decoders' `PF_VKD_TEST_READBACK` hook), cropped to the display region, +//! SHA-256-hashed in DISPLAY order and compared against goldens from libavcodec's +//! SOFTWARE decoder (the reference implementation — provenance in +//! `data/test-25fps.nv12.sha256`, `data/test-25fps-h265.nv12.sha256` and +//! `data/test-25fps-av1.nv12.sha256`). ALL +//! frames are collected, including the tail `flush` delivers, and the frame count +//! must match libavcodec's too. +//! +//! Every leg runs ONE body ([`collect_hashes`]) over `common::TestDecoder`, so the +//! H.265 and AV1 legs cannot quietly test something weaker than the H.264 one. A box +//! that decodes only some of the three runs those legs and reports the rest as "no +//! physical device with VK_KHR_video_decode_…", which is a fact about the box — +//! and on today's fleet AV1 is the one most likely to say so. +//! +//! The two Annex-B codecs run that body TWICE: once over the vendored vector as it +//! sits, and once over the same vector rewritten to FOUR-byte start codes, which is +//! what the real host emits on 100% of access units in both codecs (1514/1514 +//! H.264 and 1133/1133 HEVC, measured off the M0 NVENC corpus). Prefix width +//! carries no information, so both runs must reproduce the same goldens — +//! and submitting the four-byte form to the driver unchanged is precisely the +//! defect that made HEVC unplayable on every driver tested. Until these legs +//! existed no parity vector exercised the form that actually ships. **AV1 has no +//! such twin and needs none**: OBUs are length-delimited, so there is no start-code +//! prefix for a driver to mis-skip and no second framing to test (see +//! `common::split_av1_aus`). Its absence is deliberate. +//! +//! # Why the AV1 leg exists at all +//! +//! Because until it did, the AV1 rung had no pixel evidence whatsoever. An +//! adversarial review of the conversion found four defects — per-frame flags left +//! unset on all 274 frames, a units error in `LoopRestorationSize`, per-reference +//! info describing the wrong picture, and zeroed film-grain fields — and every one +//! of them would have shown as a hash mismatch on frame 0 or shortly after, while +//! NONE of them failed clippy or the crate's unit tests. Type-checking a struct +//! conversion cannot tell you the struct describes the right picture; only the +//! pixels can. +//! +//! The readback follows the presenter's exact frame contract: wait the frame's +//! timeline `value`, round-trip the layout, signal `value + 1` in the SAME +//! submission, then `release_frame(frame, true)` — and every submission is +//! host-waited before the next decode, so nothing here races the decode queue. +//! +//! Reading a failure: frame 0 is intra-only — if it already mismatches, suspect +//! the readback geometry (row pitch / crop) or intra decode; mismatches that +//! only appear on later frames point at inter prediction / DPB management. + +#![deny(clippy::undocumented_unsafe_blocks)] + +mod common; + +use ash::vk; +use common::TestDecoder; +use pf_vkdecode::DecodeStatus; +use pf_vkdecode::DecodedVkFrame; +use pf_vkdecode::NoopQueueLock; +use pf_vkdecode::VkAv1Decoder; +use pf_vkdecode::VkH264Decoder; +use pf_vkdecode::VkH265Decoder; +use sha2::Digest; + +/// Golden SHA-256 per display-order frame of the H.264 vector, from libavcodec +/// software decode (generation command + ffmpeg version in the file's header). +const GOLDENS_H264: &str = include_str!("data/test-25fps.nv12.sha256"); + +/// The H.265 twin, cross-checked between two independent FFmpeg builds (header). +const GOLDENS_H265: &str = include_str!("data/test-25fps-h265.nv12.sha256"); + +/// The AV1 twin: 250 DISPLAYED frames of a 274-coded-frame vector, cross-checked +/// between two independent FFmpeg builds on two architectures AND against the +/// per-frame MD5s cros-codecs vendored beside the vector (full provenance in the +/// file's header — it is the only golden here with a third-party corroboration). +const GOLDENS_AV1: &str = include_str!("data/test-25fps-av1.nv12.sha256"); + +/// The AV1 vector's FIRST frame, as libavcodec decodes it: the 320x240 render +/// region, tightly packed NV12, 115200 bytes — the same bytes +/// [`GOLDENS_AV1`]'s first line hashes. +/// +/// Hashes tell you a frame is wrong; only pixels tell you HOW. This exists for +/// [`av1_frame0_pixels_say_which_plane_and_how_badly`], whose whole job is to turn +/// "FIRST DIVERGENT FRAME = 0" into a class: luma or chroma, a shift or a +/// difference, a filter's worth of error or a structural one. Frame 0 earns the +/// 113 KiB because it is intra-only — nothing upstream of it can be blamed — and +/// because on this rung it is where a divergence appears first. +/// +/// It cannot drift from the golden set it was cut out of: +/// [`the_av1_frame0_reference_is_the_first_golden`] re-derives its SHA-256 and +/// compares, in ordinary CI, with no GPU. +const AV1_FRAME0: &[u8] = include_bytes!("data/test-25fps-av1.frame0.nv12"); + +/// The ten-bit vector and its goldens. No hardware leg in this file consumes them +/// yet — the D3D11VA rung is where the ten-bit parity leg currently runs — but the +/// files live here, beside the other goldens, so the guard that keeps them honest +/// belongs here too and runs on every platform rather than only on Windows. +const TEST_MAIN10_H265: &[u8] = include_bytes!("data/test-main10.h265"); +const GOLDENS_MAIN10: &str = include_str!("data/test-main10.p010.sha256"); + +/// The Main 10 vector is 50 display frames. +const MAIN10_FRAME_COUNT: usize = 50; + +/// The H.264 vector's display (conformance-window) region; the goldens hash +/// exactly this as tightly packed NV12. +const DISPLAY_H264: (u32, u32) = (320, 240); + +/// The H.265 vector's display region. Its SPS carries NO conformance window at +/// all, so this is also its coded size (golden header) — the two vectors merely +/// HAPPEN to share dimensions, which is why [`Readback`] takes the size as a +/// parameter instead of reading one global pair. +const DISPLAY_H265: (u32, u32) = (320, 240); + +/// The AV1 vector's display region — its `render_width` x `render_height`, AV1's +/// answer to a conformance window, and what [`DecodedVkFrame::crop`] carries on this +/// rung. Equal to the coded (post-superres) size for this vector, which +/// [`av1_goldens_and_the_ivf_split_agree_with_the_planner`] pins rather than assumes: +/// a re-synced vector whose render region shrank would make the readback crop a +/// region the goldens never hashed. +const DISPLAY_AV1: (u32, u32) = (320, 240); + +/// All three vectors' picture format: 8-bit 4:2:0. H.264 is NV12 by envelope +/// (`derive_caps` wants nothing else), H.265 Main resolves to it from the SPS and +/// AV1 Main (`seq_profile = 0`, `high_bitdepth = 0`) from the sequence header — +/// and [`DecodedVkFrame::format`] exists precisely so a pool misconfigured to +/// P010 fails loudly instead of hashing differently. +const EXPECTED_FORMAT: vk::Format = pf_vkdecode::NV12; + +/// Every vendored 25fps vector, in all three codecs, is 250 DISPLAY frames. +/// +/// For H.264 and H.265 that is also one per access unit. For AV1 it is emphatically +/// not: its 250 temporal units carry [`AV1_CODED_FRAME_COUNT`] coded frames, 24 of +/// which are HIDDEN — decoded, referenced by later frames, never shown (the vector +/// uses no `show_existing_frame`, so they are displayed by no route at all). The +/// rung delivers one frame per `dpb.outputs` id, so 250 is the number the goldens +/// carry and the number the parity leg must compare. +const FRAME_COUNT: usize = 250; + +/// The AV1 vector's CODED frame count — 24 more than [`FRAME_COUNT`]. +/// +/// Asserted by the CPU guard so the display/coded distinction stays a measured fact +/// rather than a comment: if a re-sync ever made these two numbers equal, the vector +/// would have lost its hidden-frame coverage (the exact thing that makes AV1's +/// multi-frame temporal units worth testing) while every hash still matched. +const AV1_CODED_FRAME_COUNT: usize = 274; + +/// The golden file's hash lines (comments and blanks skipped). +fn golden_hashes(file: &'static str) -> Vec<&'static str> { + file.lines() + .map(str::trim) + .filter(|line| !line.is_empty() && !line.starts_with('#')) + .collect() +} + +/// Refuse a golden set that could make a parity verdict vacuous. +/// +/// Three ways a comparison can "pass" while proving nothing, all closed here: +/// +/// - **an empty or short set** — [`assert_bit_identical`] compares `hashes` against +/// `goldens` pairwise and asserts the lengths match, so a file that lost its +/// entries to a bad regeneration would agree with a decoder that delivered +/// nothing. Pinning the count against a constant the CPU guards also re-derive +/// from the planner closes that. +/// - **junk that is not a digest** — a truncated or re-formatted line can never +/// equal a real hash, but a file of blank-looking lines could quietly become a +/// comparison of nothing. +/// - **all entries identical** — the one that matters most on a video codec. If +/// every golden were the same digest, a decoder emitting one frozen frame 250 +/// times would pass, which is precisely the failure mode a broken reference +/// conversion produces. All four golden sets here are fully distinct (250/250 +/// H.264, 250/250 H.265, 250/250 AV1, 50/50 Main 10), so requiring full +/// distinctness is not a weak bound. +fn assert_goldens_are_a_real_set(goldens: &[&str], expected: usize, path: &str) { + assert_eq!( + goldens.len(), + expected, + "{path} must carry one hash per display frame" + ); + assert!( + goldens + .iter() + .all(|line| line.len() == 64 && line.bytes().all(|b| b.is_ascii_hexdigit())), + "{path}: every golden line is a bare lowercase SHA-256 hex digest" + ); + let distinct = goldens + .iter() + .collect::>() + .len(); + assert_eq!( + distinct, + goldens.len(), + "{path}: {distinct} of {} goldens are distinct — a set with repeats (and \ + above all a set that is ALL one digest) would let a decoder that froze on \ + a single frame pass parity", + goldens.len() + ); +} + +fn sha256_hex(data: &[u8]) -> String { + use std::fmt::Write as _; + sha2::Sha256::digest(data) + .iter() + .fold(String::with_capacity(64), |mut out, byte| { + let _ = write!(out, "{byte:02x}"); + out + }) +} + +/// Test-only GPU→CPU readback: one persistently mapped staging buffer plus one +/// command buffer on the GRAPHICS queue. Each read follows the presenter's +/// frame contract — wait the frame's timeline `value`, transition the image out +/// of its video layout, copy, restore the layout, signal `value + 1` in the +/// same submission — and is host-waited (fence) before returning, so the test +/// stays fully serialized against the decode queue. +/// +/// The display size is a CONSTRUCTION parameter, not a module constant: it sizes +/// the staging buffer and is the crop every read asserts against, and the two +/// vectors sharing 320x240 today is a coincidence that must not become the next +/// vector's silent corruption. +struct Readback { + device: ash::Device, + queue: vk::Queue, + cmd_pool: vk::CommandPool, + cmd: vk::CommandBuffer, + fence: vk::Fence, + buffer: vk::Buffer, + memory: vk::DeviceMemory, + mapped: *const u8, + /// The display region every read copies, and the crop it requires. + display: (u32, u32), + /// The picture format the pool must carry. Held here rather than read from a + /// module constant so the sizing below and the per-frame assertion come from + /// ONE source — a readback sized for eight bits that then accepted a ten-bit + /// frame would hash half a picture and blame the decoder. + format: vk::Format, + /// 1 for NV12, 2 for the `3PACK16` ten-bit family (its samples are 16-bit + /// words with the ten bits in the high end — the same layout P010 has, which + /// is why one golden file serves both this rung and the D3D11VA one). + bytes_per_sample: u32, + /// `w * h * 3 / 2 * bytes_per_sample` — the tightly packed frame this buffer + /// holds. + frame_bytes: usize, +} + +impl Readback { + /// # Safety + /// + /// `instance`/`pd`/`device` are live; `graphics_qf` names a queue family a + /// queue was created on (index 0) whose family supports TRANSFER (GRAPHICS + /// implies it). + unsafe fn new( + instance: &ash::Instance, + pd: vk::PhysicalDevice, + device: &ash::Device, + graphics_qf: u32, + display: (u32, u32), + format: vk::Format, + ) -> Self { + let (width, height) = display; + // The two-plane copy below halves both dimensions for the chroma plane, so + // an odd display region would silently drop a chroma row/column. + assert_eq!( + (width % 2, height % 2), + (0, 0), + "the display region must be chroma-aligned" + ); + let bytes_per_sample = match format { + f if f == pf_vkdecode::NV12 => 1, + f if f == pf_vkdecode::P010 => 2, + other => panic!("readback has no sample size for {other:?}"), + }; + let frame_bytes = (width * height * 3 / 2 * bytes_per_sample) as usize; + + // SAFETY: fn contract — live device, queue 0 of this family exists. + let queue = unsafe { device.get_device_queue(graphics_qf, 0) }; + let pool_ci = vk::CommandPoolCreateInfo::default() + .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER) + .queue_family_index(graphics_qf); + // SAFETY: live device; destroyed in `destroy`. + let cmd_pool = unsafe { device.create_command_pool(&pool_ci, None) } + .expect("create the readback command pool"); + let alloc_ci = vk::CommandBufferAllocateInfo::default() + .command_pool(cmd_pool) + .level(vk::CommandBufferLevel::PRIMARY) + .command_buffer_count(1); + // SAFETY: the pool was just created on this device. + let cmd = unsafe { device.allocate_command_buffers(&alloc_ci) } + .expect("allocate the readback command buffer")[0]; + // SAFETY: live device; destroyed in `destroy`. + let fence = unsafe { device.create_fence(&vk::FenceCreateInfo::default(), None) } + .expect("create the readback fence"); + + let buffer_ci = vk::BufferCreateInfo::default() + .size(frame_bytes as u64) + .usage(vk::BufferUsageFlags::TRANSFER_DST) + .sharing_mode(vk::SharingMode::EXCLUSIVE); + // SAFETY: live device; destroyed in `destroy`. + let buffer = + unsafe { device.create_buffer(&buffer_ci, None) }.expect("create the staging buffer"); + // SAFETY: the buffer was just created on this device. + let req = unsafe { device.get_buffer_memory_requirements(buffer) }; + // SAFETY: live instance + physical device (fn contract). + let props = unsafe { instance.get_physical_device_memory_properties(pd) }; + let wanted = vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT; + let type_index = (0..props.memory_type_count) + .find(|&i| { + req.memory_type_bits & (1u32 << i) != 0 + && props.memory_types[i as usize] + .property_flags + .contains(wanted) + }) + .expect("a HOST_VISIBLE|HOST_COHERENT memory type for the staging buffer"); + let alloc = vk::MemoryAllocateInfo::default() + .allocation_size(req.size) + .memory_type_index(type_index); + // SAFETY: live device, size from the requirements just queried; freed in + // `destroy`. + let memory = + unsafe { device.allocate_memory(&alloc, None) }.expect("allocate staging memory"); + // SAFETY: fresh buffer bound to fresh memory of the required size. + unsafe { device.bind_buffer_memory(buffer, memory, 0) }.expect("bind staging memory"); + // SAFETY: the memory is HOST_VISIBLE and not yet mapped; the mapping + // lives until `destroy` frees the memory (implicit unmap). + let mapped = + unsafe { device.map_memory(memory, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty()) } + .expect("map the staging buffer") + .cast_const() + .cast::(); + + Self { + device: device.clone(), + queue, + cmd_pool, + cmd, + fence, + buffer, + memory, + mapped, + display, + format, + bytes_per_sample, + frame_bytes, + } + } + + /// Copy `frame`'s cropped NV12 planes into the staging buffer and return + /// them tightly packed (Y `w*h` bytes, then interleaved UV `w*h/2` bytes) — + /// exactly the layout ffmpeg's `-f rawvideo -pix_fmt nv12` writes, so the + /// hashes compare 1:1 and row pitch/crop padding can never leak in + /// (`bufferRowLength = 0` packs rows at the copy extent). + /// + /// # Safety + /// + /// `frame` was delivered by a decoder on this device and is not yet + /// released; its image carries TRANSFER_SRC usage (the decoders' + /// `PF_VKD_TEST_READBACK` hook); no other work uses the graphics queue or + /// this frame's image concurrently (the test is fully serialized). + unsafe fn read_nv12(&self, frame: &DecodedVkFrame) -> Vec { + let (width, height) = self.display; + assert_eq!( + (frame.crop.width, frame.crop.height), + self.display, + "the vector's display size this readback was built for (the goldens \ + hash exactly this region)" + ); + assert_eq!( + (frame.crop.x % 2, frame.crop.y % 2), + (0, 0), + "chroma-aligned crop origin" + ); + + let begin = vk::CommandBufferBeginInfo::default() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); + // SAFETY: the buffer came from a RESET_COMMAND_BUFFER pool (begin + // implicitly resets) and its previous submission was fence-waited. + unsafe { self.device.begin_command_buffer(self.cmd, &begin) } + .expect("begin the readback command buffer"); + + let subresource = vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: frame.layer, + layer_count: 1, + }; + // Into TRANSFER_SRC: execution/memory dependencies against the decode + // are carried by the timeline wait at submit (visibility included), so + // no src access is needed here. + let to_transfer = vk::ImageMemoryBarrier2::default() + .src_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS) + .src_access_mask(vk::AccessFlags2::empty()) + .dst_stage_mask(vk::PipelineStageFlags2::COPY) + .dst_access_mask(vk::AccessFlags2::TRANSFER_READ) + .old_layout(frame.layout) + .new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(frame.image) + .subresource_range(subresource); + let dep = + vk::DependencyInfo::default().image_memory_barriers(std::slice::from_ref(&to_transfer)); + // SAFETY: recording state; the image is live until release (fn contract). + unsafe { self.device.cmd_pipeline_barrier2(self.cmd, &dep) }; + + // Two plane copies, crop applied at the source (plane-1 offsets/extents + // are in the R8G8 plane's own half-resolution coordinates), rows packed + // into the buffer at the copy extent. + let layers = |aspect| vk::ImageSubresourceLayers { + aspect_mask: aspect, + mip_level: 0, + base_array_layer: frame.layer, + layer_count: 1, + }; + let regions = [ + vk::BufferImageCopy { + buffer_offset: 0, + buffer_row_length: 0, + buffer_image_height: 0, + image_subresource: layers(vk::ImageAspectFlags::PLANE_0), + image_offset: vk::Offset3D { + x: frame.crop.x as i32, + y: frame.crop.y as i32, + z: 0, + }, + image_extent: vk::Extent3D { + width, + height, + depth: 1, + }, + }, + vk::BufferImageCopy { + // A BYTE offset, unlike the extents above, which are texels: the + // luma plane occupies `w * h * bytes_per_sample` bytes. + buffer_offset: u64::from(width * height * self.bytes_per_sample), + buffer_row_length: 0, + buffer_image_height: 0, + image_subresource: layers(vk::ImageAspectFlags::PLANE_1), + image_offset: vk::Offset3D { + x: (frame.crop.x / 2) as i32, + y: (frame.crop.y / 2) as i32, + z: 0, + }, + image_extent: vk::Extent3D { + width: width / 2, + height: height / 2, + depth: 1, + }, + }, + ]; + // SAFETY: the image is in TRANSFER_SRC_OPTIMAL via the barrier above and + // carries TRANSFER_SRC usage (fn contract); the buffer's `frame_bytes` + // exactly spans the two packed regions. + unsafe { + self.device.cmd_copy_image_to_buffer( + self.cmd, + frame.image, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + self.buffer, + ®ions, + ); + } + + // Restore the video layout (the presenter contract: the consumer puts + // the image back exactly as delivered) and make the copy host-readable. + let restore = vk::ImageMemoryBarrier2::default() + .src_stage_mask(vk::PipelineStageFlags2::COPY) + .src_access_mask(vk::AccessFlags2::empty()) + .dst_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS) + .dst_access_mask(vk::AccessFlags2::empty()) + .old_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL) + .new_layout(frame.layout) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(frame.image) + .subresource_range(subresource); + let host_read = vk::BufferMemoryBarrier2::default() + .src_stage_mask(vk::PipelineStageFlags2::COPY) + .src_access_mask(vk::AccessFlags2::TRANSFER_WRITE) + .dst_stage_mask(vk::PipelineStageFlags2::HOST) + .dst_access_mask(vk::AccessFlags2::HOST_READ) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .buffer(self.buffer) + .offset(0) + .size(vk::WHOLE_SIZE); + let dep = vk::DependencyInfo::default() + .image_memory_barriers(std::slice::from_ref(&restore)) + .buffer_memory_barriers(std::slice::from_ref(&host_read)); + // SAFETY: recording state; own buffer, live image. + unsafe { self.device.cmd_pipeline_barrier2(self.cmd, &dep) }; + // SAFETY: recording above is complete and valid. + unsafe { self.device.end_command_buffer(self.cmd) }.expect("end the readback commands"); + + // Wait `value`, signal `value + 1` — the DecodedVkFrame sync contract. + let wait = vk::SemaphoreSubmitInfo::default() + .semaphore(frame.semaphore) + .value(frame.value) + .stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS); + let signal = vk::SemaphoreSubmitInfo::default() + .semaphore(frame.semaphore) + .value(frame.value + 1) + .stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS); + let cmd_info = vk::CommandBufferSubmitInfo::default().command_buffer(self.cmd); + let submit = vk::SubmitInfo2::default() + .wait_semaphore_infos(std::slice::from_ref(&wait)) + .command_buffer_infos(std::slice::from_ref(&cmd_info)) + .signal_semaphore_infos(std::slice::from_ref(&signal)); + // SAFETY: live queue/fence; the semaphore is the frame's timeline + // semaphore (fn contract), the fence was reset after its last use. + unsafe { + self.device + .queue_submit2(self.queue, std::slice::from_ref(&submit), self.fence) + } + .expect("submit the readback"); + // SAFETY: the fence was just submitted. + unsafe { + self.device + .wait_for_fences(&[self.fence], true, 10_000_000_000) + } + .expect("readback completes within 10s"); + // SAFETY: the fence was observed signalled above. + unsafe { self.device.reset_fences(&[self.fence]) }.expect("reset the readback fence"); + + // SAFETY: `mapped` points at `frame_bytes` host-coherent bytes (the + // buffer was created at that size); the fence wait (plus the HOST_READ + // barrier) ordered the device writes before this host read. + unsafe { std::slice::from_raw_parts(self.mapped, self.frame_bytes) }.to_vec() + } + + /// # Safety + /// + /// No submission in flight (every `read_nv12` fence-waited before + /// returning) and nothing else references these handles. + unsafe fn destroy(&self) { + // SAFETY: own handles on the live device, idle per the fn contract; + // freeing the memory implicitly unmaps it. + unsafe { + self.device.destroy_buffer(self.buffer, None); + self.device.free_memory(self.memory, None); + self.device.destroy_fence(self.fence, None); + self.device.destroy_command_pool(self.cmd_pool, None); + } + } +} + +/// Wait the frame's decode verdict, read + hash its pixels, release it (with +/// the presenter write-back the readback enqueued). `index` is the display +/// index the hash will land at. +fn consume_frame( + decoder: &mut impl TestDecoder, + readback: &Readback, + frame: &DecodedVkFrame, + index: usize, +) -> String { + assert_eq!( + decoder.wait_status(frame), + DecodeStatus::Ok, + "frame {index}: decode op not COMPLETE\n state: {}", + decoder.debug_snapshot() + ); + // A pool built for the wrong picture format would decode correctly and then + // hash differently for a reason no mismatch report could explain + // (`DecodedVkFrame::format` docs) — refuse it here instead. + assert_eq!( + frame.format, readback.format, + "frame {index}: the vector must decode into the pool format the readback \ + was built for" + ); + // SAFETY: the frame is delivered and unreleased on the readback's device; + // the pool carries TRANSFER_SRC (PF_VKD_TEST_READBACK was set before the + // decoder's first decode); the test is fully serialized, so nothing else + // touches the graphics queue or this image. + let nv12 = unsafe { readback.read_nv12(frame) }; + decoder + .release_frame(frame, true) + .unwrap_or_else(|e| panic!("frame {index}: release failed: {e}")); + sha256_hex(&nv12) +} + +/// Decode every AU, hash every delivered frame in display order, including the +/// tail `flush` hands back. One body for all three codecs. +/// +/// The flush tail is where the codecs legitimately differ and the body deliberately +/// does not: H.264 and H.265 can hold pictures back for reorder, so their planners' +/// `flush` releases a tail. AV1's planner has no `flush` at all — a shown frame is +/// output by the very temporal unit that decodes it — so `VkAv1Decoder::flush` frees +/// the hidden pictures' images and hands back nothing. Draining afterwards is +/// therefore a no-op for AV1 rather than a special case, and running the identical +/// body means an AV1 rung that ever DID strand a shown frame would be caught by the +/// frame-count assertion instead of hidden by a codec-specific shortcut. +fn collect_hashes( + decoder: &mut impl TestDecoder, + readback: &Readback, + aus: &[&[u8]], +) -> Vec { + let mut hashes: Vec = Vec::new(); + for (au_index, au) in aus.iter().enumerate() { + let mut next = decoder.decode(au).unwrap_or_else(|e| { + panic!( + "AU {au_index}: decode failed: {e}\n state: {}", + decoder.debug_snapshot() + ) + }); + while let Some(frame) = next { + let hash = consume_frame(decoder, readback, &frame, hashes.len()); + hashes.push(hash); + next = decoder.take_ready(); + } + } + // The decoders emit in bumping (display) order and a stream may hold frames — + // the flush tail belongs in the comparison too. + decoder.flush(); + while let Some(frame) = decoder.take_ready() { + let hash = consume_frame(decoder, readback, &frame, hashes.len()); + hashes.push(hash); + } + eprintln!( + "final state: {} status_queries={}", + decoder.debug_snapshot(), + decoder.status_queries() + ); + hashes +} + +/// The verdict, run AFTER teardown so a mismatch panic cannot leave the device +/// alive. +fn assert_bit_identical(hashes: &[String], goldens: &[&str], codec: &str) { + assert_eq!( + hashes.len(), + goldens.len(), + "{codec}: frame count diverges from libavcodec ({} decoded vs {} golden)", + hashes.len(), + goldens.len() + ); + let mut mismatches = 0usize; + let mut first_divergence: Option = None; + for (index, (got, want)) in hashes.iter().zip(goldens.iter()).enumerate() { + if got.as_str() != *want { + if mismatches < 10 { + eprintln!("frame {index}: MISMATCH\n ours: {got}\n golden: {want}"); + } + first_divergence.get_or_insert(index); + mismatches += 1; + } + } + // The FIRST divergent index is the whole diagnostic: everything after it may be + // downstream of that one frame through prediction and the DPB, so a report that + // only counted mismatches would bury the one number that localises the defect. + assert!( + first_divergence.is_none(), + "{codec}: FIRST DIVERGENT FRAME = {} ({mismatches}/{} frames diverge from \ + libavcodec; up to 10 printed above). Frame 0 is intra-only — if IT is the \ + first, suspect readback geometry (pitch/crop), the picture format, or intra \ + decode / the per-frame parameter conversion; a first divergence LATER points \ + at inter prediction, per-reference info or DPB management, and the frames \ + after it are probably just downstream of it.", + first_divergence.unwrap_or_default(), + hashes.len() + ); + eprintln!( + "{codec}: {} frames bit-identical to libavcodec software decode", + hashes.len() + ); +} + +/// One H.264 parity run over a caller-supplied AU list. +/// +/// The AUs are a parameter rather than a constant because two legs share this +/// body: the vendored vector as it sits (three-byte start codes) and the same +/// vector rewritten to the four-byte ones the real host actually emits. Both +/// must reproduce the SAME goldens, because prefix width carries no +/// information — and running one body twice is what makes that an equality +/// rather than two similar-looking assertions that could drift apart. +fn h264_parity_run(aus: &[&[u8]], label: &str) { + // One codec at a time on the device, and the `set_var` below happens only + // under this lock (see `common::gpu_lock`). + let _gpu = common::gpu_lock(); + + // The decoder reads this at session creation (first decode call): pool + // images grow TRANSFER_SRC so vkCmdCopyImageToBuffer is legal. + std::env::set_var("PF_VKD_TEST_READBACK", "1"); + + let goldens = golden_hashes(GOLDENS_H264); + assert_eq!( + goldens.len(), + FRAME_COUNT, + "the golden file carries one hash per libavcodec frame" + ); + + let setup = common::bring_up(&common::Request { + codec: common::H264, + // Unlike the smoke legs this one NEEDS a graphics queue (the readback + // records there), so a device without one is skipped, not defaulted. + graphics: common::Graphics::Required, + report_families: true, + }); + let handles = setup.handles(); + + let hashes = { + // SAFETY: `setup` outlives this block (destroyed below, after the decoder + // and readback drop at the block's end), it was created with the H.264 + // decode extensions + timeline/sync2 features, and its queue fields name + // the families/queues it created. + let mut decoder = unsafe { VkH264Decoder::new(&handles, Box::new(NoopQueueLock)) } + .expect("wrap the device"); + // SAFETY: live instance/device; queue 0 of `graphics_qf` was created by + // the bring-up; destroyed at the end of this block after its last read. + let readback = unsafe { + Readback::new( + &setup.instance, + setup.pd, + &setup.device, + setup.graphics_qf, + DISPLAY_H264, + EXPECTED_FORMAT, + ) + }; + let hashes = collect_hashes(&mut decoder, &readback, aus); + // SAFETY: every readback was fence-waited inside `read_nv12`; nothing + // else references its handles. + unsafe { readback.destroy() }; + hashes + }; + + // SAFETY: the decoder is gone (its Drop drained the queue and destroyed its + // session/pools), the readback's handles are destroyed, and nothing else + // references the setup's handles. + unsafe { setup.destroy() }; + + assert_bit_identical(&hashes, &goldens, label); +} + +#[test] +#[ignore = "needs a Vulkan Video H.264 decode device (fleet boxes; see module docs)"] +fn h264_every_frame_hashes_bit_identical_to_libavcodec() { + h264_parity_run(&common::split_h264_aus(common::TEST_25FPS_H264), "H.264"); +} + +/// The same 250 frames, submitted the way the real host submits them. +/// +/// A failure here where the leg above passes means the four-byte prefix is +/// reaching the driver — `ring::pack_slices` stopped trimming the leading zero +/// byte, or stopped deriving the slice offsets from the trimmed lengths — which +/// is the defect that made HEVC unplayable on every driver tested. +#[test] +#[ignore = "needs a Vulkan Video H.264 decode device (fleet boxes; see module docs)"] +fn h264_four_byte_start_codes_decode_bit_identically() { + let stream = common::h264_four_byte_start_codes(common::TEST_25FPS_H264); + h264_parity_run( + &common::split_h264_aus(&stream), + "H.264 (4-byte start codes)", + ); +} + +/// The H.265 twin of [`h264_parity_run`]; see its docs for why the AUs are a +/// parameter. +fn h265_parity_run( + aus: &[&[u8]], + goldens_file: &'static str, + expected_frames: usize, + bit_depth_luma_minus8: u8, + format: vk::Format, + display: (u32, u32), + label: &str, +) { + // As the H.264 leg: one codec at a time, `set_var` under the lock. + let _gpu = common::gpu_lock(); + + std::env::set_var("PF_VKD_TEST_READBACK", "1"); + + let goldens = golden_hashes(goldens_file); + assert_eq!( + goldens.len(), + expected_frames, + "the golden file carries one hash per libavcodec frame" + ); + + let setup = common::bring_up(&common::Request { + codec: common::H265, + graphics: common::Graphics::Required, + report_families: true, + }); + let handles = setup.handles(); + + let hashes = { + // SAFETY: as the H.264 leg — `setup` outlives this block and was created + // with the H.265 decode extensions + timeline/sync2 features. + let mut decoder = unsafe { VkH265Decoder::new(&handles, Box::new(NoopQueueLock)) } + .expect("wrap the device"); + // The construction-time shape gate the client's ladder relies on, on the + // vector's own facts (Main, 4:2:0, 8-bit → NV12): a device that cannot + // host the combination refuses here with a caps reason instead of failing + // mid-stream. + decoder + .probe_stream_support(1, bit_depth_luma_minus8) + .unwrap_or_else(|e| { + panic!("{label}: the box must host this H.265 shape — {e:?}"); + }); + // SAFETY: as the H.264 leg — live instance/device, queue 0 of + // `graphics_qf` exists; destroyed at the end of this block. + let readback = unsafe { + Readback::new( + &setup.instance, + setup.pd, + &setup.device, + setup.graphics_qf, + display, + format, + ) + }; + let hashes = collect_hashes(&mut decoder, &readback, aus); + // SAFETY: every readback was fence-waited inside `read_nv12`; nothing + // else references its handles. + unsafe { readback.destroy() }; + hashes + }; + + // SAFETY: as the H.264 leg — decoder and readback are gone. + unsafe { setup.destroy() }; + + assert_bit_identical(&hashes, &goldens, label); +} + +#[test] +#[ignore = "needs a Vulkan Video H.265 decode device (fleet boxes; see module docs)"] +fn h265_every_frame_hashes_bit_identical_to_libavcodec() { + h265_parity_run( + &common::split_h265_aus(common::TEST_25FPS_H265), + GOLDENS_H265, + FRAME_COUNT, + 0, + EXPECTED_FORMAT, + DISPLAY_H265, + "H.265", + ); +} + +/// The ten-bit path — the only leg in this file that is not eight-bit. +/// +/// Every other golden set in this program is NV12, so no rung had pixel evidence +/// for its ten-bit path: the HDR legs proved a Main10 session BUILDS and streams +/// clean, which a stream decoding to garbage would also do. The goldens are P010 +/// and the Vulkan pool is `G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16`, whose +/// samples are 16-bit words with the ten bits in the high end — the same layout, +/// which is why one golden file serves this rung and the D3D11VA one. +#[test] +#[ignore = "needs a Vulkan Video H.265 Main 10 decode device (fleet boxes; see module docs)"] +fn main10_every_frame_hashes_bit_identical_to_libavcodec() { + h265_parity_run( + &common::split_h265_aus(TEST_MAIN10_H265), + GOLDENS_MAIN10, + MAIN10_FRAME_COUNT, + 2, + pf_vkdecode::P010, + (320, 240), + "HEVC Main 10", + ); +} + +/// The HEVC leg of the production prefix form — the one that would have caught +/// the shipped defect. See [`h264_four_byte_start_codes_decode_bit_identically`]. +#[test] +#[ignore = "needs a Vulkan Video H.265 decode device (fleet boxes; see module docs)"] +fn h265_four_byte_start_codes_decode_bit_identically() { + let stream = common::h265_four_byte_start_codes(common::TEST_25FPS_H265); + h265_parity_run( + &common::split_h265_aus(&stream), + GOLDENS_H265, + FRAME_COUNT, + 0, + EXPECTED_FORMAT, + DISPLAY_H265, + "H.265 (4-byte start codes)", + ); +} + +/// The AV1 twin of [`h265_parity_run`]. +/// +/// Concrete where the H.265 one is parameterised, because AV1 has exactly one +/// vendored vector and one shape (Main 4:2:0 8-bit, no film grain, 320x240); the +/// facts it hard-codes are re-derived from the planner, without a GPU, by +/// [`av1_goldens_and_the_ivf_split_agree_with_the_planner`], so a re-synced vector +/// of another shape fails in ordinary CI with the reason rather than on the fleet as +/// a confusing probe refusal. A second AV1 vector (Main 10, or one that uses +/// `show_existing_frame`) is the point at which this should grow the same parameters +/// the H.265 body carries — not before. +fn av1_parity_run(aus: &[&[u8]], label: &str) { + // As the other legs: one codec at a time on the device, and the `set_var` below + // happens only under this lock (see `common::gpu_lock`). + let _gpu = common::gpu_lock(); + + std::env::set_var("PF_VKD_TEST_READBACK", "1"); + + let goldens = golden_hashes(GOLDENS_AV1); + // Non-vacuity, before any hardware is touched: the right number of entries, all + // real digests, all distinct (see the helper's docs — a frozen-frame decoder + // must not be able to pass this leg). + assert_goldens_are_a_real_set(&goldens, FRAME_COUNT, "data/test-25fps-av1.nv12.sha256"); + // …and the leg must actually be fed something. An IVF whose packets failed to + // parse would hand `collect_hashes` an empty AU list, which delivers no frames + // and would then fail as a frame-count mismatch that reads like a decoder defect. + assert_eq!( + aus.len(), + FRAME_COUNT, + "{label}: the vector must split into {FRAME_COUNT} temporal units" + ); + + let setup = common::bring_up(&common::Request { + codec: common::AV1, + // As the other parity legs: the readback records on the graphics queue, so a + // device without a graphics family is skipped rather than defaulted. + graphics: common::Graphics::Required, + report_families: true, + }); + let handles = setup.handles(); + + let hashes = { + // SAFETY: as the H.264/H.265 legs — `setup` outlives this block (destroyed + // below, after the decoder and readback drop at the block's end), it was + // created with the AV1 decode extension + timeline/sync2 features, and its + // queue fields name the families/queues it created. + let mut decoder = unsafe { VkAv1Decoder::new(&handles, Box::new(NoopQueueLock)) } + .expect("wrap the device"); + // The construction-time shape gate, on the vector's own facts: 4:2:0, 8-bit, + // and NO film grain. The third argument is the load-bearing one — grain + // synthesis is part of the Vulkan decode PROFILE, so a box that offers only + // the grain-enabled profile (or only the disabled one) refuses HERE with a + // caps reason instead of failing at the first temporal unit. + decoder + .probe_stream_support(1, 8, false) + .unwrap_or_else(|e| { + panic!("{label}: the box must host AV1 Main 4:2:0 8-bit, no film grain — {e:?}"); + }); + // SAFETY: as the other legs — live instance/device, queue 0 of `graphics_qf` + // was created by the bring-up; destroyed at the end of this block. + let readback = unsafe { + Readback::new( + &setup.instance, + setup.pd, + &setup.device, + setup.graphics_qf, + DISPLAY_AV1, + EXPECTED_FORMAT, + ) + }; + let hashes = collect_hashes(&mut decoder, &readback, aus); + // SAFETY: every readback was fence-waited inside `read_nv12`; nothing else + // references its handles. + unsafe { readback.destroy() }; + hashes + }; + + // SAFETY: as the other legs — the decoder is gone (its Drop drained the queue and + // destroyed its session/pools) and the readback's handles are destroyed. + unsafe { setup.destroy() }; + + assert_bit_identical(&hashes, &goldens, label); +} + +/// The AV1 rung's first pixel evidence. +/// +/// 250 temporal units in, 250 DISPLAYED frames out (the 24 hidden frames the vector +/// also codes are decoded, referenced and never shown — module docs), each read back +/// as tightly packed NV12 over its `render_width` x `render_height` region and +/// compared against libavcodec's software decode. +/// +/// What a failure looks like, and where to point it: +/// - **frame 0** — the sequence header or the per-frame parameter conversion: +/// `StdVideoAV1SequenceHeader`, the eight per-frame sub-blocks (tile info, +/// quantisation, segmentation, loop filter, CDEF, loop restoration, global motion, +/// film grain), the tile-group ranges, or the readback geometry. AV1 puts in the +/// frame header what H.26x puts in parameter sets, so a single wrong field here +/// damages every frame. +/// - **frame 1** — the first frame with a reference. Per-reference info, the +/// reference-NAME → DPB-slot table, or `ref_frame_idx` ordering. +/// - **later, then everything after** — DPB slot management, `refresh_frame_flags`, +/// or the hidden frames: a run that is clean until roughly the first multi-frame +/// temporal unit and wrong thereafter is the signature of the hidden ALTREF being +/// stored wrong or not at all. +#[test] +#[ignore = "needs a Vulkan Video AV1 decode device (fleet boxes; see module docs)"] +fn av1_every_frame_hashes_bit_identical_to_libavcodec() { + av1_parity_run(&common::split_av1_aus(common::TEST_25FPS_AV1), "AV1"); +} + +/// Frame 0's pixels against libavcodec's, byte for byte — the diagnostic leg. +/// +/// [`av1_every_frame_hashes_bit_identical_to_libavcodec`] is the verdict; this is +/// the microscope, and it decodes only as far as the first delivered frame. A hash +/// mismatch names no cause, and the four causes the parity leg's own message ranks +/// for a frame-0 divergence produce *completely different* pixel signatures: +/// +/// | printed here | what it means | +/// |---|---| +/// | `luma IDENTICAL`, chroma differs | the chroma plane's layout — `PLANE_1`'s copy region, or a pool whose chroma plane starts somewhere other than where the readback reads it. NOT a decode problem | +/// | both differ, and a **shift** matches | readback geometry: the crop origin, or a copy extent taken from the pool rather than the render region. The printed `dy`/`dx` IS the error | +/// | both differ, deltas ≤ ~8 over most of the plane | an in-loop filter parameter — CDEF, loop restoration, the deblocking levels. Small and everywhere is what a filter does, and it is why the whole 250 frames go with it: CDEF runs before the frame is stored as a reference | +/// | both differ, deltas large and structured | quantisation, tile geometry, or the tile payloads themselves — the frame was reconstructed from the wrong data rather than filtered wrongly | +/// | ours is CONSTANT | nothing was decoded into the image the readback read | +/// +/// It asserts equality last, so a failure prints the whole report above the panic. +#[test] +#[ignore = "needs a Vulkan Video AV1 decode device (fleet boxes; see module docs)"] +fn av1_frame0_pixels_say_which_plane_and_how_badly() { + let _gpu = common::gpu_lock(); + std::env::set_var("PF_VKD_TEST_READBACK", "1"); + + let aus = common::split_av1_aus(common::TEST_25FPS_AV1); + assert_eq!( + aus.len(), + FRAME_COUNT, + "the vector must split into 250 units" + ); + let ours = av1_first_frame(&aus); + + report_nv12_divergence(&ours, AV1_FRAME0, DISPLAY_AV1); + assert_eq!( + sha256_hex(&ours), + golden_hashes(GOLDENS_AV1)[0], + "AV1 frame 0 is not libavcodec's — read the report above for the class" + ); + eprintln!("AV1 frame 0 is byte-identical to libavcodec"); +} + +/// `loop_filter_level[2]` and `[3]` — the U and V deblocking levels — as BIT +/// offsets from the start of the vector's FIRST access unit. +/// +/// Derived rather than found: the IVF packet holds a temporal-delimiter OBU (2 +/// bytes), a sequence-header OBU (2 + 11) and an `OBU_FRAME` header (1 + a 2-byte +/// leb128 size), so the uncompressed frame header starts at byte 18. Inside it +/// `loop_filter_level[0]` begins at bit 35 and the four levels are `f(6)` back to +/// back (5.9.11), which puts U at bit 47 and V at bit 53. +/// +/// [`av1_frame0_probes_whether_the_driver_reads_the_chroma_deblocking_levels`] +/// re-parses the mutated unit before it decodes anything, so a re-synced vector +/// makes this fail loudly instead of poking an unrelated field. +const AV1_FRAME0_FILTER_LEVEL_U_BIT: usize = 18 * 8 + 47; +const AV1_FRAME0_FILTER_LEVEL_V_BIT: usize = 18 * 8 + 53; + +/// The strongest deblocking level AV1 can code (`f(6)`), and the value the probe +/// rewrites both chroma levels to. +const MAX_LOOP_FILTER_LEVEL: u8 = 63; + +/// The driver DOES read the chroma deblocking levels — and this is the test that +/// says so, after a pass of this program's history said the opposite. +/// +/// ⚠⚠ **The claim "NVIDIA ignores `StdVideoAV1LoopFilter::loop_filter_level[2..3]`" +/// is refuted. Do not reintroduce it.** It was an honest reading of a real +/// measurement: the AV1 frame-0 parity leg came back `luma IDENTICAL, chroma +/// 319/38400 bytes differ, max |delta| 4`, software re-decode with both chroma +/// levels forced to zero reproduced that signature byte for byte, and this very +/// probe then came back IDENTICAL for `[8, 12]` and `[63, 63]`. Every step was +/// sound; the inference was not. The levels were reaching the driver intact — what +/// was NOT intact was the sequence header, whose `pColorConfig` block this crate +/// freed the instant `vkCreateVideoSessionParametersKHR` returned while the driver +/// went on dereferencing it at every decode. The recycled bytes read as +/// `mono_chrome = 1`, and a monochrome frame skips exactly `loop_filter_level[2..3]` +/// (AV1 7.14) — which is why rewriting them changed nothing, and why the +/// fingerprint was a perfect match for levels that were never applied. +/// `pf-vkdecode`'s `session_av1` module docs carry the capture and the fix. +/// +/// So the probe survives its own refutation, with its verdict inverted: it now +/// PASSES, and it is the cheapest guard there is against that whole class coming +/// back. It decodes frame 0 twice — once from the vector as it sits, once from the +/// same unit with both chroma levels rewritten to the strongest AV1 can code — and +/// requires the pixels to differ. In software that rewrite moves 793 chroma bytes +/// with `max |delta| 29`, which no readback or crop error could hide, and it leaves +/// luma bit-identical, which is the control: a mutation that changed luma would +/// have desynchronised the header rather than changed the field. +/// +/// If the two decodes are ever IDENTICAL again, the message below is the one to +/// act on — and the FIRST thing to check is not the driver but whether some block +/// the decode op points at is being freed before the op is recorded. That is what +/// it was last time, on a bug this test could not see. +#[test] +#[ignore = "needs a Vulkan Video AV1 decode device (fleet boxes; see module docs)"] +fn av1_frame0_probes_whether_the_driver_reads_the_chroma_deblocking_levels() { + let _gpu = common::gpu_lock(); + std::env::set_var("PF_VKD_TEST_READBACK", "1"); + + let aus = common::split_av1_aus(common::TEST_25FPS_AV1); + assert_eq!(aus.len(), FRAME_COUNT); + + let mutated_au = av1_frame0_with_max_chroma_deblocking(aus[0]); + let mut units: Vec<&[u8]> = aus.clone(); + units[0] = &mutated_au; + + let coded = av1_first_frame(&aus); + let maxed = av1_first_frame(&units); + + let luma = (DISPLAY_AV1.0 * DISPLAY_AV1.1) as usize; + eprintln!(" coded chroma levels [8, 12] {}", sha256_hex(&coded)); + eprintln!(" chroma levels [63, 63] {}", sha256_hex(&maxed)); + eprintln!(" libavcodec's frame 0 {}", sha256_hex(AV1_FRAME0)); + assert_eq!( + coded[..luma], + maxed[..luma], + "the chroma deblocking levels must not move a luma sample — if they did, \ + the mutation desynchronised the frame header and the chroma comparison \ + below means nothing" + ); + assert_ne!( + coded[luma..], + maxed[luma..], + "the driver produced the SAME chroma from loop_filter_level[2..3] = [8, 12] \ + and from [63, 63]. This happened once before and the driver was INNOCENT: \ + a monochrome-looking sequence header makes it skip both levels, and ours \ + looked monochrome because its `pColorConfig` block had been freed and \ + reused before the decode op was recorded (see this test's docs). So audit \ + the LIFETIME of everything the submission points at — the Std sequence \ + header behind the parameters object first — before blaming the vendor" + ); + eprintln!("the driver reads the chroma deblocking levels — the two decodes differ"); +} + +/// The vector's first access unit with both CHROMA deblocking levels rewritten to +/// [`MAX_LOOP_FILTER_LEVEL`] — and the proof, through the real parser, that this is +/// the only thing it changed. +/// +/// The proof is not decoration. The offsets are derived from the spec's syntax +/// order rather than searched for, and a rewrite landing one field over would +/// desynchronise nothing (both neighbours are fixed-width) while silently probing +/// the wrong parameter. So every block the conversion reads is compared before and +/// after, and [`the_av1_chroma_deblocking_mutation_changes_only_those_two_levels`] +/// runs this on CPU in ordinary CI — the hardware run cannot be spent discovering +/// that the mutation was wrong. +fn av1_frame0_with_max_chroma_deblocking(au: &[u8]) -> Vec { + let mut mutated = au.to_vec(); + for bit in [AV1_FRAME0_FILTER_LEVEL_U_BIT, AV1_FRAME0_FILTER_LEVEL_V_BIT] { + set_bits(&mut mutated, bit, 6, MAX_LOOP_FILTER_LEVEL); + } + + let before = av1_first_header(au); + let after = av1_first_header(&mutated); + assert_eq!( + before.loop_filter_params.loop_filter_level, + [1, 7, 8, 12], + "the vendored vector's frame 0 codes these levels, and the whole probe is \ + built around the last two of them" + ); + assert_eq!( + after.loop_filter_params.loop_filter_level, + [1, 7, MAX_LOOP_FILTER_LEVEL, MAX_LOOP_FILTER_LEVEL], + "the rewrite must land on the two CHROMA levels and leave the luma pair \ + alone — a luma change would make the probe's control meaningless" + ); + // Everything else the conversion reads, unchanged: a rewrite that shifted the + // header would show up in one of these long before it showed up in pixels. + assert_eq!( + after.cdef_params, before.cdef_params, + "the CDEF block follows the loop filter block and is what a shifted rewrite \ + would corrupt first" + ); + assert_eq!(after.quantization_params, before.quantization_params); + assert_eq!(after.tile_info, before.tile_info); + assert_eq!( + after.loop_restoration_params, + before.loop_restoration_params + ); + assert_eq!(after.segmentation_params, before.segmentation_params); + assert_eq!( + ( + after.loop_filter_params.loop_filter_sharpness, + after.loop_filter_params.loop_filter_ref_deltas, + after.loop_filter_params.loop_filter_mode_deltas, + ), + ( + before.loop_filter_params.loop_filter_sharpness, + before.loop_filter_params.loop_filter_ref_deltas, + before.loop_filter_params.loop_filter_mode_deltas, + ), + "the rest of the loop filter block rides after the levels and must survive" + ); + assert_ne!(mutated, au, "the rewrite must actually change bytes"); + mutated +} + +/// [`av1_frame0_with_max_chroma_deblocking`] on CPU, so the GPU probe's mutation is +/// known-good before any device time is spent on it. +#[test] +fn the_av1_chroma_deblocking_mutation_changes_only_those_two_levels() { + let aus = common::split_av1_aus(common::TEST_25FPS_AV1); + let mutated = av1_frame0_with_max_chroma_deblocking(aus[0]); + // One byte may carry bits of both fields (U ends mid-byte), so the rewrite + // touches two or three bytes and no more — a whole-unit difference would mean + // `set_bits` walked off its field. + let changed = aus[0].iter().zip(&mutated).filter(|(a, b)| a != b).count(); + assert!( + (1..=3).contains(&changed), + "twelve bits spanning at most three bytes, and {changed} bytes changed" + ); +} + +/// Overwrite the `bits`-wide big-endian bitfield at `bit` in `data`. +/// +/// AV1's `f(n)` is MSB-first from the start of the OBU payload, which is what the +/// probe above needs to rewrite a syntax element in place: same width, same +/// position, so nothing after it shifts. +fn set_bits(data: &mut [u8], bit: usize, bits: usize, value: u8) { + for i in 0..bits { + let at = bit + i; + let mask = 1u8 << (7 - (at % 8)); + let set = (value >> (bits - 1 - i)) & 1 == 1; + if set { + data[at / 8] |= mask; + } else { + data[at / 8] &= !mask; + } + } +} + +/// The parsed frame header of the FIRST frame in one access unit. +fn av1_first_header(au: &[u8]) -> pf_bitstream::av1::ParsedFrameHeader { + let mut planner = pf_bitstream::av1::Av1Planner::new(); + let plans = planner.plan_au(au).expect("the unit plans"); + let plan = plans.first().expect("the unit carries a frame"); + (*plan.header).clone() +} + +/// Decode `aus` only as far as the FIRST delivered frame, and read it back as +/// tightly packed NV12 — the device half of both frame-0 legs. +/// +/// Brings its own device up and tears it down, so one test may call it more than +/// once; the GPU lock and the readback hook are the caller's. +fn av1_first_frame(aus: &[&[u8]]) -> Vec { + let setup = common::bring_up(&common::Request { + codec: common::AV1, + graphics: common::Graphics::Required, + report_families: true, + }); + let handles = setup.handles(); + + let ours = { + // SAFETY: as the parity legs — `setup` outlives this block and was created + // with the AV1 decode extension + timeline/sync2 features. + let mut decoder = unsafe { VkAv1Decoder::new(&handles, Box::new(NoopQueueLock)) } + .expect("wrap the device"); + decoder + .probe_stream_support(1, 8, false) + .expect("the box must host AV1 Main 4:2:0 8-bit, no film grain"); + // SAFETY: as the parity legs — live instance/device, queue 0 of `graphics_qf`. + let readback = unsafe { + Readback::new( + &setup.instance, + setup.pd, + &setup.device, + setup.graphics_qf, + DISPLAY_AV1, + EXPECTED_FORMAT, + ) + }; + + // The FIRST delivered frame and no further: the first temporal unit is a + // key frame that shows, so this is one decode. + let mut first: Option> = None; + for (index, au) in aus.iter().enumerate() { + let frame = decoder + .decode(au) + .unwrap_or_else(|e| panic!("AU {index}: decode failed: {e}")); + if let Some(frame) = frame { + assert_eq!( + decoder.wait_status(&frame), + DecodeStatus::Ok, + "frame 0: decode op not COMPLETE\n state: {}", + decoder.debug_snapshot() + ); + assert_eq!(frame.format, EXPECTED_FORMAT, "frame 0: pool format"); + // SAFETY: the frame is delivered and unreleased on the readback's + // device, the pool carries TRANSFER_SRC, and the test is serialized. + first = Some(unsafe { readback.read_nv12(&frame) }); + decoder + .release_frame(&frame, true) + .expect("frame 0: release"); + // A temporal unit may carry more than one frame, and this leg stops + // at the first. Anything else the unit made ready is handed straight + // back — with `false`, because no presenter signalled its timeline + // (nothing read it) — rather than left held while the decoder drops. + while let Some(spare) = decoder.take_ready() { + decoder + .release_frame(&spare, false) + .expect("release an unread frame of the same temporal unit"); + } + break; + } + } + // SAFETY: every readback was fence-waited inside `read_nv12`. + unsafe { readback.destroy() }; + first.expect("the vector's first temporal unit shows a frame") + }; + + // SAFETY: as the parity legs — the decoder and readback are gone. + unsafe { setup.destroy() }; + ours +} + +/// Per-plane statistics of `ours` against `want`, printed rather than asserted. +/// +/// Everything here answers a question a hash cannot: WHICH plane, whether the +/// difference is a displacement or a value error, and how big. See +/// [`av1_frame0_pixels_say_which_plane_and_how_badly`] for how to read it. +fn report_nv12_divergence(ours: &[u8], want: &[u8], display: (u32, u32)) { + let (width, height) = (display.0 as usize, display.1 as usize); + let luma = width * height; + assert_eq!(ours.len(), want.len(), "both frames are the same layout"); + assert_eq!(ours.len(), luma * 3 / 2, "tightly packed NV12"); + + eprintln!( + "--- AV1 frame 0: {width}x{height} NV12, {} bytes ---", + ours.len() + ); + eprintln!(" ours {}", sha256_hex(ours)); + eprintln!(" golden {}", sha256_hex(want)); + + // A plane that never varies means nothing was decoded into the image at all, + // which is a different failure from decoding it wrongly. + let flat = |plane: &[u8]| plane.iter().all(|b| *b == plane[0]); + if flat(&ours[..luma]) { + eprintln!( + " ⚠ our LUMA is constant ({}) — nothing decoded here", + ours[0] + ); + } + if flat(&ours[luma..]) { + eprintln!( + " ⚠ our CHROMA is constant ({}) — nothing decoded here", + ours[luma] + ); + } + + for (name, ours, want) in [ + ("luma ", &ours[..luma], &want[..luma]), + ("chroma", &ours[luma..], &want[luma..]), + ] { + if ours == want { + eprintln!(" {name}: IDENTICAL ({} bytes)", ours.len()); + continue; + } + let mut differing = 0usize; + let mut max_delta = 0u32; + let mut total_delta = 0u64; + // |delta| buckets: 1, 2, 3-4, 5-8, 9-16, 17-64, 65+. + let mut buckets = [0usize; 7]; + let mut first: Vec<(usize, u8, u8)> = Vec::new(); + for (i, (a, b)) in ours.iter().zip(want.iter()).enumerate() { + if a == b { + continue; + } + let delta = u32::from(a.abs_diff(*b)); + differing += 1; + max_delta = max_delta.max(delta); + total_delta += u64::from(delta); + let bucket = match delta { + 1 => 0, + 2 => 1, + 3..=4 => 2, + 5..=8 => 3, + 9..=16 => 4, + 17..=64 => 5, + _ => 6, + }; + buckets[bucket] += 1; + if first.len() < 8 { + first.push((i, *a, *b)); + } + } + let percent = 100.0 * differing as f64 / ours.len() as f64; + eprintln!( + " {name}: {differing}/{} bytes differ ({percent:.2}%), max |delta| {max_delta}, \ + mean |delta| over the differing bytes {:.2}", + ours.len(), + total_delta as f64 / differing as f64 + ); + eprintln!( + " |delta| histogram 1:{} 2:{} 3-4:{} 5-8:{} 9-16:{} 17-64:{} 65+:{}", + buckets[0], buckets[1], buckets[2], buckets[3], buckets[4], buckets[5], buckets[6] + ); + // One ROW is `width` bytes in both planes — luma because it is `width` + // samples wide, interleaved chroma because it is `width / 2` samples wide + // and two bytes per sample. So one formula serves both, and the chroma + // coordinates it prints are in chroma units. + let positions: Vec = first + .iter() + .map(|(i, a, b)| format!("(x{},y{}) {a}≠{b}", i % width, i / width)) + .collect(); + eprintln!(" first differing: {}", positions.join(" ")); + } + + // A displacement, not a difference: does our luma equal the reference read a + // few rows or columns over? That is what a wrong crop origin or a copy extent + // taken from the pool rather than the render region looks like, and the shift + // that matches IS the error. + if ours[..luma] != want[..luma] { + let mut best: Option<(i32, i32, f64)> = None; + for dy in -4i32..=4 { + for dx in -8i32..=8 { + if (dy, dx) == (0, 0) { + continue; + } + let (mut hit, mut seen) = (0usize, 0usize); + for y in 8..height - 8 { + for x in 8..width - 8 { + let sy = (y as i32 + dy) as usize; + let sx = (x as i32 + dx) as usize; + seen += 1; + if ours[y * width + x] == want[sy * width + sx] { + hit += 1; + } + } + } + let score = hit as f64 / seen as f64; + if best.is_none_or(|(_, _, b)| score > b) { + best = Some((dy, dx, score)); + } + } + } + // The identity's own score, for scale: a decode that is merely slightly + // wrong still matches most bytes in place, so a shift only means something + // when it beats staying put. + let (mut hit, mut seen) = (0usize, 0usize); + for y in 8..height - 8 { + for x in 8..width - 8 { + seen += 1; + if ours[y * width + x] == want[y * width + x] { + hit += 1; + } + } + } + let identity = hit as f64 / seen as f64; + if let Some((dy, dx, score)) = best { + eprintln!( + " luma shift probe: in place {:.3} · best shift dy{dy:+} dx{dx:+} {score:.3}{}", + identity, + if score > identity + 0.05 { + " ⚠ A SHIFT FITS BETTER — this is readback geometry, not decode" + } else { + " (no shift fits better: the pixels are in the right place and \ + carry the wrong values)" + } + ); + } + } +} + +// --------------------------------------------------------------------------- +// CPU coherence guards — NOT `#[ignore]`d. +// +// The legs above only run on the fleet, so without these nothing in ordinary CI +// notices that a re-synced vendored vector, a golden regeneration or an edit to +// `common`'s AU splitters has made the two disagree. They would then fail on the +// fleet as a frame-count mismatch, which reads like a decoder defect and costs a +// hardware round trip to disprove. +// +// Each guard pins the whole chain the parity verdict rests on: the AU split, the +// planner's output count, the vector's shape and the golden set — with NO GPU +// involved. And they are what make the verdicts non-vacuous: a comparison of zero +// frames, or of 250 copies of one digest, would otherwise "pass" on any hardware +// (see [`assert_goldens_are_a_real_set`]). +// --------------------------------------------------------------------------- + +#[test] +fn h265_goldens_and_au_split_agree_with_the_planner() { + use pf_bitstream::h265::H265Planner; + + let goldens = golden_hashes(GOLDENS_H265); + assert_goldens_are_a_real_set(&goldens, FRAME_COUNT, "data/test-25fps-h265.nv12.sha256"); + + // The AU split the parity leg feeds the decoder. `common::split_h265_aus` is + // the copy of pf-bitstream's private splitter, and it keys on HEVC's 2-byte + // NAL header — a `+ 1` there (H.264's offset) silently merges or splits AUs. + let aus = common::split_h265_aus(common::TEST_25FPS_H265); + assert_eq!( + aus.len(), + FRAME_COUNT, + "the vendored H.265 vector is {FRAME_COUNT} access units \ + (pf-bitstream's own planner test pins the same number)" + ); + + // Walk the CPU planner over the same AUs: it is the authority on how many + // frames the GPU leg can possibly deliver, because the decoder builds exactly + // one delivered frame per `dpb.outputs` id (plus the flush tail). + let mut planner = H265Planner::new(); + let mut outputs = 0usize; + let mut iraps = 0usize; + for (index, au) in aus.iter().enumerate() { + let plan = planner.plan_au(au).unwrap_or_else(|e| { + panic!( + "AU {index}: the clean vector must plan without errors, got {e:?} \ + — if this is RaslSkipped the vector has gained CRA/RASL pictures \ + and the parity legs' expected frame count needs rederiving" + ); + }); + outputs += plan.dpb.outputs.len(); + iraps += usize::from(plan.picture.is_irap); + // Pin the picture shape the H.265 legs hard-code. They call + // `probe_stream_support(1, 0)` (4:2:0, 8-bit) and assert the NV12 output + // format; a re-synced Main-10 or 4:4:4 vector would make both of those + // silently probe and expect the WRONG profile on the fleet, which is a + // confusing hardware-only failure. Fail here, on CPU, with the reason. + assert_eq!( + ( + plan.picture.chroma_format_idc, + plan.picture.bit_depth_luma_minus8 + ), + (1, 0), + "AU {index}: the vendored H.265 vector must stay Main 4:2:0 8-bit — \ + the parity and smoke legs hard-code probe_stream_support(1, 0) and \ + an NV12 output format, so a re-synced vector of another shape needs \ + both legs updated, not just the goldens" + ); + if index == 0 { + assert!(plan.picture.is_idr, "the vector opens with an IDR"); + assert_eq!( + (plan.picture.coded_width, plan.picture.coded_height), + DISPLAY_H265, + "the vector is 320x240" + ); + assert_eq!( + ( + plan.picture.display_crop.x, + plan.picture.display_crop.y, + plan.picture.display_crop.width, + plan.picture.display_crop.height, + ), + (0, 0, DISPLAY_H265.0, DISPLAY_H265.1), + "the vector carries NO conformance window — coded size IS display \ + size (the golden header's claim, and what `Readback` asserts)" + ); + } + } + outputs += planner.flush().outputs.len(); + + assert_eq!( + outputs, + goldens.len(), + "the planner outputs {outputs} pictures but the goldens carry {} hashes — \ + the parity leg's frame-count assertion would fail on hardware for a \ + reason that has nothing to do with the GPU", + goldens.len() + ); + // No CRA/BLA anywhere means `PlanError::RaslSkipped` — the Ok-skip that + // returns `Ok(None)` rather than an error (h265 module docs, and + // `VkH265Decoder::decode`'s RASL arm) — is UNREACHABLE on this vector, so the + // count above cannot be perturbed by it. If a re-synced vector ever opens with + // a CRA, this assertion fires first and says where to look. + assert_eq!( + iraps, 1, + "the vector holds exactly one IRAP (the opening IDR); a CRA/BLA would make \ + RASL skips reachable and the expected frame count needs rederiving" + ); +} + +#[test] +fn h264_goldens_and_au_split_agree_with_the_planner() { + use pf_bitstream::h264::H264Planner; + + let goldens = golden_hashes(GOLDENS_H264); + assert_goldens_are_a_real_set(&goldens, FRAME_COUNT, "data/test-25fps.nv12.sha256"); + + let aus = common::split_h264_aus(common::TEST_25FPS_H264); + assert_eq!( + aus.len(), + FRAME_COUNT, + "the vendored H.264 vector is {FRAME_COUNT} access units" + ); + + let mut planner = H264Planner::new(); + let mut outputs = 0usize; + for (index, au) in aus.iter().enumerate() { + let plan = planner + .plan_au(au) + .unwrap_or_else(|e| panic!("AU {index}: the clean vector must plan, got {e:?}")); + outputs += plan.dpb.outputs.len(); + } + outputs += planner.flush().outputs.len(); + assert_eq!( + outputs, + goldens.len(), + "the planner outputs {outputs} pictures but the goldens carry {} hashes", + goldens.len() + ); +} + +#[test] +fn the_main10_vector_is_ten_bit_and_agrees_with_its_goldens() { + use pf_bitstream::h265::H265Planner; + + let goldens = golden_hashes(GOLDENS_MAIN10); + assert_goldens_are_a_real_set(&goldens, MAIN10_FRAME_COUNT, "data/test-main10.p010.sha256"); + + let aus = common::split_h265_aus(TEST_MAIN10_H265); + assert_eq!( + aus.len(), + MAIN10_FRAME_COUNT, + "the Main 10 vector is {MAIN10_FRAME_COUNT} access units" + ); + + let mut planner = H265Planner::new(); + let mut outputs = 0usize; + for (index, au) in aus.iter().enumerate() { + let plan = planner.plan_au(au).unwrap_or_else(|e| { + panic!("AU {index}: the Main 10 vector must plan without errors, got {e:?}") + }); + // The whole reason this vector exists. Every other golden set in this + // program is eight-bit; a regenerated vector that came out eight-bit would + // turn the ten-bit parity leg into a second run of the eight-bit path, and + // it would PASS, because its goldens would have been regenerated with it. + assert_eq!( + ( + plan.picture.chroma_format_idc, + plan.picture.bit_depth_luma_minus8, + plan.picture.bit_depth_chroma_minus8, + ), + (1, 2, 2), + "AU {index}: the Main 10 vector must stay 4:2:0 at ten bits" + ); + if index == 0 { + assert!(plan.picture.is_idr, "the vector opens with an IDR"); + assert_eq!( + (plan.picture.coded_width, plan.picture.coded_height), + (320, 240), + "the goldens hash a 320x240 picture" + ); + } + outputs += plan.dpb.outputs.len(); + } + outputs += planner.flush().outputs.len(); + assert_eq!( + outputs, + goldens.len(), + "the planner outputs {outputs} pictures but the goldens carry {}", + goldens.len() + ); +} + +/// The AV1 leg's whole chain, with no GPU: the golden set, the IVF split, the shape +/// the leg hard-codes, and — the one that matters — that **250 goldens is the +/// DISPLAY count of a 274-frame vector**, re-derived from the planner rather than +/// asserted from a comment. +/// +/// Every number here is a way the fleet run could otherwise fail for a reason that +/// is not the decoder: +/// +/// - a golden file regenerated per CODED frame would carry 274 hashes and the leg +/// would report a frame-count mismatch that reads exactly like dropped frames; +/// - an IVF reader that lost packets would feed a short AU list and the leg would +/// report the same thing; +/// - a re-synced vector at another bit depth, sampling, or with film grain would +/// make `probe_stream_support(1, 8, false)` probe the WRONG Vulkan profile and the +/// readback expect the wrong format, which on hardware surfaces as a caps refusal +/// or half a hashed picture; +/// - and if the 24 hidden frames ever disappeared, the leg would still pass while +/// having quietly stopped exercising multi-frame temporal units at all — the one +/// thing AV1 has that neither H.26x vector does. +#[test] +fn av1_goldens_and_the_ivf_split_agree_with_the_planner() { + use pf_bitstream::av1::Av1Planner; + + let goldens = golden_hashes(GOLDENS_AV1); + assert_goldens_are_a_real_set(&goldens, FRAME_COUNT, "data/test-25fps-av1.nv12.sha256"); + + // The AU split the parity leg feeds the decoder: one IVF packet per temporal + // unit. AV1 carries no start codes, so this is the container's framing rather + // than something a scan could get subtly wrong — but a truncated or re-muxed + // vector would still shorten it silently. + let aus = common::split_av1_aus(common::TEST_25FPS_AV1); + assert_eq!( + aus.len(), + FRAME_COUNT, + "the vendored AV1 vector is {FRAME_COUNT} temporal units" + ); + assert!( + aus.iter().all(|au| !au.is_empty()), + "no temporal unit is empty — an IVF reader that returned empty packets would \ + make the parity leg decode nothing and blame the decoder" + ); + + // Walk the CPU planner over the same temporal units. It is the authority on how + // many frames the GPU leg can possibly deliver: the decoder builds exactly one + // delivered frame per `dpb.outputs` id, and AV1's planner has no `flush` tail. + let mut planner = Av1Planner::new(); + let mut outputs = 0usize; + let mut coded_frames = 0usize; + let mut multi_frame_units = 0usize; + let mut show_existing = 0usize; + let mut warnings = 0usize; + for (index, au) in aus.iter().enumerate() { + let plans = planner.plan_au(au).unwrap_or_else(|e| { + panic!("temporal unit {index}: the clean vector must plan without errors, got {e:?}") + }); + if plans.len() > 1 { + multi_frame_units += 1; + } + for plan in &plans { + coded_frames += 1; + outputs += plan.dpb.outputs.len(); + warnings += plan.warnings.len(); + // A `show_existing_frame` decodes nothing and stores nothing. + if plan.dpb.stored.is_none() { + show_existing += 1; + } + // Pin the picture shape both AV1 legs hard-code. They call + // `probe_stream_support(1, 8, false)` and assert an NV12 output format; + // a re-synced Main-10, 4:4:4 or film-grain vector would make both + // silently probe and expect the WRONG Vulkan decode profile on the + // fleet — grain synthesis is part of the PROFILE, not a per-frame + // toggle, so a grain-bearing vector is a different device requirement, + // not merely different pixels. + assert_eq!( + ( + plan.picture.chroma_format_idc, + plan.picture.bit_depth, + plan.sequence.film_grain_params_present, + ), + (1, 8, false), + "frame {coded_frames} (temporal unit {index}): the vendored AV1 vector \ + must stay Main 4:2:0 8-bit with no film grain" + ); + if coded_frames == 1 { + assert!(plan.picture.is_key, "the vector opens on a key frame"); + // What `Readback` crops to, and what the goldens hash. + assert_eq!( + (plan.picture.render_width, plan.picture.render_height), + DISPLAY_AV1, + "the display (render) region the readback asserts against" + ); + // …and what the pool allocates. Equal to the render region here, so + // the vector needs no AV1 conformance-window equivalent — the golden + // header's claim. + assert_eq!( + (plan.picture.upscaled_width, plan.picture.frame_height), + DISPLAY_AV1, + "the decoded (post-superres) picture IS the display region for \ + this vector — coded size and render size coincide" + ); + } + } + } + + assert_eq!( + outputs, + goldens.len(), + "the planner outputs {outputs} pictures but the goldens carry {} hashes — the \ + parity leg's frame-count assertion would fail on hardware for a reason that \ + has nothing to do with the GPU", + goldens.len() + ); + assert_eq!( + coded_frames, + AV1_CODED_FRAME_COUNT, + "the vendored AV1 vector codes {AV1_CODED_FRAME_COUNT} frames; {} of them are \ + hidden, which is why the goldens are {FRAME_COUNT} and not {coded_frames}", + AV1_CODED_FRAME_COUNT - FRAME_COUNT + ); + assert_eq!( + multi_frame_units, 24, + "24 temporal units carry two frames each — the hidden ALTREFs, and the only \ + reason AV1's `plan_au` returns a vector at all. If this reaches 0 the parity \ + leg has stopped exercising multi-frame temporal units while still passing" + ); + assert_eq!( + show_existing, 0, + "this vector uses no `show_existing_frame`; if that ever changes, frames start \ + being displayed by a route the decoder handles differently and the display \ + order the goldens assume needs rederiving" + ); + assert_eq!( + warnings, 0, + "a clean conformance vector must plan without concealment — any warning here \ + means the parity leg would be hashing concealed pixels against a clean \ + reference" + ); +} + +/// The vendored frame-0 pixels ARE the first golden — not a second opinion about it. +/// +/// [`AV1_FRAME0`] is the one place in this file where reference PIXELS live rather +/// than hashes, and pixels are exactly the kind of file that rots: regenerate the +/// goldens from a re-synced vector and this blob keeps describing the old one, while +/// the diagnostic leg that reads it goes on confidently naming the wrong cause. So +/// its digest is re-derived here and compared against `GOLDENS_AV1`'s first line — +/// the trusted, three-way cross-checked set — on every platform, with no GPU. +/// +/// It also pins the layout the diagnostic's arithmetic assumes: 320x240 tightly +/// packed NV12 is 115200 bytes, luma first. +#[test] +fn the_av1_frame0_reference_is_the_first_golden() { + let (width, height) = (DISPLAY_AV1.0 as usize, DISPLAY_AV1.1 as usize); + assert_eq!( + AV1_FRAME0.len(), + width * height * 3 / 2, + "data/test-25fps-av1.frame0.nv12 must be one tightly packed NV12 frame of \ + the vector's render region" + ); + let goldens = golden_hashes(GOLDENS_AV1); + assert_goldens_are_a_real_set(&goldens, FRAME_COUNT, "data/test-25fps-av1.nv12.sha256"); + assert_eq!( + sha256_hex(AV1_FRAME0), + goldens[0], + "the vendored frame-0 pixels must hash to the AV1 golden set's FIRST entry — \ + if they no longer do, the blob is from a different decode than the goldens \ + and `av1_frame0_pixels_say_which_plane_and_how_badly` would attribute a \ + divergence to the wrong cause. Regenerate it alongside the goldens: decode \ + the vector with `-f rawvideo -pix_fmt nv12 -fps_mode passthrough` and take \ + the first 115200 bytes (the golden file's header carries the full command)" + ); + // Not a flat blob: a frame of one repeated byte would satisfy a length check and + // make every per-plane statistic in the diagnostic meaningless. + let luma = &AV1_FRAME0[..width * height]; + let chroma = &AV1_FRAME0[width * height..]; + assert!( + luma.iter().any(|b| *b != luma[0]) && chroma.iter().any(|b| *b != chroma[0]), + "both planes must carry real picture content" + ); +} + +/// Count Annex-B start codes in `stream` as `(total, three_byte)`. +/// +/// Emulation prevention guarantees `00 00 01` cannot occur inside a NAL payload, +/// so every hit is a real prefix; a hit not preceded by a zero byte is a +/// three-byte one. +fn annexb_prefixes(stream: &[u8]) -> (usize, usize) { + let mut total = 0; + let mut three_byte = 0; + for i in 0..stream.len().saturating_sub(2) { + if stream[i..i + 3] == [0x00, 0x00, 0x01] { + total += 1; + if i == 0 || stream[i - 1] != 0x00 { + three_byte += 1; + } + } + } + (total, three_byte) +} + +// The two guards below are what stop the four-byte hardware legs from passing +// vacuously. Those legs assert that a rewritten vector decodes to the SAME +// goldens as the original — which is trivially true if the rewrite quietly +// returned its input, or dropped NALs the planner never missed. Nothing on the +// fleet would notice; these notice in ordinary CI, with the reason. + +#[test] +fn the_h264_four_byte_rewrite_changes_prefixes_and_nothing_else() { + use pf_bitstream::h264::H264Planner; + + let original = common::TEST_25FPS_H264; + let rewritten = common::h264_four_byte_start_codes(original); + + let (original_total, original_three) = annexb_prefixes(original); + let (rewritten_total, rewritten_three) = annexb_prefixes(&rewritten); + + assert!( + original_three > 0, + "the vendored H.264 vector is supposed to carry THREE-byte start codes; \ + if it no longer does, `h264_four_byte_start_codes_decode_bit_identically` \ + is feeding the hardware the same bytes as the leg above it and proves \ + nothing" + ); + assert_eq!( + rewritten_three, 0, + "every start code in the rewritten stream must be four-byte — {rewritten_three} \ + of {rewritten_total} are not" + ); + assert_eq!( + rewritten_total, original_total, + "the rewrite must preserve the NAL count exactly ({original_total}), not \ + drop or invent units" + ); + assert!( + rewritten.len() > original.len(), + "widening every prefix cannot shrink the stream" + ); + + // Same access units, same planner verdict: the rewrite changed the framing + // and nothing the decoder acts on. + let aus = common::split_h264_aus(&rewritten); + assert_eq!( + aus.len(), + common::split_h264_aus(original).len(), + "the rewritten stream must split into the same access units" + ); + assert_eq!( + aus.len(), + FRAME_COUNT, + "…and there are {FRAME_COUNT} of them" + ); + + let mut planner = H264Planner::new(); + let mut outputs = 0usize; + for (index, au) in aus.iter().enumerate() { + let plan = planner.plan_au(au).unwrap_or_else(|e| { + panic!("AU {index}: the four-byte rewrite must plan as the original does, got {e:?}") + }); + outputs += plan.dpb.outputs.len(); + } + outputs += planner.flush().outputs.len(); + assert_eq!( + outputs, FRAME_COUNT, + "the rewritten vector must still output {FRAME_COUNT} pictures" + ); +} + +#[test] +fn the_h265_four_byte_rewrite_changes_prefixes_and_nothing_else() { + use pf_bitstream::h265::H265Planner; + + let original = common::TEST_25FPS_H265; + let rewritten = common::h265_four_byte_start_codes(original); + + let (original_total, original_three) = annexb_prefixes(original); + let (rewritten_total, rewritten_three) = annexb_prefixes(&rewritten); + + assert!( + original_three > 0, + "the vendored H.265 vector is supposed to carry THREE-byte start codes; \ + if it no longer does, `h265_four_byte_start_codes_decode_bit_identically` \ + proves nothing" + ); + assert_eq!( + rewritten_three, 0, + "every start code in the rewritten stream must be four-byte — {rewritten_three} \ + of {rewritten_total} are not" + ); + assert_eq!( + rewritten_total, original_total, + "the rewrite must preserve the NAL count exactly ({original_total})" + ); + assert!( + rewritten.len() > original.len(), + "widening every prefix cannot shrink the stream" + ); + + let aus = common::split_h265_aus(&rewritten); + assert_eq!( + aus.len(), + common::split_h265_aus(original).len(), + "the rewritten stream must split into the same access units" + ); + assert_eq!( + aus.len(), + FRAME_COUNT, + "…and there are {FRAME_COUNT} of them" + ); + + let mut planner = H265Planner::new(); + let mut outputs = 0usize; + for (index, au) in aus.iter().enumerate() { + let plan = planner.plan_au(au).unwrap_or_else(|e| { + panic!("AU {index}: the four-byte rewrite must plan as the original does, got {e:?}") + }); + outputs += plan.dpb.outputs.len(); + } + outputs += planner.flush().outputs.len(); + assert_eq!( + outputs, FRAME_COUNT, + "the rewritten vector must still output {FRAME_COUNT} pictures" + ); +} diff --git a/crates/pf-vkdecode/tests/gpu_smoke.rs b/crates/pf-vkdecode/tests/gpu_smoke.rs new file mode 100644 index 00000000..b7c4bba3 --- /dev/null +++ b/crates/pf-vkdecode/tests/gpu_smoke.rs @@ -0,0 +1,426 @@ +//! GPU smoke tests — `#[ignore]`d because they need real Vulkan Video hardware. +//! +//! Run on a Vulkan-Video box with: +//! +//! ```text +//! cargo test -p pf-vkdecode -- --ignored +//! ``` +//! +//! Environment expectations (the fleet's RADV boxes .21/.25, the NVIDIA .173, or +//! any machine like them): +//! - a Vulkan 1.3 loader on the library path (`libvulkan.so.1` / `vulkan-1.dll`); +//! - a physical device advertising `VK_KHR_video_queue`, +//! `VK_KHR_video_decode_queue` and the leg's codec extension +//! (`VK_KHR_video_decode_h264` / `VK_KHR_video_decode_h265` / +//! `VK_KHR_video_decode_av1`), with a queue +//! family carrying `VIDEO_DECODE_KHR` ops for that codec; +//! - `timelineSemaphore` + `synchronization2` feature support (Vulkan 1.3 core); +//! - on RADV, `RADV_PERFTEST=video_decode` in the environment — for AV1 exactly as +//! for the other two, and without it the AV1 leg reports missing silicon it has. +//! +//! One leg per codec, running the SAME body ([`smoke`]) over the vendored 25fps +//! vector of that codec — a box that decodes only some of the three runs those legs +//! and reports the rest as "no physical device with VK_KHR_video_decode_…", which is +//! a fact about the box rather than a failure (AV1 is the one most likely to say so +//! on today's fleet). Device bring-up lives in `tests/common/mod.rs`. +//! +//! What they prove: device wrap → caps query/derivation on REAL caps → session + +//! parameters creation → the decoupled picture pool → 48 AUs of the vendored +//! 25fps vector decoded through `vkCmdDecodeVideoKHR` — well past DPB-full, so +//! slot re-activation binds fresh pool images repeatedly — while the consumer +//! HOLDS FOUR delivered frames unreleased at steady state (the real client's +//! pipeline shape: bounded channels, FrameStore preroll, in-flight present). +//! Every frame's RESULT_STATUS_ONLY query must read COMPLETE before its +//! release. This is the regression test for the .25 field failure class: any +//! pool sizing that ignores the stream's DPB depth or the client's hold depth +//! starves exactly here. What they deliberately do NOT prove (that is +//! `gpu_parity`'s and WP-D on-glass's ground): pixel correctness vs the ffmpeg +//! rung, presenter interop (the `value + 1` signal-back — no presenter runs here, +//! so releases pass `false`), soak, and both vendors' DPB arrangements at once +//! (each box exercises only its own). + +#![deny(clippy::undocumented_unsafe_blocks)] + +mod common; + +use ash::vk; +use common::TestDecoder; +use pf_vkdecode::DecodeStatus; +use pf_vkdecode::DecodedVkFrame; +use pf_vkdecode::NoopQueueLock; +use pf_vkdecode::VkAv1Decoder; +use pf_vkdecode::VkH264Decoder; +use pf_vkdecode::VkH265Decoder; + +/// AUs fed: far past every vector's DPB depth (`max_dpb_frames = 7` for the +/// H.264 clip, eight reference slots for AV1), so DPB slots re-activate onto fresh +/// pool images repeatedly. +const AUS: usize = 48; +/// The REAL client's consumption shape: the consumer holds four delivered frames +/// and releases only the oldest beyond that (its channels + preroll + in-flight +/// present hold ~4-7). +const CLIENT_HOLD: usize = 4; +/// 48 AUs may legitimately leave a few pictures buffered for reorder; anything +/// below this is a delivery failure, not reordering. +const MIN_DELIVERED: usize = 40; + +/// The geometry one leg's vector must deliver. +struct Geometry { + /// The vector's display (conformance-window) region. + display: (u32, u32), + /// The ALLOCATED extent, when the leg knows it for a fact. + /// + /// `pictureAccessGranularity` rounds the coded size up, so this is a + /// per-vector AND per-driver fact, not a property of the bitstream. The H.264 + /// leg has asserted `(320, 240)` on the fleet since WP-B and keeps asserting + /// it; the H.265 leg has NO hardware evidence yet, so it asserts only the + /// invariant that always holds (allocated >= display) and PRINTS what it got + /// — which is exactly what a first fleet run needs in order to pin it later. + exact_coded: Option<(u32, u32)>, +} + +/// Decode [`AUS`] access units while holding [`CLIENT_HOLD`] frames, asserting the +/// decode verdict of every frame before its release. +/// +/// One body for all three codecs (over `common::TestDecoder`) so "the AV1 leg proves +/// what the H.264 leg proves" is structural rather than a claim about three copies. +fn smoke(decoder: &mut impl TestDecoder, aus: &[&[u8]], geometry: &Geometry) { + // The smoke legs exist to prove the PRODUCTION pool arrangement survives 48 + // AUs at the client's hold depth. `PF_VKD_TEST_READBACK` adds TRANSFER_SRC to + // the picture pool for whoever sets it, so a shell that exported it while + // iterating on the parity legs would quietly test a pool production never + // builds — and the leg would still pass. Refuse rather than mislead. + assert!( + std::env::var_os("PF_VKD_TEST_READBACK").is_none(), + "PF_VKD_TEST_READBACK is set in the environment: it grows the picture pool \ + a usage flag production never carries, so this leg would no longer be \ + testing the production pool arrangement. Unset it for the smoke legs \ + (the parity legs set it themselves, under the same GPU lock)." + ); + // Status is read (COMPLETE required, the program's whole point) as each frame + // retires; `take_ready` is drained every AU so nothing is stranded. No + // presenter runs here, so releases report `presenter_signaled = false` (no + // `value + 1` write-back). + let mut held: std::collections::VecDeque = std::collections::VecDeque::new(); + let mut delivered = 0usize; + let mut geometry_checked = false; + for (index, au) in aus.iter().enumerate().take(AUS) { + let mut next = decoder.decode(au).unwrap_or_else(|e| { + panic!( + "AU {index}: decode failed: {e}\n state: {}", + decoder.debug_snapshot() + ) + }); + while let Some(frame) = next { + if !geometry_checked { + assert_eq!( + (frame.crop.width, frame.crop.height), + geometry.display, + "the vector's display region" + ); + assert!( + frame.coded_width >= frame.crop.width + && frame.coded_height >= frame.crop.height, + "the ALLOCATED extent ({}x{}) must cover the display region ({}x{})", + frame.coded_width, + frame.coded_height, + frame.crop.width, + frame.crop.height, + ); + if let Some(exact) = geometry.exact_coded { + assert_eq!( + (frame.coded_width, frame.coded_height), + exact, + "ALLOCATED extent (this vector needs no granularity padding here)" + ); + } + // A pool built for the wrong picture format decodes and then + // renders with the wrong maths (`DecodedVkFrame::format` docs); + // all three vectors are 8-bit 4:2:0, so all three must land on NV12. + assert_eq!( + frame.format, + pf_vkdecode::NV12, + "8-bit 4:2:0 vector must decode into an NV12 pool" + ); + assert_ne!(frame.image, vk::Image::null()); + assert_ne!(frame.semaphore, vk::Semaphore::null()); + assert!(frame.value > 0); + eprintln!( + "geometry: allocated {}x{} display {}x{} format {:?} layout {:?}", + frame.coded_width, + frame.coded_height, + frame.crop.width, + frame.crop.height, + frame.format, + frame.layout, + ); + geometry_checked = true; + } + held.push_back(frame); + delivered += 1; + // Steady state: keep CLIENT_HOLD frames in hand, retire beyond. + while held.len() > CLIENT_HOLD { + let oldest = held.pop_front().expect("nonempty"); + assert_eq!( + decoder.wait_status(&oldest), + DecodeStatus::Ok, + "AU {index}: decode op not COMPLETE\n state: {}", + decoder.debug_snapshot() + ); + decoder + .release_frame(&oldest, false) + .unwrap_or_else(|e| panic!("AU {index}: release failed: {e}")); + } + next = decoder.take_ready(); + } + } + // Retire the tail the consumer still holds. + for frame in held.drain(..) { + assert_eq!(decoder.wait_status(&frame), DecodeStatus::Ok); + decoder + .release_frame(&frame, false) + .expect("tail frames release"); + } + assert!( + delivered >= MIN_DELIVERED, + "expected at least {MIN_DELIVERED} delivered frames from {AUS} AUs, got {delivered}" + ); + // The DPB mode the caps derivation chose, and whether this box answers per-op + // status at all — a passing run should say so too (failure paths already carry + // the snapshot). + eprintln!( + "final state: {} status_queries={}", + decoder.debug_snapshot(), + decoder.status_queries() + ); +} + +#[test] +#[ignore = "needs a Vulkan Video H.264 decode device (fleet boxes; see module docs)"] +fn h264_decodes_48_aus_holding_four_frames_like_the_real_client() { + // One codec at a time on the device (see `common::gpu_lock`). + let _gpu = common::gpu_lock(); + + let setup = common::bring_up(&common::Request { + codec: common::H264, + // The smoke legs submit nothing outside the decoder, so a decode-only + // device is usable (and its EXCLUSIVE pool sharing is worth exercising). + graphics: common::Graphics::DecodeFamilyIsFine, + report_families: true, + }); + let handles = setup.handles(); + { + // SAFETY: `setup` outlives this block (destroyed below, after the decoder + // drops at the block's end), it was created with the H.264 decode + // extensions + timeline/sync2 features, and its queue fields name the + // families/queues it created. + let mut decoder = unsafe { VkH264Decoder::new(&handles, Box::new(NoopQueueLock)) } + .expect("wrap the device"); + smoke( + &mut decoder, + &common::split_h264_aus(common::TEST_25FPS_H264), + &Geometry { + display: (320, 240), + exact_coded: Some((320, 240)), + }, + ); + } + // SAFETY: the decoder is gone (its Drop drained the queue and destroyed its + // session/pools), and nothing else references the setup's handles. + unsafe { setup.destroy() }; +} + +#[test] +#[ignore = "needs a Vulkan Video H.265 decode device (fleet boxes; see module docs)"] +fn h265_decodes_48_aus_holding_four_frames_like_the_real_client() { + // One codec at a time on the device (see `common::gpu_lock`). + let _gpu = common::gpu_lock(); + + let setup = common::bring_up(&common::Request { + codec: common::H265, + graphics: common::Graphics::DecodeFamilyIsFine, + report_families: true, + }); + let handles = setup.handles(); + { + // SAFETY: as the H.264 leg — `setup` outlives this block and was created + // with the H.265 decode extensions + timeline/sync2 features. + let mut decoder = unsafe { VkH265Decoder::new(&handles, Box::new(NoopQueueLock)) } + .expect("wrap the device"); + // The construction-time shape gate the client's ladder relies on, on the + // vector's own facts (Main, 4:2:0, 8-bit → NV12). Called here rather than + // left to the first AU so a device that cannot host the combination says + // so as a refusal with a caps reason, not as a mid-stream decode failure — + // and so this path has hardware evidence at all. + decoder + .probe_stream_support(1, 0) + .expect("the box must host H.265 Main 8-bit 4:2:0 (the vector's shape)"); + smoke( + &mut decoder, + &common::split_h265_aus(common::TEST_25FPS_H265), + &Geometry { + display: (320, 240), + // No hardware evidence for HEVC's `pictureAccessGranularity` on + // any fleet box yet; the leg prints what it allocates instead of + // asserting a number nobody has observed. + exact_coded: None, + }, + ); + } + // SAFETY: as the H.264 leg — the decoder is gone and nothing else references + // the setup's handles. + unsafe { setup.destroy() }; +} + +/// The AV1 leg — the rung's first hardware evidence of ANY kind. +/// +/// The same 48 access units at the same client hold depth, but AV1 loads the pool +/// harder than either H.26x leg does and that is the point of running it: the first +/// 48 temporal units carry 53 coded frames to show 48 (the number +/// [`the_delivery_floor_is_under_what_the_planners_emit_from_the_first_48_aus`] +/// prints), each hidden frame keeps a pool image resident as a reference while +/// nothing displays it, and eight reference slots re-activate against that. A pool +/// sized as if one access unit meant one picture starves exactly here — which is this +/// leg's whole job, since `gpu_parity`'s AV1 leg would report the same starvation as a +/// decode failure with a less obvious cause. +#[test] +#[ignore = "needs a Vulkan Video AV1 decode device (fleet boxes; see module docs)"] +fn av1_decodes_48_aus_holding_four_frames_like_the_real_client() { + // One codec at a time on the device (see `common::gpu_lock`). + let _gpu = common::gpu_lock(); + + let setup = common::bring_up(&common::Request { + codec: common::AV1, + graphics: common::Graphics::DecodeFamilyIsFine, + report_families: true, + }); + let handles = setup.handles(); + { + // SAFETY: as the H.264 leg — `setup` outlives this block and was created + // with the AV1 decode extension + timeline/sync2 features. + let mut decoder = unsafe { VkAv1Decoder::new(&handles, Box::new(NoopQueueLock)) } + .expect("wrap the device"); + // The construction-time shape gate on the vector's own facts (Main, 4:2:0, + // 8-bit, NO film grain → NV12). The film-grain argument is the one that has + // no H.26x counterpart: grain synthesis is part of the Vulkan decode PROFILE, + // so a box offering only the grain-enabled profile refuses HERE with a caps + // reason rather than at the first temporal unit. + decoder.probe_stream_support(1, 8, false).expect( + "the box must host AV1 Main 4:2:0 8-bit without film grain (the vector's shape)", + ); + smoke( + &mut decoder, + &common::split_av1_aus(common::TEST_25FPS_AV1), + &Geometry { + display: (320, 240), + // As HEVC: no hardware evidence for AV1's `pictureAccessGranularity` + // on any fleet box yet, so the leg prints what it allocates rather + // than asserting a number nobody has observed. AV1's decode extent is + // the POST-superres width, which is another reason not to guess. + exact_coded: None, + }, + ); + } + // SAFETY: as the H.264 leg — the decoder is gone and nothing else references + // the setup's handles. + unsafe { setup.destroy() }; +} + +// --------------------------------------------------------------------------- +// CPU coherence guards — NOT `#[ignore]`d. +// +// The legs above only run on the fleet, so [`MIN_DELIVERED`] would otherwise be a +// number copied from the H.264 leg and never checked against the H.265 or AV1 +// vector's own reorder depth. It is the CPU planner that decides how many of the +// first [`AUS`] pictures can possibly be delivered — the decoder builds exactly one +// frame per `dpb.outputs` id — so the floor is checkable here, without a GPU, and +// a re-synced vector that reorders more deeply fails HERE instead of looking like +// a pool-starvation bug on hardware. +// --------------------------------------------------------------------------- + +#[test] +fn the_delivery_floor_is_under_what_the_planners_emit_from_the_first_48_aus() { + let h264 = { + let mut planner = pf_bitstream::h264::H264Planner::new(); + common::split_h264_aus(common::TEST_25FPS_H264) + .iter() + .take(AUS) + .enumerate() + .map(|(index, au)| { + planner + .plan_au(au) + .unwrap_or_else(|e| panic!("H.264 AU {index} must plan, got {e:?}")) + .dpb + .outputs + .len() + }) + .sum::() + }; + let h265 = { + let mut planner = pf_bitstream::h265::H265Planner::new(); + common::split_h265_aus(common::TEST_25FPS_H265) + .iter() + .take(AUS) + .enumerate() + .map(|(index, au)| { + planner + .plan_au(au) + .unwrap_or_else(|e| panic!("H.265 AU {index} must plan, got {e:?}")) + .dpb + .outputs + .len() + }) + .sum::() + }; + // AV1 needs the extra fold: one temporal unit can plan SEVERAL frames, so the + // outputs of a unit are the outputs of all of its plans — and counting one plan + // per unit is exactly how a reader would under-count here. + let (av1, av1_frames) = { + let mut planner = pf_bitstream::av1::Av1Planner::new(); + let mut outputs = 0usize; + let mut frames = 0usize; + for (index, au) in common::split_av1_aus(common::TEST_25FPS_AV1) + .iter() + .take(AUS) + .enumerate() + { + let plans = planner + .plan_au(au) + .unwrap_or_else(|e| panic!("AV1 temporal unit {index} must plan, got {e:?}")); + frames += plans.len(); + outputs += plans.iter().map(|p| p.dpb.outputs.len()).sum::(); + } + (outputs, frames) + }; + eprintln!( + "outputs from the first {AUS} AUs: h264={h264} h265={h265} av1={av1} \ + (av1 decoded {av1_frames} frames to show {av1} — the hidden ones)" + ); + // No `flush` here on purpose: the smoke legs do not flush either, so the + // planner's un-flushed output count is exactly the frame budget they have. + assert!( + h264 >= MIN_DELIVERED, + "the H.264 leg asserts >= {MIN_DELIVERED} delivered but the planner only \ + outputs {h264} pictures from the first {AUS} AUs" + ); + assert!( + h265 >= MIN_DELIVERED, + "the H.265 leg asserts >= {MIN_DELIVERED} delivered but the planner only \ + outputs {h265} pictures from the first {AUS} AUs" + ); + assert!( + av1 >= MIN_DELIVERED, + "the AV1 leg asserts >= {MIN_DELIVERED} delivered but the planner only \ + outputs {av1} pictures from the first {AUS} temporal units" + ); + // The AV1 leg's load is not the same as the other two's, and the assertion above + // cannot see the difference: the pool must hold the hidden frames as well as the + // shown ones. If these ever became equal, the leg would have stopped exercising + // multi-frame temporal units — the one pool pressure AV1 has that H.26x has not — + // while still passing everything above. + assert!( + av1_frames > av1, + "the first {AUS} AV1 temporal units must decode MORE frames ({av1_frames}) \ + than they show ({av1}); equal counts mean the hidden-frame coverage is gone" + ); +} diff --git a/crates/pf-zerocopy/src/imp/cuda.rs b/crates/pf-zerocopy/src/imp/cuda.rs index 5d5a0a47..bfb09f48 100644 --- a/crates/pf-zerocopy/src/imp/cuda.rs +++ b/crates/pf-zerocopy/src/imp/cuda.rs @@ -62,6 +62,47 @@ pub fn read_plane_to_host( Ok(host) } +/// Upload a tightly-packed host plane into a pitched device plane `(dst_ptr, dst_pitch)`. +/// Synchronous on the priority stream. The exact mirror of [`read_plane_to_host`]. +/// +/// Not a hot path and never used by a session — this exists so ENCODE BENCHMARKS can put real, +/// high-entropy content in front of the encoder. Every synthetic frame this crate could otherwise +/// produce is uninitialised device memory, which the driver hands back **zeroed**; under CBR the +/// rate controller then runs out of things to code and every measurement collapses into the +/// low-bits/frame corner (~300 B/AU against an 833 KB quota, measured). That made the entire +/// split-encode programme blind to the bits/frame regime, which is the regime the field report +/// came from. +pub fn write_plane_from_host( + dst_ptr: CUdeviceptr, + dst_pitch: usize, + src: &[u8], + width_bytes: usize, + height: usize, +) -> Result<()> { + anyhow::ensure!( + src.len() >= width_bytes * height, + "write_plane_from_host: source is {} bytes, need {}", + src.len(), + width_bytes * height + ); + let copy = CUDA_MEMCPY2D { + srcMemoryType: 1, // CU_MEMORYTYPE_HOST + srcHost: src.as_ptr() as *const c_void, + srcPitch: width_bytes, + dstMemoryType: CU_MEMORYTYPE_DEVICE, + dstDevice: dst_ptr, + dstPitch: dst_pitch, + WidthInBytes: width_bytes, + Height: height, + ..Default::default() + }; + // SAFETY: mirrors `read_plane_to_host`. `©` is a live local `#[repr(C)] CUDA_MEMCPY2D` + // outliving the synchronous call; `srcHost` addresses `src`, checked above to hold at least + // `width_bytes*height` bytes, and `dstDevice`/`dstPitch` are the caller's live pitched device + // plane. The copy is synchronous, so `src` need not outlive the call. + unsafe { copy_blocking(©, "cuMemcpy2DAsync_v2(host->dev)") } +} + /// Export a device allocation (from `cuMemAllocPitch`/`cuMemAlloc`) as a cross-process CUDA IPC /// handle — an opaque 64-byte blob another process opens with [`ipc_open`]. The allocation must /// stay alive for as long as any importer has it open. The shared context must be current. diff --git a/crates/punktfunk-core/src/quic/caps.rs b/crates/punktfunk-core/src/quic/caps.rs index af11d01a..45c36380 100644 --- a/crates/punktfunk-core/src/quic/caps.rs +++ b/crates/punktfunk-core/src/quic/caps.rs @@ -58,15 +58,16 @@ pub const VIDEO_CAP_CHACHA20: u8 = 0x40; /// [`Hello::video_caps`] bit: the client's decoder accepts **multi-slice access units** — H.264/ /// HEVC frames carrying several slice NALs (latency plan §7 LN1: the encoder splits frames so /// sub-frame readback can ship early slices while the tail encodes). Decoder-level, so the -/// EMBEDDER sets it from what its decode stack actually handles: the desktop clients' FFmpeg/ -/// D3D11VA/Vulkan-video decoders are fine, but mobile/TV MediaCodec is per-SoC — Amlogic HEVC -/// decoders (Chromecast with Google TV, Fire TV) wedge the whole DEVICE on multi-slice frames -/// (the 0.17.0 field regression: the 4-slice Linux default froze streams on first frame and -/// watchdog-rebooted the CCwGTV), which is exactly why Moonlight requests 1 slice per frame for -/// every hardware decoder. The host defaults to >1 slice ONLY toward a client that sets this -/// bit (`PUNKTFUNK_NVENC_SLICES` stays the explicit operator override in both directions); -/// every other client gets single-slice frames — the pre-0.17 wire shape. NOTE: this takes the -/// video_caps byte's last free bit — the next video cap needs a second byte (ABI bump). +/// EMBEDDER sets it from what its decode stack actually handles: every desktop decode stack +/// (Vulkan Video, D3D11VA, VAAPI, openh264/rav1d) is fine, but mobile/TV MediaCodec is per-SoC +/// — Amlogic HEVC decoders (Chromecast with Google TV, Fire TV) wedge the whole DEVICE on +/// multi-slice frames (the 0.17.0 field regression: the 4-slice Linux default froze streams on +/// first frame and watchdog-rebooted the CCwGTV), which is exactly why Moonlight requests 1 +/// slice per frame for every hardware decoder. The host defaults to >1 slice ONLY toward a +/// client that sets this bit (`PUNKTFUNK_NVENC_SLICES` stays the explicit operator override in +/// both directions); every other client gets single-slice frames — the pre-0.17 wire shape. +/// NOTE: this takes the video_caps byte's last free bit — the next video cap needs a second +/// byte (ABI bump). pub const VIDEO_CAP_MULTI_SLICE: u8 = 0x80; /// [`Welcome::host_caps`] bit: the host applies [`InputKind::GamepadState`] diff --git a/crates/punktfunk-core/src/reanchor.rs b/crates/punktfunk-core/src/reanchor.rs index 795630d9..d005cc6c 100644 --- a/crates/punktfunk-core/src/reanchor.rs +++ b/crates/punktfunk-core/src/reanchor.rs @@ -13,6 +13,13 @@ //! and — over the C ABI — the Apple client). The state machine is time-driven but takes `now` as a //! parameter so it is unit-testable without a clock; the C ABI wrappers supply `Instant::now()`. //! +//! A client whose decoder parses the bitstream ITSELF has a fourth, independent way to see a clean +//! re-anchor: the **recovery point SEI**, which an intra-refresh encoder emits to name the picture at +//! which its wave has healed the frame. [`on_local_recovery`](ReanchorGate::on_local_recovery) is +//! that path. It is strictly ADDITIONAL — a client without a local parser (Android MediaCodec, Apple +//! VideoToolbox, every FFmpeg rung, which exposes no SEI) simply never calls it and every wire +//! behaviour above is bit-for-bit unchanged. +//! //! [`USER_FLAG_RECOVERY_POINT`]: crate::packet::USER_FLAG_RECOVERY_POINT //! [`USER_FLAG_RECOVERY_ANCHOR`]: crate::packet::USER_FLAG_RECOVERY_ANCHOR @@ -100,6 +107,37 @@ fn reanchor_after_frame( } } +/// What a client's OWN bitstream parser saw about intra-refresh recovery on one decoded frame — the +/// in-band counterpart of the wire's [`USER_FLAG_RECOVERY_POINT`](crate::packet::USER_FLAG_RECOVERY_POINT). +/// +/// Two facts rather than one verdict, because the gate needs both and only the gate knows how to +/// combine them. A recovery point SEI promises: *a decoder that starts at THIS AU has a correct +/// picture N frames later*. That promise covers a decoder which lost references BEFORE the SEI (the +/// wave re-codes every stripe after it, so the stale content is fully overwritten) and says nothing +/// at all about one which lost references AFTER it (the already-swept stripes still reference the +/// lost picture). So a recovery point may only lift a freeze when its SEI was observed at or after +/// the loss — which is the pairing [`ReanchorGate::on_local_recovery`] performs, since the gate is +/// the only party that knows when the loss was. +/// +/// Produced by pf-vkdecode's `RecoveryWatch` on the native decode lane. Every other lane leaves it +/// [`Default`] and nothing changes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct LocalRecovery { + /// The AU that produced this frame carried a recovery point SEI — a heal starts here. + pub sei_here: bool, + /// This frame IS the recovery point a previously-seen SEI named — the heal completed. + pub is_recovery_point: bool, +} + +impl LocalRecovery { + /// Nothing observed — what every frame of a stream without recovery point SEIs reports, and what + /// every client without a local parser passes. + pub const NONE: LocalRecovery = LocalRecovery { + sei_here: false, + is_recovery_point: false, + }; +} + /// Whether a decoded frame should be shown or withheld while the gate is (or isn't) frozen. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GateVerdict { @@ -136,6 +174,17 @@ pub struct ReanchorGate { /// The last `frames_dropped` value [`poll`](Self::poll) observed; a climb means the reassembler /// declared an AU unrecoverable and the following deltas will conceal, so arm. last_dropped: u64, + /// A local recovery point SEI has been observed SINCE the latest arm — the precondition for a + /// later recovery point to be trusted ([`LocalRecovery`]). Zeroed at every arm, so a heal that + /// began before the loss can never lift the freeze the loss raised. + local_sei_since_arm: bool, + /// How many times the freeze has been armed ([`Self::arms`]) — a monotonic counter, never + /// reset. Only a client with its OWN bitstream parser needs it: `local_sei_since_arm` pairs an + /// SEI against the arm in TIME, but a decoder can hand back a frame it decoded *before* the + /// loss (a post-failure DPB flush does exactly that), and no wall clock separates those. Such + /// a client stamps the decoder's decode-order watermark whenever this counter moves and + /// discards the local recovery of anything older. Every other client ignores it. + arms: u64, } impl ReanchorGate { @@ -148,9 +197,24 @@ impl ReanchorGate { deadline: None, no_output_streak: 0, last_dropped: frames_dropped, + local_sei_since_arm: false, + arms: 0, } } + /// How many times the freeze has been armed since the gate was created — monotonic, never + /// reset, and moved by EVERY arm site including the ones inside [`Self::on_no_output`] and + /// [`Self::poll`] (but not by the overdue backstop, which re-asks without re-arming). + /// + /// A client whose decoder parses the bitstream itself watches this so it can pair a frame's + /// [`LocalRecovery`] against the arm by DECODE ORDER rather than by arrival: a decoder that + /// flushes its DPB after a failed AU hands back pictures decoded before the loss, and their + /// recovery marks describe a wave that completed before it. Every other client can ignore + /// this entirely — nothing in the gate's own behaviour reads it. + pub fn arms(&self) -> u64 { + self.arms + } + /// Arm the freeze: a loss was detected (a frame-index gap, a dropped-count climb, or a decoder /// wedge/demotion). Zeroes the mark count so a fresh loss waits out two fresh recovery marks, and /// (re-)sets the backstop deadline. Idempotent while already frozen (re-arming just re-zeroes the @@ -158,9 +222,60 @@ impl ReanchorGate { pub fn arm(&mut self, now: Instant) { self.awaiting = true; self.marks = 0; + self.arms = self.arms.saturating_add(1); + // A heal that was already in flight when this loss landed proves nothing about it: the + // stripes the wave already swept still reference the picture we just lost. Only an SEI seen + // from here on may be trusted ([`LocalRecovery`]). + self.local_sei_since_arm = false; self.deadline = Some(now + REANCHOR_FREEZE_MAX); } + /// Fold the client's OWN recovery-point observation for one decoded frame, BEFORE handing that + /// frame to [`on_decoded`](Self::on_decoded). Returns `true` when it lifted the freeze. + /// + /// This is the only re-anchor signal that does not come off the wire, and it exists because + /// intra-refresh sessions otherwise have NO clean point a client can see. The host's wave never + /// emits an IDR; libavcodec flags `AV_FRAME_FLAG_KEY` only for true IDRs; and the wire mark + /// ([`USER_FLAG_RECOVERY_POINT`](crate::packet::USER_FLAG_RECOVERY_POINT)) is set by exactly one + /// encoder backend — pf-encode's Linux libav-NVENC under its `PUNKTFUNK_INTRA_REFRESH` opt-in — + /// while the other two that run a wave (Windows AMF, QSV) leave it off pending on-glass GDR + /// validation. So a client on one of those sessions freezes on loss and holds until + /// [`REANCHOR_FREEZE_MAX`] expires, then forces the very IDR the wave exists to avoid: half a + /// second of frozen picture followed by a 20-40× frame, on a stream that healed itself long + /// before. A decoder that parses the recovery point SEI can simply watch it happen — and unlike + /// the wire flag, that signal cannot be lost separately from the picture it describes. + /// + /// The rule, and the reason it is a pairing rather than a single flag: a recovery point is + /// trustworthy only when its SEI arrived at or after the loss ([`LocalRecovery`] carries the + /// argument). A mark whose SEI predates the arm is IGNORED — silently and deliberately; the + /// backstop still covers it, exactly as today. + /// + /// It lifts on the FIRST trusted recovery point, unlike the wire mark's two + /// ([`REANCHOR_MARKS_TO_LIFT`]), and the difference is not a relaxation. The wire mark is a + /// phase-fixed WAVE BOUNDARY with no knowledge of when the loss was, so the first boundary after + /// a loss is only partially healed and a second must be waited out. The SEI names the recovery + /// point of a specific wave and is only honoured for a wave that STARTED after the loss, so the + /// picture at that point is fully swept by construction — the same guarantee an + /// [`USER_FLAG_RECOVERY_ANCHOR`](crate::packet::USER_FLAG_RECOVERY_ANCHOR) gives, derived + /// locally instead of trusted from the host. + /// + /// Called on a gate that is not frozen it only records the SEI; there is nothing to lift. + pub fn on_local_recovery(&mut self, obs: LocalRecovery) -> bool { + if obs.sei_here { + self.local_sei_since_arm = true; + } + if !(obs.is_recovery_point && self.local_sei_since_arm && self.awaiting) { + return false; + } + self.awaiting = false; + self.deadline = None; + self.marks = 0; + // Spent: the next heal needs its own SEI. Without this a single wave's recovery point could + // lift a freeze armed by a LATER loss, which is the one thing the pairing exists to prevent. + self.local_sei_since_arm = false; + true + } + /// Fold one decoded frame and decide whether to present or withhold it. /// /// `wire_flags` is the AU's `user_flags` word ([`crate::session::Frame::flags`] / @@ -456,6 +571,167 @@ mod tests { assert!(g.is_holding(), "but never resumes to the concealed picture"); } + // ---- the local (recovery point SEI) path ---- + + /// One decoded frame's local observation, spelled as the two facts it is. + fn local(sei_here: bool, is_recovery_point: bool) -> LocalRecovery { + LocalRecovery { + sei_here, + is_recovery_point, + } + } + + /// The headline: an intra-refresh session heals and the freeze lifts on the SEI's own recovery + /// point — no wire flag, no IDR, and above all no waiting out REANCHOR_FREEZE_MAX. This is the + /// half-second of frozen picture M4 exists to remove. + #[test] + fn a_local_recovery_point_lifts_the_freeze_without_the_backstop() { + let mut g = ReanchorGate::new(0); + let start = t0(); + g.arm(start); // a frame-index gap + assert_eq!(g.on_decoded(0, false, start), GateVerdict::Hold); + + // The wave starts (SEI) and sweeps; the client keeps holding meanwhile. + assert!(!g.on_local_recovery(local(true, false))); + assert_eq!(g.on_decoded(0, false, start), GateVerdict::Hold); + assert!(!g.on_local_recovery(local(false, false))); + assert_eq!(g.on_decoded(0, false, start), GateVerdict::Hold); + + // The recovery point: healed, and the very next frame is presented — while the backstop + // deadline is still far away, which is the whole point. + let mid = start + Duration::from_millis(120); + assert!(g.on_local_recovery(local(false, true)), "the heal lifts it"); + assert!(!g.is_holding()); + assert_eq!(g.on_decoded(0, false, mid), GateVerdict::Present); + assert!( + !g.poll(0, mid), + "and no keyframe is ever asked for — no IDR spike on a stream that healed itself" + ); + } + + /// The pairing rule. A recovery point whose SEI arrived BEFORE the loss guarantees nothing: the + /// stripes that wave already swept still reference the picture that was lost, so lifting there + /// would flash a half-stale frame. It must be ignored and the freeze must hold. + #[test] + fn a_recovery_point_from_a_wave_that_predates_the_loss_is_ignored() { + let mut g = ReanchorGate::new(0); + let now = t0(); + // A wave is in flight when the loss lands. + assert!(!g.on_local_recovery(local(true, false))); + g.arm(now); + // Its recovery point arrives — about a wave that started before the loss. + assert!( + !g.on_local_recovery(local(false, true)), + "a pre-loss wave's recovery point must not lift" + ); + assert!(g.is_holding()); + assert_eq!(g.on_decoded(0, false, now), GateVerdict::Hold); + // The NEXT wave — started after the loss — does lift. + assert!(!g.on_local_recovery(local(true, false))); + assert!(g.on_local_recovery(local(false, true))); + assert_eq!(g.on_decoded(0, false, now), GateVerdict::Present); + } + + /// A single recovery point is spent when it lifts: it must not also lift a freeze armed by a + /// LATER loss, which is exactly what a sticky "an SEI was seen once" flag would do. + #[test] + fn a_spent_recovery_point_cannot_lift_the_next_loss() { + let mut g = ReanchorGate::new(0); + let now = t0(); + g.arm(now); + g.on_local_recovery(local(true, false)); + assert!(g.on_local_recovery(local(false, true))); + // A fresh loss, and a stray recovery point with no new SEI behind it. + g.arm(now); + assert!( + !g.on_local_recovery(local(false, true)), + "the previous wave's credit is gone" + ); + assert!(g.is_holding()); + } + + /// An SEI whose count is zero puts both facts on ONE frame ("start here, this picture is already + /// exact"). It must still lift — the pairing is about ORDER, not about needing two frames. + #[test] + fn an_sei_that_is_its_own_recovery_point_lifts_on_that_frame() { + let mut g = ReanchorGate::new(0); + let now = t0(); + g.arm(now); + assert!(g.on_local_recovery(local(true, true))); + assert_eq!(g.on_decoded(0, false, now), GateVerdict::Present); + } + + /// The whole path is inert for every client that has no local parser — Android MediaCodec, Apple + /// VideoToolbox, and all four FFmpeg rungs (libavcodec exposes no SEI). They never call it, and + /// even if they did with an empty observation nothing may change. + #[test] + fn a_client_without_a_local_parser_sees_no_behaviour_change() { + let mut g = ReanchorGate::new(0); + let now = t0(); + g.arm(now); + for _ in 0..8 { + assert!(!g.on_local_recovery(LocalRecovery::NONE)); + assert_eq!(g.on_decoded(0, false, now), GateVerdict::Hold); + } + assert!( + g.is_holding(), + "still frozen — only the wire can lift this one" + ); + assert_eq!(g.on_decoded(SOF, false, now), GateVerdict::Present); + } + + /// A local recovery point on a gate that is NOT frozen changes nothing: there is no freeze to + /// lift, and the observation must not become a stored credit that pre-lifts the next loss. + #[test] + fn a_recovery_point_on_an_unfrozen_gate_is_not_banked() { + let mut g = ReanchorGate::new(0); + let now = t0(); + assert!(!g.on_local_recovery(local(true, true))); + assert!(!g.is_holding()); + g.arm(now); + assert!( + !g.on_local_recovery(local(false, true)), + "the pre-arm SEI was cleared by the arm" + ); + assert!(g.is_holding()); + } + + /// `arms()` has to move at EVERY arm site — including the two that arm from inside the gate — + /// because a client pairing local recovery by decode order re-stamps its watermark off exactly + /// this counter. An arm site that did not move it would leave that client trusting the + /// recovery marks of pictures decoded before the loss. The overdue backstop is the one thing + /// that must NOT move it: it re-asks without re-arming, and re-stamping there would discard a + /// heal that is legitimately in flight. + #[test] + fn every_arm_site_moves_the_arm_counter_and_the_backstop_does_not() { + let mut g = ReanchorGate::new(0); + let start = t0(); + assert_eq!(g.arms(), 0, "a fresh gate has never armed"); + + g.arm(start); + assert_eq!(g.arms(), 1); + // Re-arming mid-freeze is a second loss — it counts. + g.arm(start); + assert_eq!(g.arms(), 2); + + // The drop-climb arm inside `poll`. + assert!(g.poll(1, start)); + assert_eq!(g.arms(), 3); + + // The overdue backstop re-asks and keeps holding — but does not re-arm. + let later = start + REANCHOR_FREEZE_MAX + Duration::from_millis(1); + assert!(g.poll(1, later)); + assert_eq!(g.arms(), 3, "the backstop re-asks, it does not re-arm"); + + // The no-output streak's arm. + let mut g = ReanchorGate::new(0); + assert!(!g.on_no_output(start)); + assert!(!g.on_no_output(start)); + assert_eq!(g.arms(), 0, "the streak has not tripped yet"); + assert!(g.on_no_output(start)); + assert_eq!(g.arms(), 1); + } + #[test] fn a_live_mark_stream_pushes_the_deadline_out() { // A healing wave (marks arriving) must not be pre-empted by the overdue IDR floor. diff --git a/crates/punktfunk-host/src/gamelease.rs b/crates/punktfunk-host/src/gamelease.rs index 9ee94157..ac48ff98 100644 --- a/crates/punktfunk-host/src/gamelease.rs +++ b/crates/punktfunk-host/src/gamelease.rs @@ -286,7 +286,14 @@ pub struct LeaseRequest { /// Seconds-since-boot from **before** the launch ([`launch_clock`]): the floor for adopting a /// process, which is what keeps a copy of the game the player already had open from being /// mistaken for this session's. `None` disables the filter (no readable uptime clock). + /// + /// A *reconnecting* session inherits this from [`crate::launchreg`] rather than minting its own, + /// which is the only way its lease can see a game the previous session started. pub launch_stamp: Option, + /// Where the watcher publishes the processes it adopts, so the host's launch record can still + /// answer "is our launch up?" after this lease and its watcher are gone + /// ([`crate::launchreg::LiveProcs`]). `None` for a launch that isn't recorded. + pub procs: Option, } /// The reference instant for adopting this launch's processes, in seconds since boot. Call it @@ -317,6 +324,7 @@ pub fn open(req: LeaseRequest, on_exit: OnExit) -> GameLease { launcher, child, launch_stamp, + procs, } = req; // A launcher tile is untracked FIRST, before anything else is considered — see @@ -379,7 +387,7 @@ pub fn open(req: LeaseRequest, on_exit: OnExit) -> GameLease { ); } - let watcher = spawn_watcher(shared.clone(), child, on_exit); + let watcher = spawn_watcher(shared.clone(), child, procs, on_exit); if watcher.is_none() { // Nothing is polling this lease (no signals to poll, or a platform without a matcher yet), so // its state will never advance on its own. Report it as running rather than leaving the @@ -395,6 +403,7 @@ pub fn open(req: LeaseRequest, on_exit: OnExit) -> GameLease { fn spawn_watcher( shared: Arc, child: Option, + procs: Option, on_exit: OnExit, ) -> Option> { // An untracked lease has nothing to observe (it still exposes state for the status surface). @@ -415,23 +424,45 @@ fn spawn_watcher( // surface, but nothing polls it. #[cfg(not(any(target_os = "linux", windows)))] { - let _ = (child, on_exit); + let _ = (child, procs, on_exit); return None; } #[cfg(any(target_os = "linux", windows))] { std::thread::Builder::new() .name("pf1-gamelease".into()) - .spawn(move || watch(shared, child, on_exit)) + .spawn(move || watch(shared, child, procs, on_exit)) .ok() } } /// The watch loop: wait for the game to appear, then for it to go away. #[cfg(any(target_os = "linux", windows))] -fn watch(shared: Arc, mut child: Option, on_exit: OnExit) { +fn watch( + shared: Arc, + mut child: Option, + procs: Option, + on_exit: OnExit, +) { let scanner = crate::procscan::Scanner::system(); let cancelled = || shared.cancel.load(Ordering::Relaxed); + // Publish what this lease adopted to the host's launch record, so a LATER session can tell "this + // host's launch is still up" from "nothing of ours is running" — which is what lets it inherit + // this launch instead of starting a second copy (`crate::launchreg`). + // + // Only ever the CONCRETE processes, and only ever a non-empty set. Never the spec: a later re-scan + // by spec would find a copy the player started for themselves since, and adopting that is exactly + // what procscan's rule 1 forbids. And never cleared on exit: the last set the watcher saw is what + // makes the record answer `Gone` (every recorded pid re-verified dead) rather than "no opinion", + // which is how a game the player quit becomes relaunchable at once. + let publish = |live: &[crate::procscan::ProcRef]| { + if live.is_empty() { + return; + } + if let Some(slot) = procs.as_ref() { + *slot.lock().unwrap_or_else(|e| e.into_inner()) = live.to_vec(); + } + }; let spawned_at = Instant::now(); let mut kind = shared.kind.clone(); @@ -527,6 +558,7 @@ fn watch(shared: Arc, mut child: Option, on_ex let live = scanner.find(&shared.spec, shared.launch_stamp); if !live.is_empty() || child_alive { known = live.clone(); + publish(&live); shared.was_running.store(true, Ordering::Relaxed); shared.last_seen_ms.store(now_ms(), Ordering::Relaxed); shared.set_state(GameState::Running); @@ -582,6 +614,7 @@ fn watch(shared: Arc, mut child: Option, on_ex } }; if !live.is_empty() || child_alive { + publish(&live); known = live; gone_since = None; vetoed = false; @@ -866,8 +899,17 @@ fn windows_term_ladder(shared: &LeaseShared) { // The grace registry: leases whose session is gone but whose game is on probation // --------------------------------------------------------------------------------------------- -/// A lease waiting out its reconnect window. If the client comes back before the deadline the lease -/// is handed to the new session and nothing is ended; if it doesn't, the game ends. +/// A lease waiting out its reconnect window. If the client comes back before the deadline the +/// pending termination is dropped and the game keeps running; if it doesn't, the game ends. +/// +/// The lease object itself is **not** handed to the new session, and cannot be: by the time an entry +/// lands here its [`GameLease`] has already been dropped (the guard's `Drop` runs [`on_session_end`] +/// and then drops the lease), which cancels its watcher — and its `on_exit` action closes a +/// connection that no longer exists. What the new session re-adopts is the *game*, through +/// [`crate::launchreg`], which is what carries the original launch's reference instant across +/// sessions so a fresh lease can see a game started before it. (This doc used to claim the lease was +/// handed over; nothing ever did that, and a reconnecting session was left with no game-exit +/// detection at all.) pub struct Pending { pub shared: Arc, pub deadline: Instant, @@ -902,10 +944,16 @@ pub fn arm_grace(shared: Arc, fingerprint: Option, grace: D } /// A reconnecting client takes its game back: drops any pending termination for `fingerprint` whose -/// title matches `app`. Returns the number of leases reprieved. -pub fn readopt(fingerprint: Option<&str>, app: Option<&str>) -> usize { +/// title matches `app`. +/// +/// Returns the reprieved leases, so a caller can name what it saved (and read the launch it came +/// from) rather than being handed a bare count. They are **corpses by design** — see [`Pending`]: +/// their watchers are cancelled and their exit actions point at a dead connection. The new session +/// opens its own lease; what it needs from the old launch (the reference instant to adopt against) +/// comes from [`crate::launchreg`], not from here. +pub fn readopt(fingerprint: Option<&str>, app: Option<&str>) -> Vec> { let mut reg = registry().lock().unwrap_or_else(|e| e.into_inner()); - let before = reg.len(); + let mut reprieved = Vec::new(); reg.retain(|p| { let same_client = match (&p.fingerprint, fingerprint) { (Some(a), Some(b)) => a == b, @@ -919,12 +967,13 @@ pub fn readopt(fingerprint: Option<&str>, app: Option<&str>) -> usize { title = %p.shared.game.title, "the client reconnected inside the window — the game keeps running" ); + reprieved.push(p.shared.clone()); false } else { true } }); - before - reg.len() + reprieved } /// Every lease currently on probation, with the time left, for the status surface. @@ -991,13 +1040,40 @@ fn start_reaper() { /// What a session should do with its game when it ends. The policy lives in /// [`crate::session_settings`]; this is the one place that turns it into an action, so both planes /// behave identically. -pub fn on_session_end(lease: &GameLease, deliberate: bool, fingerprint: Option<&str>) { +/// +/// `launch` is this session's hold on the host's launch record ([`crate::launchreg`]), consulted for +/// one question only: has a newer session already taken this launch over? +pub fn on_session_end( + lease: &GameLease, + deliberate: bool, + fingerprint: Option<&str>, + launch: Option<&crate::launchreg::Claim>, +) { use crate::session_settings::GameOnSessionEnd; let settings = crate::session_settings::get(); let shared = lease.shared(); if !shared.is_trackable() || shared.state() == GameState::Exited { return; // nothing to end (or it already ended on its own) } + // A newer session has already claimed this launch: the game this lease tracks is the game that + // session is now streaming. Anything this policy would do to "our" game would be done to theirs, + // so it does nothing at all. + // + // **Which order this runs in relative to the new session's handshake does not matter, and that is + // the point.** The two are concurrent — the old session's stream loop exits (here) while the new + // one is already deciding its launch. If the teardown wins the race, `superseded` is false and the + // policy runs exactly as it always has; the new session then finds the record released and adopts + // it through the window/liveness arms. If the handshake wins, the new session's claim is already + // recorded when this runs, and this returns — which is the case that needed fixing: under + // `Always`, the handshake's `readopt` would have run BEFORE this `arm_grace` and so could not + // reprieve it, and the reaper would have ended the new session's game when the window closed. + if launch.is_some_and(|c| c.superseded()) { + tracing::info!( + title = %shared.game.title, + "this client already came back for this game — leaving it to the session that has it now" + ); + return; + } let end_now = |shared: Arc| { // A deliberate stop already forces this session's display down immediately (the `quit` flag // beats keep-alive linger), and for a nested launch that teardown *is* what ends the game — @@ -1053,16 +1129,28 @@ pub struct SessionGuard { quit: Arc, /// Hex client fingerprint, so a reconnecting client can reclaim its own game and nothing else. fingerprint: Option, + /// This session's hold on the host's launch record. Held here because its lifetime is exactly the + /// session's: its drop is what opens the reconnect window a re-dial is matched against, and it + /// must not happen until after the policy above has read it. Rust drops fields **after** the + /// `Drop` body, so declaring it here is what orders those two. + launch: Option, } impl SessionGuard { /// Bind `lease` to the calling session's lifetime. `quit` is the session's deliberate-stop flag, - /// read at drop; `fingerprint` identifies the client allowed to reclaim the game on reconnect. - pub fn new(lease: GameLease, quit: Arc, fingerprint: Option) -> Self { + /// read at drop; `fingerprint` identifies the client allowed to reclaim the game on reconnect; + /// `launch` is this session's claim on the host's launch record ([`crate::launchreg::claim`]). + pub fn new( + lease: GameLease, + quit: Arc, + fingerprint: Option, + launch: Option, + ) -> Self { Self { lease, quit, fingerprint, + launch, } } @@ -1078,6 +1166,7 @@ impl Drop for SessionGuard { &self.lease, self.quit.load(Ordering::SeqCst), self.fingerprint.as_deref(), + self.launch.as_ref(), ); } } @@ -1104,6 +1193,8 @@ mod tests { child: None, // No start-time floor: these leases are never matched against real processes. launch_stamp: None, + // Not a recorded launch — nothing here spawns anything (`crate::launchreg`). + procs: None, } } @@ -1250,14 +1341,16 @@ mod tests { Duration::from_secs(3_600), ); // A different client, or a different title, does not reprieve it. - assert_eq!(readopt(Some("fp-other"), Some(id)), 0); - assert_eq!(readopt(Some("fp-130"), Some("steam:9999")), 0); + assert!(readopt(Some("fp-other"), Some(id)).is_empty()); + assert!(readopt(Some("fp-130"), Some("steam:9999")).is_empty()); // A missing fingerprint on either side must not reprieve anything either — otherwise any // unidentified reconnect could keep any game alive. - assert_eq!(readopt(None, Some(id)), 0); + assert!(readopt(None, Some(id)).is_empty()); assert!(is_pending(id), "none of those should have reprieved it"); - // The right client coming back for the right title does. - assert_eq!(readopt(Some("fp-130"), Some(id)), 1); + // The right client coming back for the right title does — and names what it saved. + let saved = readopt(Some("fp-130"), Some(id)); + assert_eq!(saved.len(), 1); + assert_eq!(saved[0].game.id.as_deref(), Some(id)); assert!(!is_pending(id)); } @@ -1272,7 +1365,7 @@ mod tests { .expect("armed lease is pending"); assert!(mine.1 > 290 && mine.1 <= 300, "remaining was {}", mine.1); // Leave the registry as we found it, so a sibling test's sweep can't see this entry. - assert_eq!(readopt(Some("fp-140"), Some(id)), 1); + assert_eq!(readopt(Some("fp-140"), Some(id)).len(), 1); } #[test] @@ -1298,7 +1391,7 @@ mod tests { assert!(!lb.shared().is_terminating()); // An id nobody is waiting on ends nothing. assert_eq!(end_pending(Some("steam:99999")), 0); - assert_eq!(readopt(Some("fp-151"), Some(b)), 1); + assert_eq!(readopt(Some("fp-151"), Some(b)).len(), 1); } /// A launcher that hands off and exits must never be mistaken for the game. @@ -1335,6 +1428,7 @@ mod tests { launcher: false, child: Some((child, false)), launch_stamp: None, + procs: None, }, Box::new(|| { EXITS.fetch_add(1, Ordering::SeqCst); @@ -1394,6 +1488,7 @@ mod tests { launcher: false, child: Some((child, true)), launch_stamp, + procs: None, }, Box::new(|| { EXITS.fetch_add(1, Ordering::SeqCst); diff --git a/crates/punktfunk-host/src/gamestream/stream.rs b/crates/punktfunk-host/src/gamestream/stream.rs index b777b389..41f5d824 100644 --- a/crates/punktfunk-host/src/gamestream/stream.rs +++ b/crates/punktfunk-host/src/gamestream/stream.rs @@ -221,7 +221,7 @@ fn run( // steps, before the source (a bare-spawn gamescope nests the game inside it), before the // launch — because a reading taken later would reject the very process it is meant to find. // Erring early can only ever include more of our own launch, never a copy from before it. - let launch_stamp = crate::gamelease::launch_clock(); + let fresh_stamp = crate::gamelease::launch_clock(); // Everything the host knows about the title being launched, resolved in ONE library scan: // what to run, what to call it, and how to recognize it once it is up. let target = resolve_gs_app(app); @@ -231,14 +231,29 @@ fn run( if let Some(t) = target.as_ref() { let reprieved = crate::gamelease::readopt(life.fingerprint.as_deref(), t.game.id.as_deref()); - if reprieved > 0 { + if !reprieved.is_empty() { tracing::info!( - reprieved, + reprieved = reprieved.len(), title = %t.game.title, "gamestream: this client came back for its game — keeping it" ); } } + // ...and the other half of coming back for it: the host's own record of what it launched, for + // whom (`crate::launchreg`). Plane parity with the native path — same registry, same rule. + // A relaunch of a title this client's copy of which is still running neither starts a second + // copy nor mints a fresh reference instant the running game could never satisfy. A paired + // Moonlight client has a fingerprint; an anonymous one (or an operator-typed `apps.json` + // entry with no library id) is not recordable and behaves exactly as it always has. + let launch_claim = target.as_ref().map(|t| { + crate::launchreg::claim( + life.fingerprint.as_deref(), + t.game.id.as_deref(), + fresh_stamp, + ) + }); + let launch_stamp = launch_claim.as_ref().map_or(fresh_stamp, |c| c.stamp()); + let adopt_launch = launch_claim.as_ref().is_some_and(|c| !c.must_spawn()); // Per-app prep steps (RFC §6): the entry's own `prep` plus a custom library title's, // run synchronously BEFORE the virtual output opens or anything launches (an HDR // toggle / sink switch must land first — and gamescope's nested launch happens inside @@ -290,17 +305,34 @@ fn run( // store-qualified id — resolved against the host's OWN library (the client can only pick an // existing title, never inject a command). An apps.json entry instead carries an // operator-typed `cmd`. Library id wins when both are set. + // + // ...and once per LAUNCH, not once per `/launch` request: `adopt_launch` is the record's + // verdict that this client's copy of the title is already running (see above), and + // `spawned_now` is what actually happened — the record is settled from it below. + #[allow(unused_mut)] + let mut spawned_now = false; #[cfg(windows)] if let Some(t) = target.as_ref() { - // A library title launches by its store-qualified id (the interactive-session spawner - // resolves the store's own recipe); an operator-typed command runs as itself. - let launched = match (t.game.id.as_deref(), t.command.as_deref()) { - (Some(id), _) => crate::library::launch_gamestream_library(id), - (None, Some(cmd)) => crate::library::launch_gamestream_command(cmd), - (None, None) => Ok(()), - }; - if let Err(e) = launched { - tracing::warn!(title = %t.game.title, error = %e, "gamestream: could not launch app"); + if adopt_launch { + tracing::info!( + title = %t.game.title, + "gamestream: this client's copy of this title is already running — not starting \ + a second one" + ); + } else { + // A library title launches by its store-qualified id (the interactive-session spawner + // resolves the store's own recipe); an operator-typed command runs as itself. + let launched = match (t.game.id.as_deref(), t.command.as_deref()) { + (Some(id), _) => crate::library::launch_gamestream_library(id), + (None, Some(cmd)) => crate::library::launch_gamestream_command(cmd), + (None, None) => Ok(()), + }; + match launched { + Ok(()) => spawned_now = true, + Err(e) => { + tracing::warn!(title = %t.game.title, error = %e, "gamestream: could not launch app") + } + } } } // Linux keeps the spawned child rather than dropping it: it is the primary liveness signal @@ -309,11 +341,28 @@ fn run( // source open), so launching again would start it twice. #[cfg(target_os = "linux")] let spawned_launch = match target.as_ref().and_then(|t| t.command.as_deref()) { + // Already ours and still running: don't hand the player a second copy. The nested arm + // below reaches the same conclusion through the display registry, whose reuse key includes + // the launch command — a kept gamescope with the game inside it is re-attached, not + // respawned. + Some(cmd) if adopt_launch => { + tracing::info!( + command = %cmd, + "gamestream: this client's copy of this title is already running — not starting \ + a second one" + ); + None + } Some(_) if crate::vdisplay::launch_is_nested(compositor, gamescope_route.as_ref()) => { + // gamescope spawned it as its own nested child when the source opened above. + spawned_now = true; None } Some(cmd) => match crate::library::launch_session_command(compositor, cmd) { - Ok(spawned) => Some(spawned), + Ok(spawned) => { + spawned_now = true; + Some(spawned) + } Err(e) => { tracing::warn!(command = %cmd, error = %e, "gamestream: could not launch app"); None @@ -321,6 +370,16 @@ fn run( }, None => None, }; + #[cfg(not(any(target_os = "windows", target_os = "linux")))] + let _ = adopt_launch; + // Settle the record against what actually happened (see the native plane). + if let Some(c) = launch_claim.as_ref() { + if spawned_now { + c.launched(); + } else if c.must_spawn() { + c.abandon(); + } + } // The launched game's lifetime, in both directions (design/session-game-lifetime.md) — the // compat plane's half of what the native plane already does: @@ -372,6 +431,9 @@ fn run( launcher: t.launcher, child, launch_stamp, + // For an adopted launch this is the ORIGINAL launch's slot, so the record keeps + // tracking the same processes across the handover. + procs: launch_claim.as_ref().and_then(|c| c.procs()), }, on_exit, ); @@ -384,6 +446,9 @@ fn run( lease, life.quit.clone(), life.fingerprint.clone(), + // The record's hold moves in here — its drop opens the reconnect window the next + // `/launch` of this title is matched against. + launch_claim, ), ) }); diff --git a/crates/punktfunk-host/src/launchreg.rs b/crates/punktfunk-host/src/launchreg.rs new file mode 100644 index 00000000..4dd5fb49 --- /dev/null +++ b/crates/punktfunk-host/src/launchreg.rs @@ -0,0 +1,652 @@ +//! What this host launched, for whom, and when — the record a *second* session needs in order not to +//! launch the same title twice (design/session-game-lifetime.md). +//! +//! A client that re-dials mid-session re-sends its `Hello::launch` **verbatim**. It cannot drop the +//! field: on Linux the per-session gamescope is re-adopted through pf-vdisplay's display registry, +//! whose reuse key includes the launch command, so a retry without it orphans the running game. The +//! host therefore has to be the one that notices, and two things go wrong when it doesn't: +//! +//! * **the title is launched twice.** `steam://rungameid`, Epic's launcher URI and an AUMID +//! activation all dedupe inside the launcher — it focuses the copy that is already up — but a +//! `gog:` or `custom:` target is a plain spawn and really does start a second copy of the game. +//! * **the reconnected session never notices the game exit.** A fresh session mints a fresh +//! [`crate::gamelease::launch_clock`] stamp, and [`crate::procscan`] refuses to adopt any process +//! that started more than [`crate::procscan::START_SLACK_SECS`] before it. The game was started by +//! the *original* session, minutes earlier, so it can never be adopted — and the reconnected +//! session has no game-exit detection for the rest of its life. +//! +//! ### Why a registry, and not "is this title already running?" +//! +//! Because that second question has a catastrophic answer. [`crate::procscan`]'s first rule is that a +//! process predating the launch is never adopted: a player may already have the game open when a +//! session starts, and treating that instance as "this session's game" would let a session end kill +//! something it never started — on Windows the host runs as SYSTEM and can signal anything. A +//! registry of the host's **own** launches preserves that rule *by construction*: a game the player +//! started for themselves was never recorded here, so it can never be reclaimed from here, and the +//! only reference instant a session can inherit is one this host took immediately before a spawn it +//! performed itself. +//! +//! The same care runs through the liveness probe: it only ever *re-verifies the processes a lease +//! actually adopted* ([`Liveness`]), never re-scans by [`crate::library::DetectSpec`]. A fresh scan +//! would find a copy the player started since, which is exactly the process rule 1 exists to keep out. +//! +//! ### Not the grace registry +//! +//! Deliberately separate from [`crate::gamelease::arm_grace`]. That one is **policy**: it exists only +//! under `GameOnSessionEnd::Always` and a non-deliberate end, so on the shipped default (`Keep`) it is +//! empty — which is precisely the configuration both defects above were reported on. This one is a +//! **record**: written whenever the host launches a title, whatever the operator's termination policy +//! says, and read at the next session's launch decision. Different owners, different lifetimes, and +//! no shared state between them. + +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex, OnceLock}; +use std::time::{Duration, Instant}; + +/// How long after its last session let go a launch is still treated as the *same* launch even though +/// nothing of it has ever been seen running. +/// +/// This window covers exactly one shape: a client that tears its session down and re-dials while the +/// launcher is still bringing the game up. The presenter's HEVC→H.264 codec fallback does that within +/// seconds; a client crash-and-restart within tens of them. Once the game *has* been seen, [`Liveness`] +/// answers the question exactly and this window stops mattering — and a launch whose processes are +/// confirmed gone is re-launchable immediately, whatever the window says. +/// +/// Kept short on purpose. The cost of it being too long is a title the player asked for and did not +/// get, which is a far worse failure than the second copy it exists to prevent. +const IN_FLIGHT_WINDOW: Duration = Duration::from_secs(90); + +/// How long an unheld record survives at all, so the registry can't grow without bound across a long +/// host uptime. Generous: a launch idle this long whose game is somehow *still* running gets started +/// again, which is exactly what the host did before this module existed. +const MAX_RECORD_AGE: Duration = Duration::from_secs(24 * 60 * 60); + +/// The processes a launch's watcher adopted, published as it sees them so the record can still answer +/// "is *our* launch up?" after the session — and therefore the watcher — is gone. +/// +/// Written by [`crate::gamelease`]'s watch loop, read here. Only ever re-verified through +/// [`crate::procscan::Scanner::alive`], which re-checks each process's start time and so cannot be +/// fooled by a recycled pid (rule 2), and never re-scanned by spec (rule 1 — see the module docs). +pub type LiveProcs = Arc>>; + +/// What became of the processes a recorded launch adopted. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Liveness { + /// At least one process this launch adopted is still the same live process. + Running, + /// Every process it adopted is gone. The launch is over. + Gone, + /// No opinion — nothing was ever adopted (the game has not appeared yet, or the title has no + /// detect signals and its only liveness signal was a child handle that died with its session), or + /// this platform has no process matcher at all (macOS, which has no launch path either). + /// + /// The no-signals case is worth naming: a title the host can only track through the child it + /// spawned is [`crate::gamelease::LeaseKind::Child`], and adopting it hands the new session a + /// lease with no child and no signals — [`crate::gamelease::LeaseKind::Untracked`], for which + /// both lifetime behaviors were already inert. So inside [`IN_FLIGHT_WINDOW`] such a reconnect + /// trades game-exit detection for not handing the player a second copy of the game. That is the + /// right way round: the missing detection is an annoyance, a second running copy is not. + Unknown, +} + +/// What a starting session must do about the title it was asked to launch. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Plan { + /// Start it, and adopt its processes against the freshly minted reference instant. + Spawn, + /// This host already launched this title for this client and that launch is still ours: do **not** + /// start a second copy, and adopt against the **original** launch's reference instant so the + /// lease can still see (and therefore notice the exit of) the game that is already running. + Adopt, +} + +/// Who a launch belongs to. Both halves are required — see [`key_for`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Key { + fingerprint: String, + game_id: String, +} + +/// The identity half of the match rule, pure and total: which (client, title) pair may ever reclaim a +/// launch. `None` = this launch is not recordable at all, so it is started exactly as it always was. +/// +/// Both sides must name a client **and** a title: +/// +/// * no fingerprint — an anonymous client (TOFU / `--open`, and the whole GameStream compat plane) — +/// because otherwise any unidentified client could reclaim any other unidentified client's launch, +/// and on Linux the second client would then get its own empty display with the first one's game +/// nowhere on it. This is the same conservatism [`crate::gamelease::readopt`] already applies. +/// * no library id — an operator-typed `apps.json` command, which has no library entry behind it — because +/// every such launch would otherwise share the one `None` id and reclaim each other. +/// +/// Both exclusions are the safe direction: the affected launches simply keep the pre-existing +/// behavior (start it again), rather than reclaiming something that might not be theirs. +pub fn key_for(fingerprint: Option<&str>, game_id: Option<&str>) -> Option { + Some(Key { + fingerprint: fingerprint?.to_string(), + game_id: game_id?.to_string(), + }) +} + +impl Key { + /// The fingerprint prefix the rest of the host logs clients by (`client_label`), so a launch line + /// can be lined up with the session lines around it without printing a full cert hash. + fn short_client(&self) -> &str { + self.fingerprint.get(..12).unwrap_or(&self.fingerprint) + } +} + +/// One launch this host performed, for one client, for one title. +struct Record { + key: Key, + /// The reference instant taken immediately before that launch ([`crate::gamelease::launch_clock`]). + /// This is the value a reconnecting session inherits; `None` only ever means "this platform has no + /// process-start clock", never "we failed to inherit one" — a session that inherits nothing gets + /// [`Plan::Spawn`] and its own fresh stamp instead. + stamp: Option, + /// The processes the launch's lease adopted; see [`LiveProcs`]. + procs: LiveProcs, + /// Set once the launch *actually happened*. A record made by a session that then failed to spawn + /// (or never had a launch path at all) is never matched — nothing is running for it to reclaim. + launched: bool, + /// How many live sessions hold this record. A count, not a flag: an old session's teardown and a + /// new session's launch decision overlap, and the old one releasing must never zero out the new + /// one's hold. + holders: u32, + /// When the last holder let go. `None` while held. + released_at: Option, + /// The newest claim taken on this record. An older session compares its own claim against this to + /// find out that its game now belongs to somebody else ([`Claim::superseded`]). + claim: u64, +} + +impl Record { + fn new(key: Key, stamp: Option, claim: u64) -> Self { + Self { + key, + stamp, + // A fresh slot per launch: the previous launch's dead processes must never be inherited + // by the new one, and its lease may still be writing into the old handle. + procs: Arc::new(Mutex::new(Vec::new())), + launched: false, + holders: 1, + released_at: None, + claim, + } + } +} + +/// **The match rule.** Does `rec` cover a new session's request to launch the title it is keyed on? +/// +/// Pure and total: the caller supplies the liveness verdict, the clock and the window, so the rule is +/// unit-testable without a live session, a process table or real time. Identity is not checked here — +/// it is the record's key, decided once by [`key_for`]. +/// +/// Liveness is authoritative wherever it has an opinion. Only when it has none do the two tie-breakers +/// apply, and they are the two shapes a reconnect actually takes: another session is holding the +/// launch right now (the teardown and the re-dial overlapped), or the client came back promptly while +/// the launcher was still working (the [`IN_FLIGHT_WINDOW`]). +fn covers(rec: &Record, live: Liveness, now: Instant, window: Duration) -> bool { + // A launch that never happened has nothing running to reclaim, and inheriting its reference + // instant would hand the new lease a floor with no game above it. + if !rec.launched { + return false; + } + match live { + // Processes this very launch adopted are still alive. This IS the game — start a second copy + // and the player gets two. + Liveness::Running => true, + // Every process it adopted is dead. Whatever the client is asking for now, it is not this + // launch — so a title that crashed on startup, or that the player quit, launches again at once. + Liveness::Gone => false, + Liveness::Unknown => { + rec.holders > 0 + || rec + .released_at + .is_some_and(|t| now.saturating_duration_since(t) <= window) + } + } +} + +/// Re-verify the processes this launch adopted. Never a fresh scan — see the module docs. +fn liveness(rec: &Record) -> Liveness { + let procs = rec.procs.lock().unwrap_or_else(|e| e.into_inner()); + if procs.is_empty() { + return Liveness::Unknown; + } + match alive_count(&procs) { + Some(0) => Liveness::Gone, + Some(_) => Liveness::Running, + None => Liveness::Unknown, + } +} + +/// How many of `procs` are still the same live processes. `None` on a platform with no matcher +/// (macOS), which is "no opinion" — never "gone". +fn alive_count(procs: &[crate::procscan::ProcRef]) -> Option { + #[cfg(any(target_os = "linux", windows))] + { + Some(crate::procscan::Scanner::system().alive(procs).len()) + } + #[cfg(not(any(target_os = "linux", windows)))] + { + let _ = procs; + None + } +} + +/// Forget records nothing can reclaim: an unheld launch that never happened, and an unheld one idle +/// past [`MAX_RECORD_AGE`]. Deliberately free of any process scan — it runs under the registry lock, +/// on the one path that touches the registry at all (a session deciding its launch). +fn sweep(recs: &mut Vec, now: Instant) { + recs.retain(|r| { + if r.holders > 0 { + return true; + } + let idle = r + .released_at + .map_or(Duration::ZERO, |t| now.saturating_duration_since(t)); + r.launched && idle < MAX_RECORD_AGE + }); +} + +struct Reg { + records: Mutex>, + next_claim: AtomicU64, +} + +fn reg() -> &'static Reg { + static REG: OnceLock = OnceLock::new(); + REG.get_or_init(|| Reg { + records: Mutex::new(Vec::new()), + // 0 is reserved for an unrecorded claim, which must never look current. + next_claim: AtomicU64::new(1), + }) +} + +/// Decide what this session must do about its launch, and claim the answer. +/// +/// `fresh_stamp` is this session's own [`crate::gamelease::launch_clock`] reading, taken before +/// anything spawns; it is used when the answer is [`Plan::Spawn`], and discarded in favour of the +/// recorded one when it is [`Plan::Adopt`]. +/// +/// The returned [`Claim`] is an RAII guard: hold it for the whole session (see +/// [`crate::gamelease::SessionGuard`]), because its drop is what starts the reconnect window. +pub fn claim(fingerprint: Option<&str>, game_id: Option<&str>, fresh_stamp: Option) -> Claim { + let Some(key) = key_for(fingerprint, game_id) else { + // Nothing to key a record on. Launch exactly as this host always has. + return Claim { + key: None, + id: 0, + plan: Plan::Spawn, + stamp: fresh_stamp, + procs: None, + }; + }; + let reg = reg(); + let now = Instant::now(); + let mut recs = reg.records.lock().unwrap_or_else(|e| e.into_inner()); + // Allocated **under the lock**, so claim ids and record writes agree on their order. An id handed + // out before the lock could be stamped onto a record after a higher one had been, and then neither + // session would see itself superseded. + let id = reg.next_claim.fetch_add(1, Ordering::Relaxed); + sweep(&mut recs, now); + + let procs = if let Some(i) = recs.iter().position(|r| r.key == key) { + let live = liveness(&recs[i]); + let rec = &mut recs[i]; + if covers(rec, live, now, IN_FLIGHT_WINDOW) { + rec.holders += 1; + rec.released_at = None; + rec.claim = id; + let (stamp, procs) = (rec.stamp, rec.procs.clone()); + drop(recs); + tracing::info!( + app = %key.game_id, + client = %key.short_client(), + ?live, + "this client's own launch of this title is still this host's — adopting it instead \ + of starting a second copy" + ); + return Claim { + key: Some(key), + id, + plan: Plan::Adopt, + stamp, + procs: Some(procs), + }; + } + // The previous launch of this title by this client is over (or never happened). Re-stamp the + // record for the launch about to happen: a fresh reference instant and a fresh process slot, + // so the dead launch's processes can never be inherited by the new one. + // + // Reset in place rather than replaced, because `holders` must survive: an older session may + // still be holding this record, and its release has to decrement the count it incremented. + rec.stamp = fresh_stamp; + rec.procs = Arc::new(Mutex::new(Vec::new())); + rec.launched = false; + rec.holders += 1; + rec.released_at = None; + rec.claim = id; + rec.procs.clone() + } else { + let rec = Record::new(key.clone(), fresh_stamp, id); + let procs = rec.procs.clone(); + recs.push(rec); + procs + }; + drop(recs); + tracing::debug!( + app = %key.game_id, + client = %key.short_client(), + "recording this host's launch of the title" + ); + Claim { + key: Some(key), + id, + plan: Plan::Spawn, + stamp: fresh_stamp, + procs: Some(procs), + } +} + +/// A session's hold on its launch record. Its drop starts the reconnect window. +pub struct Claim { + /// `None` for an unrecordable launch ([`key_for`]) — every method is then inert. + key: Option, + id: u64, + plan: Plan, + stamp: Option, + procs: Option, +} + +impl Claim { + /// Must this session actually start the title? + pub fn must_spawn(&self) -> bool { + matches!(self.plan, Plan::Spawn) + } + + /// The reference instant this session's lease must adopt against + /// ([`crate::gamelease::LeaseRequest::launch_stamp`]) — freshly taken for a [`Plan::Spawn`], + /// inherited from the original launch for a [`Plan::Adopt`]. + /// + /// `None` here always means what it means everywhere else in [`crate::procscan`]: this platform + /// has no process-start clock, so there is no start-time filter. It never means "inheritance + /// failed" — a session that finds nothing to inherit is given [`Plan::Spawn`] and its own fresh + /// reading instead. + pub fn stamp(&self) -> Option { + self.stamp + } + + /// The slot this launch's lease publishes its adopted processes into + /// ([`crate::gamelease::LeaseRequest::procs`]). For a [`Plan::Adopt`] it is the *original* + /// launch's slot, so the record keeps tracking the same processes across the handover. + pub fn procs(&self) -> Option { + self.procs.clone() + } + + /// The launch happened. Only a confirmed record is ever matched by a later session. + /// + /// Ignored once a newer session has re-stamped the record: what it would be confirming is that + /// session's launch, not this one's, and that session confirms its own. + pub fn launched(&self) { + self.with_record(|r| { + if r.claim == self.id { + r.launched = true; + } + }); + } + + /// The launch did not happen — it failed, or this platform has no launch path. Forget the record + /// entirely, so a retry starts the title rather than inheriting a launch that never occurred. + pub fn abandon(&self) { + let Some(key) = self.key.as_ref() else { + return; + }; + let mut recs = reg().records.lock().unwrap_or_else(|e| e.into_inner()); + recs.retain(|r| &r.key != key || r.claim != self.id); + } + + /// Has a **newer** session claimed this launch? `false` when there is no record at all, so only a + /// positive signal ever changes a caller's behavior. + pub fn superseded(&self) -> bool { + let Some(key) = self.key.as_ref() else { + return false; + }; + let recs = reg().records.lock().unwrap_or_else(|e| e.into_inner()); + recs.iter().any(|r| &r.key == key && r.claim > self.id) + } + + /// Run `f` against this claim's record, if it still exists. Deliberately **not** claim-checked: + /// the release in [`Drop`] must decrement the very count it incremented, even after a newer + /// session re-stamped the record. Callers that need "only if it is still mine" check `claim` + /// themselves ([`Claim::launched`]). + fn with_record(&self, f: impl FnOnce(&mut Record)) { + let Some(key) = self.key.as_ref() else { + return; + }; + let mut recs = reg().records.lock().unwrap_or_else(|e| e.into_inner()); + if let Some(r) = recs.iter_mut().find(|r| &r.key == key) { + f(r); + } + } +} + +impl Drop for Claim { + fn drop(&mut self) { + self.with_record(|r| { + r.holders = r.holders.saturating_sub(1); + if r.holders == 0 { + r.released_at = Some(Instant::now()); + } + }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A record shaped for the pure-rule table below. Never touches the global registry. + fn rec(launched: bool, holders: u32, released_at: Option) -> Record { + Record { + key: Key { + fingerprint: "fp".into(), + game_id: "steam:1".into(), + }, + stamp: Some(1.0), + procs: Arc::new(Mutex::new(Vec::new())), + launched, + holders, + released_at, + claim: 1, + } + } + + /// Identity is the record's key, and a launch that can't be keyed is never reclaimed. + #[test] + fn a_launch_is_keyed_by_both_the_client_and_the_title() { + assert!(key_for(Some("fp"), Some("steam:570")).is_some()); + // An anonymous client, or a title with no library entry, is not recordable — the launch + // behaves exactly as it did before this module existed. + assert!(key_for(None, Some("steam:570")).is_none()); + assert!(key_for(Some("fp"), None).is_none()); + assert!(key_for(None, None).is_none()); + // Different client, or different title, is a different launch. + assert_ne!( + key_for(Some("a"), Some("steam:570")), + key_for(Some("b"), Some("steam:570")) + ); + assert_ne!( + key_for(Some("a"), Some("steam:570")), + key_for(Some("a"), Some("gog:1")) + ); + } + + /// The match rule itself: liveness first, then the two tie-breakers. + #[test] + fn the_match_rule_puts_liveness_ahead_of_the_window() { + let t0 = Instant::now(); + let window = Duration::from_secs(90); + let inside = t0 + Duration::from_secs(30); + let outside = t0 + Duration::from_secs(600); + + // A launch that never happened is never reclaimed, however alive something looks. + let never = rec(false, 1, None); + assert!(!covers(&never, Liveness::Running, inside, window)); + + // Our own processes are still up: reclaim it, no matter how long ago the session let go. + let old = rec(true, 0, Some(t0)); + assert!(covers(&old, Liveness::Running, outside, window)); + + // Confirmed gone beats everything — including a live holder and a fresh release. This is what + // keeps a title that crashed on startup (or that the player quit) launchable at once. + let held = rec(true, 1, None); + assert!(!covers(&held, Liveness::Gone, inside, window)); + assert!(!covers(&old, Liveness::Gone, inside, window)); + + // Nothing seen yet: a live holder is itself the answer (the teardown and the re-dial + // overlapped), and a prompt return is the same launch. + assert!(covers(&held, Liveness::Unknown, inside, window)); + assert!(covers(&old, Liveness::Unknown, inside, window)); + // ...but a return long after the window, with nothing ever seen running, is a new launch. + assert!(!covers(&old, Liveness::Unknown, outside, window)); + } + + /// The sweep drops what nobody can reclaim and keeps what somebody can. + #[test] + fn the_sweep_keeps_only_reclaimable_records() { + let t0 = Instant::now(); + let mut recs = vec![ + rec(false, 0, Some(t0)), // never launched, nobody holding + rec(true, 0, Some(t0)), // launched, recently released + rec(false, 1, None), // never launched but HELD — its session is still deciding + ]; + sweep(&mut recs, t0 + Duration::from_secs(1)); + assert_eq!(recs.len(), 2); + // ...and an ancient one goes too. + let mut recs = vec![rec(true, 0, Some(t0))]; + sweep(&mut recs, t0 + MAX_RECORD_AGE + Duration::from_secs(1)); + assert!(recs.is_empty()); + } + + /// **Defect A.** A client that re-dials and re-sends `Hello::launch` verbatim must not get a + /// second copy of its game. + /// + /// Before this module the host launched unconditionally at + /// `native/stream.rs`'s launch site — i.e. the second decision here was always "spawn". + #[test] + fn a_reconnect_does_not_launch_the_title_a_second_time() { + let (fp, app) = (Some("fp-double"), Some("gog:double")); + let first = claim(fp, app, Some(100.0)); + assert!(first.must_spawn(), "the first session starts the title"); + first.launched(); + drop(first); // the session ends; the reconnect window opens + + let second = claim(fp, app, Some(900.0)); + assert!( + !second.must_spawn(), + "a reconnect inside the window must adopt the running launch, not start a second copy" + ); + second.abandon(); // leave the process-global registry as we found it + } + + /// **Defect B.** The reconnected session must adopt against the ORIGINAL launch's reference + /// instant, or [`crate::procscan`] rejects the game — started minutes before this session — and + /// the session has no game-exit detection for the rest of its life. + #[test] + fn a_reconnect_inherits_the_original_launchs_reference_instant() { + let (fp, app) = (Some("fp-stamp"), Some("steam:stamp")); + let first = claim(fp, app, Some(100.0)); + assert_eq!(first.stamp(), Some(100.0)); + first.launched(); + drop(first); + + // The new session mints its own (much later) reading and passes it in; the record's wins. + let second = claim(fp, app, Some(900.0)); + assert_eq!( + second.stamp(), + Some(100.0), + "the reconnect must adopt against the original launch, not against its own start" + ); + // Spelled out, because this is exactly what the host did before: a fresh reading here is a + // floor minutes above the running game's start time, and `procscan` rejects everything under + // it — the reconnected session then has no game-exit detection for the rest of its life. + assert_ne!(second.stamp(), Some(900.0)); + // Both sessions publish into the SAME slot, so the record keeps tracking the same processes + // across the handover. + assert!(second.procs().is_some()); + second.abandon(); + } + + /// Inheriting nothing must never turn into "adopt anything": wherever a launch has a reference + /// instant, every decision made from it has one too. + #[test] + fn a_decision_never_downgrades_a_reference_instant_to_no_filter() { + let (fp, app) = (Some("fp-filter"), Some("steam:filter")); + let first = claim(fp, app, Some(42.0)); + assert!(first.stamp().is_some()); + first.launched(); + drop(first); + let second = claim(fp, app, Some(99.0)); + assert!( + second.stamp().is_some(), + "a reconnect must never end up with the start-time filter disabled" + ); + second.abandon(); + // A launch that cannot be recorded still carries this session's own fresh reading through. + let anon = claim(None, app, Some(7.0)); + assert!(anon.must_spawn()); + assert_eq!(anon.stamp(), Some(7.0)); + assert!(anon.procs().is_none()); + } + + /// A launch that failed (or a platform with no launch path) leaves nothing behind: the next + /// attempt starts the title and gets its own reference instant. + #[test] + fn an_abandoned_launch_is_never_reclaimed() { + let (fp, app) = (Some("fp-fail"), Some("custom:fail")); + let first = claim(fp, app, Some(100.0)); + assert!(first.must_spawn()); + first.abandon(); // the spawn failed + drop(first); + + let second = claim(fp, app, Some(900.0)); + assert!(second.must_spawn(), "a failed launch must be retried"); + assert_eq!(second.stamp(), Some(900.0)); + second.abandon(); + } + + /// A confirmed launch that was never released is still adopted by an overlapping second session — + /// the order where the new session's handshake beats the old session's teardown. + #[test] + fn an_overlapping_session_adopts_a_still_held_launch() { + let (fp, app) = (Some("fp-overlap"), Some("steam:overlap")); + let old = claim(fp, app, Some(100.0)); + old.launched(); + // The old session has NOT torn down yet. + let new = claim(fp, app, Some(900.0)); + assert!(!new.must_spawn(), "a held launch is still ours"); + assert_eq!(new.stamp(), Some(100.0)); + // ...and the old session can see that its game now belongs to the new one, so its teardown + // policy leaves it alone. + assert!(old.superseded()); + assert!(!new.superseded()); + drop(old); + assert!( + !new.superseded(), + "the older session releasing must not look like a newer claim" + ); + new.abandon(); + } + + /// An unrecordable launch is inert in every direction. + #[test] + fn an_unrecordable_launch_never_supersedes_anything() { + let anon = claim(None, None, None); + assert!(anon.must_spawn()); + assert!(!anon.superseded()); + anon.launched(); // no-op + anon.abandon(); // no-op + } +} diff --git a/crates/punktfunk-host/src/main.rs b/crates/punktfunk-host/src/main.rs index b4d2a418..ed28763d 100644 --- a/crates/punktfunk-host/src/main.rs +++ b/crates/punktfunk-host/src/main.rs @@ -71,6 +71,10 @@ mod install; #[cfg(target_os = "windows")] #[path = "windows/interactive.rs"] mod interactive; +// What this host launched, for whom, and when — so a client that re-dials and re-sends its +// `Hello::launch` verbatim neither gets a second copy of its game nor loses sight of the one it has +// (design/session-game-lifetime.md). +mod launchreg; mod library; mod log_capture; mod mgmt; diff --git a/crates/punktfunk-host/src/native.rs b/crates/punktfunk-host/src/native.rs index b617f654..ac12004f 100644 --- a/crates/punktfunk-host/src/native.rs +++ b/crates/punktfunk-host/src/native.rs @@ -1533,9 +1533,17 @@ async fn serve_session( // A client reconnecting inside its game's reconnect window takes the game back: nothing is ended, // and this session adopts it. Matched on (this client, this title) so it can only ever reclaim its // own game. + // + // Cancelling the pending termination is all this does — the *game* is re-adopted in the data plane + // through `crate::launchreg`, which is what carries the original launch's reference instant across + // sessions (a reprieved lease can't: its watcher is cancelled and its exit action closes a + // connection that is already gone). Both are needed, and neither subsumes the other: this one + // exists only under `GameOnSessionEnd::Always`, the record exists whatever the policy says. if let Some(target) = launch_target.as_ref() { let fp = punktfunk_core::quic::endpoint::peer_fingerprint(&conn).map(hex::encode); - crate::gamelease::readopt(fp.as_deref(), target.game.id.as_deref()); + // The reprieved leases are deliberately dropped: they are corpses (see `readopt`), and this + // plane has nothing to say about them that `readopt` has not already logged per lease. + let _reprieved = crate::gamelease::readopt(fp.as_deref(), target.game.id.as_deref()); } // Per-title prep steps (RFC §6) for a launched CUSTOM library title: run synchronously // before the data plane starts (so before the display opens and the title spawns); the diff --git a/crates/punktfunk-host/src/native/stream.rs b/crates/punktfunk-host/src/native/stream.rs index 58d0a508..524f2765 100644 --- a/crates/punktfunk-host/src/native/stream.rs +++ b/crates/punktfunk-host/src/native/stream.rs @@ -746,6 +746,11 @@ fn send_loop( probe_result_tx: tokio::sync::mpsc::UnboundedSender, stop: Arc, perf: bool, + // Smoothed whole-AU paced-send time (µs) published for the ENCODE loop, which hands it to + // `Encoder::set_send_spread_us`. The split arbiter needs it to price what engaging split + // costs on HEVC (sub-frame readback, and with it the send/encode overlap) — a number the + // encoder cannot observe. Written here because this is the only thread that sees a send. + send_spread_us: Arc, // Streamed AUs go out as slice-granularity blocks ([`USER_FLAG_SLICE_STREAM`]'s contract) // instead of the legacy full-FEC-block shape. slice_wire: bool, @@ -902,6 +907,19 @@ fn send_loop( ); } } + // Smooth before publishing: a single AU's spread swings with content and + // FEC shape, and the arbiter turns this into a latency handicap that + // decides an arm. EWMA (3:1) over completed AUs is enough to stop one + // spike flipping a verdict. + { + let prev = send_spread_us.load(Ordering::Relaxed); + let next = if prev == 0 { + stat.spread_us + } else { + ((prev as u64 * 3 + stat.spread_us as u64) / 4) as u32 + }; + send_spread_us.store(next, Ordering::Relaxed); + } if perf || stats.rec.is_armed() { // `encode_us`/`pace_us`/fps are valid for every frame (always measured), // including the Windows relay + tail-drain frames. The cap/submit/wait splits @@ -1455,7 +1473,26 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option { + tracing::info!( + command = %cmd, + "this client's copy of this title is already running from an earlier session — not \ + starting a second one" + ); + None + } Some(cmd) if crate::vdisplay::launch_is_nested(compositor, gamescope_route.as_ref()) => { tracing::info!(command = %cmd, "launch nested into the per-session gamescope"); + // gamescope spawns it as its own nested child, so the launch DID happen here. + spawned_now = true; None } Some(cmd) => match crate::library::launch_session_command(compositor, cmd) { - Ok(spawned) => Some(spawned), + Ok(spawned) => { + spawned_now = true; + Some(spawned) + } Err(e) => { tracing::warn!(command = %cmd, error = %e, "could not launch requested title into the session"); None @@ -1659,7 +1730,17 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option None, }; #[cfg(not(any(target_os = "windows", target_os = "linux")))] - let _ = &launch; + let _ = (&launch, adopt_launch); + // Settle the record against what actually happened. A spawn that never ran — it failed, or this + // platform has no launch path — must leave nothing behind, or a retry would inherit a launch that + // never occurred and then decline to start the title at all. + if let Some(c) = launch_claim.as_ref() { + if spawned_now { + c.launched(); + } else if c.must_spawn() { + c.abandon(); + } + } // The launched game's lifetime, in both directions (design/session-game-lifetime.md): // @@ -1718,6 +1799,9 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option ceiling { diff --git a/crates/punktfunk-host/src/session_status.rs b/crates/punktfunk-host/src/session_status.rs index 74cb746b..fd34b29a 100644 --- a/crates/punktfunk-host/src/session_status.rs +++ b/crates/punktfunk-host/src/session_status.rs @@ -384,6 +384,7 @@ mod tests { launcher: false, child: None, launch_stamp: None, + procs: None, }, Box::new(|| {}), ); diff --git a/crates/punktfunk-host/src/windows/install.rs b/crates/punktfunk-host/src/windows/install.rs index 76411f11..3c60e1de 100644 --- a/crates/punktfunk-host/src/windows/install.rs +++ b/crates/punktfunk-host/src/windows/install.rs @@ -160,16 +160,15 @@ fn ensure_admin_only_source(dir: &Path) -> Result<()> { rc.ok().context("GetNamedSecurityInfoW(owner + DACL)")?; let privileged = privileged_sids()?; let is_privileged = |sid: PSID| -> bool { - // SAFETY: callers pass SIDs that point into the live security descriptor returned - // above (freed only after this scope); IsValidSid only reads the structure. + // SAFETY: every `sid` handed in points into the descriptor returned above (or at an + // ACE inside it) and is valid for this scope; IsValidSid is itself the probe. if sid.is_invalid() || !unsafe { IsValidSid(sid) }.as_bool() { return false; } - privileged.iter().any(|p| { - // SAFETY: `sid` was just validated by IsValidSid; `p` is a self-contained SID - // byte copy built by `privileged_sids` (length measured by GetLengthSid). - unsafe { EqualSid(sid, PSID(p.as_ptr().cast_mut().cast())) }.is_ok() - }) + privileged + .iter() + // SAFETY: `sid` passed IsValidSid above; `p` is an owned, length-exact SID copy. + .any(|p| unsafe { EqualSid(sid, PSID(p.as_ptr().cast_mut().cast())) }.is_ok()) }; if !is_privileged(owner) { @@ -238,10 +237,9 @@ fn privileged_sids() -> Result>> { // SAFETY: `wide` is NUL-terminated and outlives the call; psid is a live out-param. unsafe { ConvertStringSidToSidW(PCWSTR(wide.as_ptr()), &mut psid) } .with_context(|| format!("ConvertStringSidToSidW({s})"))?; - // SAFETY: psid is a valid SID (the conversion above succeeded). + // SAFETY: psid is a valid SID; copy it out so the caller owns plain bytes. let len = unsafe { GetLengthSid(psid) } as usize; - // SAFETY: a SID is `len` contiguous bytes at psid — GetLengthSid just measured it — and - // the copy detaches the bytes before the LocalFree below. + // SAFETY: GetLengthSid just measured exactly `len` readable bytes at `psid`. let bytes = unsafe { std::slice::from_raw_parts(psid.0 as *const u8, len) }.to_vec(); // SAFETY: ConvertStringSidToSidW allocates with LocalAlloc. unsafe { diff --git a/crates/pyrowave-sys/Cargo.toml b/crates/pyrowave-sys/Cargo.toml index ac4e9f8e..e54e0410 100644 --- a/crates/pyrowave-sys/Cargo.toml +++ b/crates/pyrowave-sys/Cargo.toml @@ -13,7 +13,8 @@ links = "pyrowave" # Same CMake-from-vendored-source model as opus/audiopus_sys: reproducible # offline builds (CI, MSVC, flatpak — the flatpak builder has no network). cmake = "0.1" -# Same bindgen configuration as pf-ffvk (runtime = dlopen libclang). +# `runtime` (rather than the default static link) makes bindgen dlopen libclang at build time, so +# any box with a libclang on the loader path can build this without a link-time dependency on it. bindgen = { version = "0.72", features = ["runtime"], default-features = false } [lints] diff --git a/docs-site/content/docs/clients.md b/docs-site/content/docs/clients.md index 9c4c30f4..7bc527a3 100644 --- a/docs-site/content/docs/clients.md +++ b/docs-site/content/docs/clients.md @@ -49,11 +49,12 @@ protocol's FEC/encryption extensions, but for a healthy LAN that rarely matters. `punktfunk-client` is the native graphical Linux client — a GTK4 / libadwaita app that speaks `punktfunk/1` directly, with vendor-ordered hardware decode (**Vulkan Video first on NVIDIA and AMD**, **VAAPI dmabuf first on Intel**; whichever isn't first is the fallback, and software decode -is last), PipeWire audio, and SDL3 controllers (rumble, lightbar, DualSense touchpad/motion). To -force one, pick it in *Preferences → Display → Video decoder* or set -`PUNKTFUNK_DECODER=vulkan|vaapi|software`. Like the Apple app it discovers hosts on your network -automatically, does PIN pairing, pins reconnects, and browses the host's **game library** (with -cover art) so you can launch a title straight into the stream. +is last), PipeWire audio, and SDL3 controllers (rumble, lightbar, DualSense touchpad/motion). The +decoders are Punktfunk's own — the client links no FFmpeg at all, and talks to your GPU's Vulkan +and VAAPI drivers directly. To force one, pick it in *Preferences → Display → Video decoder* or +set `PUNKTFUNK_DECODER=native-vulkan|native-vaapi|software`. Like the Apple app it discovers hosts +on your network automatically, does PIN pairing, pins reconnects, and browses the host's +**game library** (with cover art) so you can launch a title straight into the stream. It ships as a real package, not just a source build — full steps in [Install a Client](/docs/install-client#linux-desktop-flatpak): diff --git a/docs-site/content/docs/configuration.md b/docs-site/content/docs/configuration.md index 429d05ff..a85cd295 100644 --- a/docs-site/content/docs/configuration.md +++ b/docs-site/content/docs/configuration.md @@ -237,7 +237,7 @@ A few knobs are read by the native **clients**, not the host: | Setting | Values | Meaning | |---|---|---| -| `PUNKTFUNK_DECODER` | `software` · `vaapi` · `vulkan` (Linux) · `d3d11va` (Windows) | Force the decode path. Default auto-selects hardware per GPU vendor and falls back on its own: **Linux** — Vulkan Video first on NVIDIA and AMD, VAAPI first on Intel and anything else; **Windows** — Vulkan Video first on NVIDIA and AMD, D3D11VA first on Intel and anything else. Whichever isn't first is the next thing tried, with software last. | +| `PUNKTFUNK_DECODER` | `native-vulkan` · `native-vaapi` (Linux) · `native-d3d11va` (Windows) · `software` | Force the decode path. Default auto-selects hardware per GPU vendor and falls back on its own: **Linux** — Vulkan Video first on NVIDIA and AMD, VAAPI first on Intel and anything else; **Windows** — Vulkan Video first on NVIDIA and AMD, D3D11VA first on Intel and anything else. Whichever isn't first is the next thing tried, with software last (OpenH264 for H.264, rav1d for AV1 — there is no software HEVC, so a client that lands there reconnects on a codec it can decode). The names are the ones the [stats overlay](/docs/stats) prints, so a pin and a reading match. The older spellings `vulkan`, `vaapi` and `d3d11va` named the FFmpeg-backed decoders the clients used before and still work — each migrates onto the native path for the same hardware, and the client says so in its log. | | `PUNKTFUNK_PREFER_PYROWAVE` | `1` | Ask for the [PyroWave](/docs/pyrowave) wavelet codec on a wired link, where the client's own setting isn't reachable (the gamepad console, a headless launch). | | `PUNKTFUNK_OSD_SCALE` | multiplier, e.g. `1.5` *(default `1`)* | Size of the in-stream overlay — the stats OSD, the capture hint and the start banner. They already follow your display's scaling setting (200 % display → twice the pixels), so set this only to nudge that: bigger for a TV across the room, smaller if your compositor reports an aggressive scale. Clamped to 0.5×–4×, and a line that would run off the screen is shrunk to fit. | | `PUNKTFUNK_NO_AEC` | `1` | Turn the microphone's echo cancellation off for this run, whatever **Echo cancellation** says in [client settings](/docs/client-settings#audio). One-way: it can only switch the processing off, never back on, and the setting is the normal way to control it. Linux and Windows clients. | diff --git a/docs-site/content/docs/hdr.md b/docs-site/content/docs/hdr.md index cdd6de92..bdfb47bf 100644 --- a/docs-site/content/docs/hdr.md +++ b/docs-site/content/docs/hdr.md @@ -39,10 +39,12 @@ told HDR, so that is the one place a Punktfunk label can outrun the picture. The Two details worth knowing: -- **HDR usually beats 4:4:4.** For HEVC and AV1 there is no 10-bit full-chroma capture source, so an - HDR session drops to 4:2:0 and says so. If you want [full chroma](/docs/client-settings) with - those codecs, turn HDR off for that profile. [PyroWave](/docs/pyrowave) is the exception: its - Windows capture path writes full-resolution 10-bit chroma, so it can carry HDR and 4:4:4 together. +- **HDR and 4:4:4 compose on Windows, not on Linux.** A **Windows** host carries both: the capture + path writes full-resolution 10-bit chroma and NVENC encodes HEVC Main 4:4:4 10, so + [full chroma](/docs/client-settings) costs you nothing on an HDR desktop. + [PyroWave](/docs/pyrowave) does the same there, in 16-bit planes. On **Linux** the 4:4:4 route is + 8-bit, so a session that negotiates both resolves back down to SDR — full chroma wins. AV1 never + carries 4:4:4 anywhere: Range Extensions are HEVC-only. - **Vulkan games need the bundled layer.** NVIDIA and AMD Vulkan drivers refuse to advertise any HDR colour space for a surface on an indirect (virtual) display, so Vulkan games decide the device "does not support HDR" — even though the driver happily presents an HDR swapchain there. The host @@ -139,8 +141,9 @@ swapchain without a tone-map, which looks washed out. Turn the client's HDR sett is SDR. Use HEVC or AV1 for HDR from Linux. One more rule if you also use full chroma: a **Linux** host encodes 4:4:4 at 8 bits, so a session -that negotiates both resolves back down to SDR before the stream starts. On Linux 4:4:4 wins; on -Windows HDR does. Full chroma is off until you turn it on, so this only bites if you did. +that negotiates both resolves back down to SDR before the stream starts — on Linux, 4:4:4 wins. A +**Windows** host has no such trade: it carries HDR and full chroma at once. Full chroma is off until +you turn it on, so this only bites if you did. ## Check it diff --git a/docs-site/content/docs/roadmap.md b/docs-site/content/docs/roadmap.md index 4e5a264d..fca193ac 100644 --- a/docs-site/content/docs/roadmap.md +++ b/docs-site/content/docs/roadmap.md @@ -92,7 +92,11 @@ head-tracked remote spatial audio that no streaming stack does today. [matrix](/docs/support-matrix#input-cursor-and-hdr). - **Hosting on macOS, iOS, tvOS or Android.** Client-only platforms by construction: every host entry point fails at compile time. There is no setting that changes this. -- **4:4:4 on AMD and Intel encoders.** A limitation of those encode blocks, not a gap in Punktfunk. +- **HEVC 4:4:4 on the AMD encode block.** AMD's VCN never encodes 4:4:4, so there is nothing to + implement. Intel is a different story and *is* a gap rather than a wall — the VAAPI backend + simply has no 4:4:4 path yet, and it waits on hardware that advertises a HEVC 4:4:4 encode + entrypoint to build and validate against. On either vendor, [PyroWave](/docs/pyrowave) already + carries full chroma today. - **DualSense voice-coil haptics.** Scoped and shelved — it rides the controller's USB audio interface and has near-zero game support on Linux. Rumble, adaptive triggers and the lightbar already work. diff --git a/docs-site/content/docs/stats.md b/docs-site/content/docs/stats.md index 147acc50..dce3cccb 100644 --- a/docs-site/content/docs/stats.md +++ b/docs-site/content/docs/stats.md @@ -68,7 +68,7 @@ Every client reports the same measurements, but each family lays them out a litt differently. Linux · Windows · Steam Deck: ``` -1920×1080@120 · 120 fps · 24.3 Mb/s · target 30 Mb/s (auto) · vulkan · HDR +1920×1080@120 · 120 fps · 24.3 Mb/s · target 30 Mb/s (auto) · native-vulkan · HDR e2e 14.2/19.8 ms (p50/p95) · host 3.1 · net 6.7 · decode 2.1 · display 2.3 ms (pace 0.6 + latch 1.7) host: queue 0.6 · encode 1.8 · xfer 0.2 · pace 0.5 ms present: mailbox @@ -110,9 +110,14 @@ lost 3 (2.4%) report one. Then the decode path, an [HDR](/docs/hdr) tag (`HDR`, or `HDR→SDR` when a PQ stream is tone-mapped onto an SDR screen), and — when you asked for [full chroma](/docs/client-settings) — the resolved chroma: `4:4:4` when the host - granted it, `4:4:4→4:2:0` when it couldn't. Android puts its decoder and the negotiated - codec, bit depth, colour and chroma on rows of their own underneath; the Apple clients - don't report a codec at all. + granted it, `4:4:4→4:2:0` when it couldn't. The decode path is exactly one of + `native-vulkan`, `native-d3d11va` (Windows), `native-vaapi` (Linux) and `software`, or + `pyrowave` on a [PyroWave](/docs/pyrowave) session — the same names + [`PUNKTFUNK_DECODER`](/docs/configuration#client-side-native-clients) takes and the same + ones the client's machine-readable `stats:` line carries, so what you pin is what you + read back, and a script that parses the line stays honest. Android puts its decoder and + the negotiated codec, bit depth, colour and chroma on rows of their own underneath; the + Apple clients don't report a codec at all. If the session resolved to a [settings profile](/docs/profiles-and-links), its name closes this line. On **Android** a `⚠ panel NN Hz` warning joins it whenever the device's panel is refreshing *below* the stream's rate — the tell for a phone or TV governor that ignored the requested mode, diff --git a/docs-site/content/docs/support-matrix.md b/docs-site/content/docs/support-matrix.md index 124c52a2..7a2f16bd 100644 --- a/docs-site/content/docs/support-matrix.md +++ b/docs-site/content/docs/support-matrix.md @@ -37,7 +37,10 @@ into a "no" on your machine: answer. The backend supports it; **your device may still refuse it**. Most codec, 10-bit and 4:4:4 cells are this kind. - **Negotiated** — both ends must advertise it before it happens. The clipboard, pen input, 4:4:4 - and client-drawn cursors all die quietly if either side says no. + and client-drawn cursors all die if either side says no. Mostly quietly — 4:4:4 is the exception, + and deliberately so: the host resolves the chroma *before* the Welcome and names the losing gate + in its log, and the client's stats overlay prints `4:4:4→4:2:0` rather than letting you assume + you got what you asked for. - **Default on / opt-in / operator-gated** — HDR and 10-bit are attempted by default; the game library is off by default on the desktop clients; the shared clipboard is off on the host until an operator turns it on. @@ -199,14 +202,21 @@ newer on AMD, Arc and newer on Intel). 1. H.264, HEVC and AV1, intersected with what the driver reports. If the probe cannot run (no driver, or a build without NVENC), the host advertises the full set rather than nothing — so an advertised codec is not always a *confirmed* codec. -2. HEVC only, and only when the GPU's 4:4:4 capability bit says yes. 4:4:4 **and** HDR together is - refused. +2. HEVC only, and only when the GPU's 4:4:4 capability bit says yes. **HDR and 4:4:4 together + depend on the platform.** On Windows they compose: the IDD-push capturer converts the FP16 + desktop to packed 10-bit BT.2020 PQ RGB and NVENC encodes HEVC Main 4:4:4 10, so a session gets + both. On Linux 4:4:4 rides an 8-bit `YUV444P` route, so a session that negotiates both resolves + the bit depth back to 8 — full chroma wins and the stream is SDR. Either way the answer is + settled before the Welcome, so the client is never told one thing and sent another. 3. A hardware limitation of AMD's encode block, not a gap in Punktfunk. VCN never encodes 4:4:4, so there is nothing to probe. 4. Only in a build that includes the native QSV backend — which the shipped installer does. In a hand build without it, 10-bit is honestly reported as unavailable rather than guessed. 5. [PyroWave](/docs/pyrowave) is a wavelet codec, not H.26x. It is never picked automatically: your - client has to ask for it by name in its codec setting. + client has to ask for it by name in its codec setting. Its 4:4:4 is the one that needs no GPU + encode probe — it does its own full-chroma colour conversion, so it resolves on any vendor — + with a single ceiling: the vendored rate controller packs its block index into 16 bits, which an + ≈8K-class 4:4:4 mode overflows, so those modes are downgraded to 4:2:0 before the Welcome. 6. Requires the direct-SDK NVENC path (which every shipped Linux package builds). Without it the frame takes a slower CPU route to reach 10-bit. 7. H.264 never uses this backend, and it exists only in a build carrying the Vulkan-encode feature @@ -224,10 +234,11 @@ newer on AMD, Arc and newer on Intel). `PUNKTFUNK_ENCODER=software` deliberately if that is what you want. On Windows, by contrast, an unrecognised adapter does resolve to software on its own. -**4:4:4 across the whole project:** only HEVC and PyroWave can carry it, only NVENC and PyroWave can -produce it, and only the Apple client asks for it. The Linux and Windows desktop clients have a -4:4:4 setting that currently does nothing — see [Client settings](/docs/client-settings). GameStream -sessions are always 4:2:0. +**4:4:4 across the whole project:** only HEVC and PyroWave can carry it, and only NVENC and +PyroWave can produce it — so on the HEVC side full chroma means an NVIDIA host. Asking for it is a +client setting, off by default, and the **Linux, Windows and Apple** clients all have a working one; +**Android does not implement 4:4:4 at all**, and GameStream sessions are always 4:2:0. See +[Client settings](/docs/client-settings). ### How the host picks a backend @@ -286,11 +297,16 @@ This is a **GPU and encoder** question, not a compositor one, which is why it is ## Client decode +**No punktfunk client contains FFmpeg.** Every decoder below is the platform's own — Vulkan Video, +DXVA, VAAPI, VideoToolbox, MediaCodec — driven directly from punktfunk's own bitstream parser, with +openh264 + rav1d as the CPU floor on the desktop. There is no libav* in any client package, and +nothing to install. + | Client | Decode path (in order) | Codecs | 10-bit / HDR | 4:4:4 | |---|---|---|---|---| -| Linux desktop | Vulkan Video → VAAPI → software ¹ | probed ² | ✅ ³ | ❌ ⁴ | -| Windows desktop | Vulkan Video → D3D11VA → software ¹ | probed ² | ✅ ³ | ❌ ⁴ | -| Steam Deck (via Decky) | as Linux desktop ⁵ | probed ² | ✅ | ❌ | +| Linux desktop | Vulkan Video → VAAPI → software ¹ | H.264, HEVC, AV1 ² | ✅ ³ | ⚠️ ⁴ | +| Windows desktop | Vulkan Video → D3D11VA → software ¹ | H.264, HEVC, AV1 ² | ✅ ³ | ⚠️ ⁴ | +| Steam Deck (via Decky) | as Linux desktop ⁵ | H.264, HEVC, AV1 ² | ✅ | ⚠️ ⁴ | | macOS · iOS · tvOS | VideoToolbox only | H.264, HEVC, AV1 ⁶ | ⚠️ ⁷ | ⚠️ ⁸ | | Android · Android TV | MediaCodec only ⁹ | H.264, HEVC, AV1 ¹⁰ | ⚠️ ⁷ | ❌ | | Moonlight | your Moonlight app's | negotiated | ⚠️ ¹¹ | ❌ | @@ -298,18 +314,39 @@ This is a **GPU and encoder** question, not a compositor one, which is why it is 1. **The order depends on your GPU vendor.** NVIDIA and AMD get Vulkan Video first; Intel and unknown vendors get the platform decoder first (VAAPI on Linux, D3D11VA on Windows), because - FFmpeg's Vulkan path is field-broken on Intel Arc even though the driver advertises it. Pick one - explicitly in Preferences or with `PUNKTFUNK_DECODER` — an explicit choice that fails is a hard - error, never a silent fallback. Mid-session demotion is laddered, and each rung needs both three + Vulkan decode on Intel Arc was field-broken even though the driver advertises it. Pick one + explicitly in Preferences or with `PUNKTFUNK_DECODER` (`native-vulkan`, `native-vaapi`, + `native-d3d11va`, `software`). A pin skips the vendor order — that is what it is for — + but a pinned rung that cannot open still falls through rather than ending the session, + and says so in the log. Settings saved before M10 (`vulkan`, `vaapi`, `d3d11va`) name + the same hardware and are migrated onto the native rung automatically. + Mid-session demotion is laddered, and each rung needs both three consecutive decode errors *and* a full second of them: Vulkan Video first demotes to VAAPI on Linux or D3D11VA on Windows, and only that backend demotes to software — so a startup burst no - longer strands you. -2. Enumerated from FFmpeg at startup, plus PyroWave when the GPU passes its compute probe. + longer strands you. The session log names the rung it landed on, and warns when that rung/codec + pair has no hardware run recorded behind it. +2. A statement about the decoders punktfunk BUILT, not about a codec registry: the hardware rungs + cover all three, the CPU floor covers H.264 (openh264) and AV1 (rav1d) and has no HEVC at all. + AV1 is advertised only where the GPU can really decode it — a CPU AV1 rung exists but a 4K AV1 + stream is not survivable on it, and the codec is fixed for the whole session once negotiated. + HEVC is the one codec advertised without a CPU floor underneath: if every hardware rung for it + fails, the session reconnects on a codec that has one rather than dying. PyroWave is added when + the GPU passes its compute probe and you pick it. 3. On by default. It is presented on a real HDR10 surface where your desktop offers one (KDE HDR, gamescope), and tone-mapped in-shader otherwise. Software-decoded frames never take the HDR - surface. -4. The 4:4:4 setting is stored and shown but is never advertised to the host, so these clients - always receive 4:2:0. + surface — the CPU rung is 8-bit by contract and refuses a 10-bit stream rather than mis-scaling + it. +4. Opt-in (Settings ▸ **Full chroma**), off by default, and advertised whenever you ask — unlike + Apple, **no client-side probe gates it**. Full chroma is a *hardware* path here: the Vulkan + presenter samples the 2-plane 4:4:4 pool formats where the driver decodes HEVC Range Extensions + in hardware, which today means NVIDIA. There is no software safety net under it — the CPU floor + is 4:2:0 8-bit by contract and has no HEVC at all (note 2), so it refuses a 4:4:4 stream rather + than converting it, and a box whose hardware 4:4:4 decode fails lands on note 2's codec + reconnect instead of a downgraded picture. None of that is silent: the Detailed + [stats overlay](/docs/stats) prints the resolved chroma (`4:4:4→4:2:0` when the host declined) + and the rung frames really took. Whether you get it at all is then the host's half — HEVC 4:4:4 + means an NVIDIA host, or pick PyroWave, which decodes on its own GPU compute path and so carries + full chroma on any vendor. 5. The Decky plugin does not decode anything — it launches the Linux client, so the decode path is identical, including the Mesa `RADV_PERFTEST=video_decode` opt-in the session binary sets before any Vulkan call (without it RADV exposes no decode queue and the Deck silently falls back to @@ -320,8 +357,11 @@ This is a **GPU and encoder** question, not a compositor one, which is why it is 7. Runtime-probed against the actual display: EDR headroom on iPhone/iPad, HDR eligibility on Mac and Apple TV, HDR capabilities on Android. On an SDR panel the client advertises no HDR at all so the host sends a correct 8-bit picture instead of PQ your screen would mangle. -8. The only client that asks for 4:4:4 — opt-in, and gated on a real hardware-decode probe (both - 8-bit and 10-bit when HDR is also on). In practice it only ever resolves against an NVIDIA host. +8. Opt-in, and the only client that **probes before asking**: it advertises 4:4:4 only where + VideoToolbox really hardware-decodes it (both 8-bit and 10-bit when HDR is also on), because + VideoToolbox's software 4:4:4 decode is far too slow for a real-time stream — validated on M3. + The desktop clients need no such probe (note 4). For HEVC it only ever resolves against an + NVIDIA host. 9. Chosen by name from a ranked device list that prefers hardware, real SoC vendors and low-latency decoders, and blocks the known-bad software ones. There is no software rung. 10. H.264 and HEVC are assumed universal on Android hardware; AV1 is probed. @@ -445,8 +485,8 @@ text from an IME), are covered in [Input](/docs/input). | Client | HDR | 4:4:4 | Surround 5.1 / 7.1 | Microphone | Clipboard | Stats overlay | |---|---|---|---|---|---|---| -| Linux desktop | ✅ ¹ | ❌ ¹³ | ✅ ² | ✅ | ❌ ³ | ✅ | -| Windows desktop | ✅ ¹ | ❌ ¹³ | ✅ ² | ✅ | ⚠️ ⁴ | ✅ | +| Linux desktop | ✅ ¹ | ⚠️ ¹³ | ✅ ² | ✅ | ❌ ³ | ✅ | +| Windows desktop | ✅ ¹ | ⚠️ ¹³ | ✅ ² | ✅ | ⚠️ ⁴ | ✅ | | macOS | ⚠️ ⁵ | ⚠️ ⁶ | ✅ ² | ✅ | ✅ ⁷ | ✅ | | iPhone · iPad | ⚠️ ⁵ | ⚠️ ⁶ | ✅ ² | ✅ | ❌ ⁸ | ✅ | | Apple TV | ⚠️ ⁵ | ⚠️ ⁶ | ✅ ² | ❌ ⁹ | ❌ ⁸ | ✅ | @@ -475,9 +515,12 @@ text from an IME), are covered in [Input](/docs/input). until the host also enables its clipboard, but the client-side consent is pre-granted. 11. Decided entirely by the host and layered into what Moonlight is offered. 12. Moonlight has its own overlay; [stats](/docs/stats) here describes Punktfunk's. -13. The desktop settings still show a **Full chroma (4:4:4)** switch, and it is a per-profile field - — but the session binary never advertises the 4:4:4 capability, so the switch has no effect - today and the stream stays 4:2:0. +13. Opt-in — the per-profile **Full chroma (4:4:4)** switch, off by default — and advertised with + no client-side probe. Both halves earn the ⚠️. On the **client** it is a hardware path with no + software floor under it (note 4 under [Client decode](#client-decode)). On the **host** it + needs HEVC on an **NVIDIA** host, or the PyroWave codec, which carries it on any vendor. On + Windows it composes with HDR; on a Linux host 4:4:4 is 8-bit, so asking for both gives you full + chroma in SDR. The stats overlay tells you which you got. **File transfer through the clipboard does not exist yet** on any client. The wire format and the host-side policy for it are in place, but no client offers files, so a copied file never crosses. @@ -531,7 +574,7 @@ own so that adding a client-side feature never locks a client out of a deployed | Contract | Current | What it governs | |---|---|---| | `punktfunk/1` wire version | **2** | The `Hello`/`Welcome` handshake and the session planes. Hosts equality-check it, so this is the one that must match. | -| C ABI version | **13** | The embeddable C surface a client links against. It grows far more often than the wire does. | +| C ABI version | **17** | The embeddable C surface a client links against. It grows far more often than the wire does. | | Virtual-display driver protocol (Windows) | **6** (accepts **3** and up) | Between the Windows host and its display driver, so an older driver keeps working after a host update. | | Windows virtual-gamepad channel | **3** | Between the host and its pad driver. | diff --git a/flake.nix b/flake.nix index 22d75a10..c6862b3c 100644 --- a/flake.nix +++ b/flake.nix @@ -176,10 +176,7 @@ pkgs.librsvg pkgs.gsettings-desktop-schemas pkgs.adwaita-icon-theme - pkgs.vulkan-headers ]; - # pf-ffvk bindgen (no /usr/include on NixOS); GPU driver libs at runtime for `cargo run`. - PF_FFVK_VULKAN_INCLUDE = "${pkgs.vulkan-headers}/include"; # CMake ≥ 4 rejects the pre-3.5 minimums some vendored C libs (libopus) still declare. CMAKE_POLICY_VERSION_MINIMUM = "3.5"; LD_LIBRARY_PATH = "/run/opengl-driver/lib:${ diff --git a/include/punktfunk_core.h b/include/punktfunk_core.h index aa19227d..1e6d31c0 100644 --- a/include/punktfunk_core.h +++ b/include/punktfunk_core.h @@ -660,15 +660,16 @@ // [`Hello::video_caps`] bit: the client's decoder accepts **multi-slice access units** — H.264/ // HEVC frames carrying several slice NALs (latency plan §7 LN1: the encoder splits frames so // sub-frame readback can ship early slices while the tail encodes). Decoder-level, so the -// EMBEDDER sets it from what its decode stack actually handles: the desktop clients' FFmpeg/ -// D3D11VA/Vulkan-video decoders are fine, but mobile/TV MediaCodec is per-SoC — Amlogic HEVC -// decoders (Chromecast with Google TV, Fire TV) wedge the whole DEVICE on multi-slice frames -// (the 0.17.0 field regression: the 4-slice Linux default froze streams on first frame and -// watchdog-rebooted the CCwGTV), which is exactly why Moonlight requests 1 slice per frame for -// every hardware decoder. The host defaults to >1 slice ONLY toward a client that sets this -// bit (`PUNKTFUNK_NVENC_SLICES` stays the explicit operator override in both directions); -// every other client gets single-slice frames — the pre-0.17 wire shape. NOTE: this takes the -// video_caps byte's last free bit — the next video cap needs a second byte (ABI bump). +// EMBEDDER sets it from what its decode stack actually handles: every desktop decode stack +// (Vulkan Video, D3D11VA, VAAPI, openh264/rav1d) is fine, but mobile/TV MediaCodec is per-SoC +// — Amlogic HEVC decoders (Chromecast with Google TV, Fire TV) wedge the whole DEVICE on +// multi-slice frames (the 0.17.0 field regression: the 4-slice Linux default froze streams on +// first frame and watchdog-rebooted the CCwGTV), which is exactly why Moonlight requests 1 +// slice per frame for every hardware decoder. The host defaults to >1 slice ONLY toward a +// client that sets this bit (`PUNKTFUNK_NVENC_SLICES` stays the explicit operator override in +// both directions); every other client gets single-slice frames — the pre-0.17 wire shape. +// NOTE: this takes the video_caps byte's last free bit — the next video cap needs a second +// byte (ABI bump). #define PUNKTFUNK_VIDEO_CAP_MULTI_SLICE 128 #endif @@ -1699,6 +1700,22 @@ typedef struct ColorInfo ColorInfo; // else happened to be using the audio graph that day. typedef struct JitterTuning JitterTuning; +// What a client's OWN bitstream parser saw about intra-refresh recovery on one decoded frame — the +// in-band counterpart of the wire's [`USER_FLAG_RECOVERY_POINT`](crate::packet::USER_FLAG_RECOVERY_POINT). +// +// Two facts rather than one verdict, because the gate needs both and only the gate knows how to +// combine them. A recovery point SEI promises: *a decoder that starts at THIS AU has a correct +// picture N frames later*. That promise covers a decoder which lost references BEFORE the SEI (the +// wave re-codes every stripe after it, so the stale content is fully overwritten) and says nothing +// at all about one which lost references AFTER it (the already-swept stripes still reference the +// lost picture). So a recovery point may only lift a freeze when its SEI was observed at or after +// the loss — which is the pairing [`ReanchorGate::on_local_recovery`] performs, since the gate is +// the only party that knows when the loss was. +// +// Produced by pf-vkdecode's `RecoveryWatch` on the native decode lane. Every other lane leaves it +// [`Default`] and nothing changes. +typedef struct LocalRecovery LocalRecovery; + #if defined(PUNKTFUNK_FEATURE_QUIC) // Opaque handle to a live `punktfunk/1` connection (QUIC control plane + UDP data plane, all // pumped on internal threads). @@ -2101,6 +2118,8 @@ typedef struct { + + // The multipliers a picker offers. `1.0` (Native) is the default; the rest are the round stops // users reason about. Shared so every client's list stays identical. #define PUNKTFUNK_PRESETS { 0.5, 0.67, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, } diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 92a59534..1eff3ca8 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -45,10 +45,12 @@ license=('MIT OR Apache-2.0') # !debug: skip the -debug split package (debuginfo bloat, not shipped). options=('!lto' '!debug') -# All build deps for both crates (Arch runtime packages ship their own headers, so these cover -# build + link). aws-lc/ring need clang+cmake; nasm is for asm. vulkan-headers: the client's -# pf-ffvk crate runs bindgen over FFmpeg's libavutil/hwcontext_vulkan.h (#include ). -makedepends=('rust' 'cargo' 'clang' 'cmake' 'nasm' 'pkgconf' 'git' 'vulkan-headers' +# All build deps for both packages (Arch runtime packages ship their own headers, so these cover +# build + link). aws-lc/ring need clang+cmake; nasm is for asm. ffmpeg stays because the HOST's +# encoder links libav* — the CLIENT dropped it in M10 and decodes natively. No vulkan-headers: +# nothing in the workspace compiles against the system Vulkan headers any more (pyrowave-sys builds +# against its own vendored copy, and both binaries reach Vulkan through ash, which dlopens it). +makedepends=('rust' 'cargo' 'clang' 'cmake' 'nasm' 'pkgconf' 'git' 'gtk4' 'libadwaita' 'sdl3' 'ffmpeg' 'pipewire' 'wayland' 'libxkbcommon' 'opus' 'libei') # Opt-in punktfunk-web / punktfunk-scripting: only then is bun (the build tool AND the vendored @@ -250,15 +252,18 @@ package_punktfunk-host() { package_punktfunk-client() { pkgdesc="Low-latency desktop/game streaming CLIENT — native GTK4/libadwaita Linux app" - # The GTK4/libadwaita shell + its Vulkan session streamer: SDL3 gamepads, FFmpeg (VAAPI + - # Vulkan Video) decode, PipeWire audio/mic. vulkan-icd-loader: the session binary loads - # libvulkan at runtime (ash) for its ash/Skia presenter. + # The GTK4/libadwaita shell + its Vulkan session streamer: SDL3 gamepads, native decode + # (Vulkan Video, VAAPI, and openh264 + rav1d in software), PipeWire audio/mic. + # No ffmpeg since M10: the client links no libav* at all, and its VAAPI rung dlopens libva + # rather than linking it, so the driver optdepends below are all it needs. + # vulkan-icd-loader: the session binary loads libvulkan at runtime (ash) for both its decoder + # and its ash/Skia presenter. # NOT pipewire-pulse: the client speaks NATIVE PipeWire (audio.rs drives libpipewire-0.3 # directly for both playback and the mic uplink) and never opens a Pulse socket, so the # compat shim buys it nothing — while `pipewire-pulse` CONFLICTS with `pulseaudio`, which # made the package uninstallable for anyone keeping real PulseAudio. Matches the .deb # (Recommends) and the RPM (Recommends, "degrade gracefully without it"). - depends=('gtk4' 'libadwaita' 'sdl3' 'ffmpeg' 'pipewire' 'wireplumber' + depends=('gtk4' 'libadwaita' 'sdl3' 'pipewire' 'wireplumber' 'opus' 'libglvnd' 'vulkan-icd-loader') optdepends=('libva-mesa-driver: VAAPI hardware decode on AMD (incl. Steam Deck); software fallback otherwise' 'intel-media-driver: VAAPI hardware decode on Intel' diff --git a/packaging/debian/build-client-deb.sh b/packaging/debian/build-client-deb.sh index e053c998..f0afb5d9 100644 --- a/packaging/debian/build-client-deb.sh +++ b/packaging/debian/build-client-deb.sh @@ -2,11 +2,20 @@ # Build the punktfunk-client .deb (the native GTK4 client) for Ubuntu/Debian desktops. # # Counterpart to build-deb.sh (the host package); same conventions: runtime Depends are -# computed by dpkg-shlibdeps from the binary's DT_NEEDED (GTK4/libadwaita, SDL3, the -# FFmpeg/PipeWire/Opus sonames), so build inside the Ubuntu 26.04 rust-ci image to pin +# computed by dpkg-shlibdeps from the binaries' DT_NEEDED (GTK4/libadwaita, SDL3, the +# PipeWire/Opus sonames), so build inside the Ubuntu 26.04 rust-ci image to pin # the package names the target boxes ship. The client links no NVIDIA libs — no filter # needed. # +# NO libav* here since M10 (design/client-native-decode.md §6): the client decodes with +# pf-vkdecode / pf-vaadec (libva is dlopen'd, never linked) / openh264+rav1d, so nothing in +# either binary has an FFmpeg DT_NEEDED and shlibdeps stops emitting `libavcodec…` on its +# own — there is no list here to prune, which is exactly the property to keep. That also +# ends the soname coupling that forced the host package's BUNDLE_FFMPEG dance: a client +# .deb built on 26.04 no longer names a libavcodec the target box must have. +# The HOST package (build-deb.sh) is unchanged — it encodes with libavcodec and still +# depends on / bundles it. +# # Usage: VERSION=0.0.1~ci42.gdeadbee [ARCH=amd64] [TARGET=] \ # bash packaging/debian/build-client-deb.sh # Output: dist/punktfunk-client__.deb @@ -103,7 +112,17 @@ install -Dm0644 LICENSE-MIT "$DOCDIR/LICENSE-MIT" install -Dm0644 LICENSE-APACHE "$DOCDIR/LICENSE-APACHE" install -Dm0644 README.md "$DOCDIR/README.md" # Third-party crate attributions (regenerate with scripts/gen-third-party-notices.sh). -if [ -f THIRD-PARTY-NOTICES.txt ]; then +# +# The CLIENT-scoped copy, not the workspace-wide one at the repo root: the root file is the host's +# and still lists ffmpeg-next plus the full FFmpeg licence text, while this package links no FFmpeg +# at all since M10. It is the same file the GTK shell shows on its About → Legal page, so the +# installed doc and the running app say the same thing. Falls back to the root file only if the +# generated copy is missing (an old checkout), because shipping no attribution at all is worse. +if [ -f clients/linux/THIRD-PARTY-NOTICES.txt ]; then + install -Dm0644 clients/linux/THIRD-PARTY-NOTICES.txt "$DOCDIR/THIRD-PARTY-NOTICES.txt" +elif [ -f THIRD-PARTY-NOTICES.txt ]; then + echo "warning: clients/linux/THIRD-PARTY-NOTICES.txt missing — shipping the workspace-wide" \ + "file, which attributes host-only dependencies to the client" >&2 install -Dm0644 THIRD-PARTY-NOTICES.txt "$DOCDIR/THIRD-PARTY-NOTICES.txt" fi diff --git a/packaging/flatpak/README.md b/packaging/flatpak/README.md index ed309dd3..2a63d7e5 100644 --- a/packaging/flatpak/README.md +++ b/packaging/flatpak/README.md @@ -23,8 +23,10 @@ published two ways by CI (`.gitea/workflows/flatpak.yml`), on every push to `mai SteamOS `/usr` is read-only and image-based, and the system is **missing `libadwaita` and `libSDL3`** — so a bare `punktfunk-client` binary dropped into `~/.local/bin` won't run. Flatpak is the Deck's native, update-survivable app path (the user already runs Moonlight and chiaki-ng -as flatpaks), and the bundle carries libadwaita (from `org.gnome.Platform//50`) + a bundled SDL3, -with HEVC-capable FFmpeg supplied automatically by the runtime's `codecs-extra` extension. +as flatpaks), and the bundle carries libadwaita (from `org.gnome.Platform//50`) + a bundled SDL3. +It carries no FFmpeg: since M10 the client decodes on the user's own GPU drivers (Vulkan Video, +VAAPI) with openh264 + rav1d as the CPU floor, so the runtime's `codecs-extra` extension — and the +encumbered-codec question it answered — no longer enter into it. App id: **`io.unom.Punktfunk`** (matches the Apple bundle id family and the Decky plugin's flatpak fallback). @@ -63,7 +65,7 @@ repo above is the better path for a human on the Deck: VER=1.2.3 URL="https://git.unom.io/api/packages/unom/generic/punktfunk-client-flatpak/$VER/punktfunk-client-$VER.flatpak" -# Flathub must be enabled (it is on the Deck) so the GNOME runtime + codecs-extra extension pull in: +# Flathub must be enabled (it is on the Deck) so the GNOME runtime pulls in: flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo curl -fL -o /tmp/punktfunk-client.flatpak "$URL" @@ -158,15 +160,17 @@ has been built. with two build-time SDK extensions: `org.freedesktop.Sdk.Extension.rust-stable` (→ //25.08, **rustc 1.96** — the GTK4 dep chain, e.g. pango-sys 0.22, needs ≥ 1.92, which the EOL GNOME-48 / 24.08 rust-stable at 1.89 could not provide) and `org.freedesktop.Sdk.Extension.llvm20` (libclang, -needed by bindgen in ffmpeg-sys-next / sdl3-sys). HEVC-capable libavcodec (soname 61, accepted by -ffmpeg-next 8.x) is supplied **automatically at runtime** by the freedesktop `codecs-extra` -extension point (auto-downloaded with the runtime; no app-side codec declaration). A bundled +needed by bindgen in sdl3-sys / pyrowave-sys). **No libavcodec at any layer** — the client links no +FFmpeg since M10, so neither the SDK's stripped build nor the runtime's `codecs-extra` shadow of it +is involved; HEVC decodes on the GPU's own driver, and there is deliberately no software HEVC +rung (see the manifest header). A bundled **SDL3 3.4.10** module (pinned to match `sdl3-sys 0.6.6+SDL-3.4.10`), and finish-args for Wayland + `--device=all` (GPU/VAAPI render node + evdev + the hidraw char-devices SDL3 needs for DualSense) + `--socket=pulseaudio` (PipeWire-pulse: playback + mic) + `--share=network`. Alongside it: `io.unom.Punktfunk.desktop`, `io.unom.Punktfunk.metainfo.xml`, `io.unom.Punktfunk.svg` (all -installed by the manifest). A `vulkan-headers` module supplies what the session binary's ash/Vulkan -build needs. `cargo-sources.json` (the offline crate cache) is a pure function of +installed by the manifest). No `vulkan-headers` module: it existed for `pf-ffvk`'s bindgen over +FFmpeg's `hwcontext_vulkan.h`, and ash generates its own bindings and dlopens the loader. +`cargo-sources.json` (the offline crate cache) is a pure function of `Cargo.lock`; CI regenerates it each build and it is **gitignored** — generate it on any box with network + `python3`/`aiohttp`/`tomlkit` (`build-flatpak.sh` does this automatically) and, for a build host that lacks those (the Deck), rsync the generated file in alongside the manifest. diff --git a/packaging/flatpak/io.unom.Punktfunk.yml b/packaging/flatpak/io.unom.Punktfunk.yml index b1d6a017..4e7bfa32 100644 --- a/packaging/flatpak/io.unom.Punktfunk.yml +++ b/packaging/flatpak/io.unom.Punktfunk.yml @@ -23,20 +23,26 @@ # libopus and the PipeWire client lib are in the freedesktop base; SDL3 is NOT, so it is built # from source as a bundled module. # -# HEVC decode: the base runtime's libavcodec is a stripped build (no encumbered codecs). The -# freedesktop runtime declares `org.freedesktop.Platform.codecs-extra` as a built-in extension -# point (directory lib/x86_64-linux-gnu/codecs-extra, add-ld-path lib, auto-downloaded with the -# runtime), whose full libavcodec.so.61 transparently shadows the base one at runtime. So HEVC -# (software + VAAPI) works with NO app-side codec extension to declare — we just build against -# the SDK's linkable libavcodec.so.61 and let the runtime swap in the capable build. +# HEVC decode needs NOTHING from the runtime since M10 (design/client-native-decode.md §6): +# the client links no FFmpeg at all. It decodes on the user's own GPU drivers — Vulkan Video +# through the loader in the GL runtime, or VAAPI through libva, which is dlopen'd and never +# linked — with openh264 + rav1d (both vendored, both BSD-2) as the CPU floor. Nothing here +# depends on the base runtime's stripped libavcodec, and nothing depends on +# `org.freedesktop.Platform.codecs-extra` shadowing it either; that whole arrangement (and the +# encumbered-codec question it existed to answer) is simply not this app's problem any more. +# +# ⚠ The one thing that follows: the CPU floor has no HEVC — no permissively-licensed HEVC +# decoder exists — so an HEVC session on a box whose GPU cannot decode HEVC reconnects on a +# codec this client can finish (pf-client-core's `last_rung_verdict`) instead of falling back +# to software. That is a refusal by design, not a packaging gap. app-id: io.unom.Punktfunk runtime: org.gnome.Platform runtime-version: '50' sdk: org.gnome.Sdk # Build-time SDK extensions: # - rust-stable: cargo/rustc 1.96 + the bundled mold linker (/usr/lib/sdk/rust-stable/bin). -# - llvm20: provides libclang (/usr/lib/sdk/llvm20/lib), which bindgen needs — ffmpeg-sys-next -# and sdl3-sys generate their FFI bindings via bindgen at build time. The base SDK ships no +# - llvm20: provides libclang (/usr/lib/sdk/llvm20/lib), which bindgen needs — sdl3-sys and +# pyrowave-sys generate their FFI bindings via bindgen at build time. The base SDK ships no # clang/libclang, so without this the build panics ("Unable to find libclang"). # Both are added to PATH / LIBCLANG_PATH in build-options below. sdk-extensions: @@ -127,12 +133,11 @@ finish-args: build-options: append-path: /usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm20/bin # The rust build resolves everything via pkg-config: gtk4/libadwaita/pipewire/opus AND a - # linkable libavcodec.so.61 from org.gnome.Sdk//50 (the multiarch /usr dir), plus the bundled - # SDL3's .pc from /app. (At runtime the codecs-extra extension swaps in the HEVC-capable - # libavcodec — see the header.) + # the SDK's own .pc files from the multiarch /usr dir (libopus for audiopus_sys, the + # PipeWire client lib, GTK4/libadwaita), plus the bundled SDL3's .pc from /app. env: PKG_CONFIG_PATH: /app/lib/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig - # bindgen (ffmpeg-sys-next / sdl3-sys) loads libclang from the llvm20 extension. + # bindgen (sdl3-sys, pyrowave-sys) loads libclang from the llvm20 extension. LIBCLANG_PATH: /usr/lib/sdk/llvm20/lib # mold (shipped in rust-stable) speeds the ~450-crate link on the Deck APU. RUSTFLAGS: -C link-arg=-fuse-ld=mold @@ -179,11 +184,20 @@ modules: - /lib/pkgconfig # --------------------------------------------------------------------------------------- - # Vulkan-Headers — the SESSION binary's pf-ffvk crate runs bindgen over FFmpeg's - # libavutil/hwcontext_vulkan.h, which `#include `. The GNOME SDK is not - # guaranteed to ship those dev headers, so install them into /app ourselves (headers only, - # no compile) and point pf-ffvk's bindgen at them via PF_FFVK_VULKAN_INCLUDE below. Kept in - # cleanup — headers aren't needed at runtime (the Vulkan LOADER comes from the GL runtime). + # Vulkan-Headers — build-time `vulkan/vulkan.h` for anything below that compiles against + # Vulkan, which the GNOME SDK is not guaranteed to ship dev headers for. Headers only, no + # compile, and `cleanup: '*'` so nothing reaches the runtime (the Vulkan LOADER comes from + # the GL runtime). + # + # It was added for the session binary's pf-ffvk crate, which ran bindgen over FFmpeg's + # libavutil/hwcontext_vulkan.h. M10 deleted pf-ffvk along with the rest of the client's + # FFmpeg (design/client-native-decode.md §6), so that consumer is gone — but the module + # stays, because the gamescope WSI layer built two modules below is itself a VULKAN LAYER + # and compiles against these headers. Module order is the dependency: this one must build + # first. Do not drop it as dead weight; flatpak.yml has no `pull_request:` trigger, so a + # manifest break of that kind reaches main invisibly and a tag then ships no Linux flatpak. + # The native decoder needs nothing from here — pf-vkdecode reaches Vulkan through `ash`, + # which is pure Rust bindings with no bindgen and no C headers. # --------------------------------------------------------------------------------------- - name: vulkan-headers buildsystem: cmake-ninja @@ -300,9 +314,6 @@ modules: # flatpak-builder pre-downloaded as a pinned source below. No {tag}/{key} # placeholders needed: a template without them is used verbatim. SKIA_BINARIES_URL: file:///run/build/punktfunk-client/skia-binaries.tar.gz - # Point pf-ffvk's bindgen at the Vulkan-Headers installed into /app above (its clang - # invocation doesn't inherit the SDK's default include search, so pass it explicitly). - PF_FFVK_VULKAN_INCLUDE: /app/include build-commands: # Drop every reference to the windows-rs GIT dependency before building. That git # source is deliberately NOT vendored into cargo-sources.json — see diff --git a/packaging/nix/README.md b/packaging/nix/README.md index d010a76a..6d1a3456 100644 --- a/packaging/nix/README.md +++ b/packaging/nix/README.md @@ -201,7 +201,7 @@ cargo build --release -p punktfunk-host -p punktfunk-client-linux -p punktfunk-c cargo build --release -p punktfunk-tray ``` -The shell exports `PF_FFVK_VULKAN_INCLUDE` (Vulkan headers for pf-ffvk bindgen) and an +The shell exports an `LD_LIBRARY_PATH` that includes `/run/opengl-driver/lib` so `cargo run` finds the GPU driver. `nix fmt` formats the `.nix` files. diff --git a/packaging/nix/packages.nix b/packaging/nix/packages.nix index a5acdbdd..06cbc4ee 100644 --- a/packaging/nix/packages.nix +++ b/packaging/nix/packages.nix @@ -55,7 +55,6 @@ librsvg, gsettings-desktop-schemas, adwaita-icon-theme, - vulkan-headers, # web console (punktfunk-web): a bun-built Nitro SSR bundle, run on bun. bun, nodejs, @@ -98,7 +97,7 @@ let cmake # pyrowave-sys (C++/Vulkan), the vendored libopus (opus crate), aws-lc-sys (rustls) nasm # libopus SIMD + OpenH264 (openh264 `source` feature) perl # aws-lc-sys asm generation (rustls' aws-lc-rs crypto provider) - rustPlatform.bindgenHook # LIBCLANG_PATH + clang args for ffmpeg-sys-next / pf-ffvk / pyrowave-sys bindgen + rustPlatform.bindgenHook # LIBCLANG_PATH + clang args for ffmpeg-sys-next (host) / pyrowave-sys bindgen addDriverRunpath # provides the `addDriverRunpath` shell fn used in postFixup ]; }; @@ -227,15 +226,12 @@ in "--locked -p punktfunk-client-linux -p punktfunk-client-session -p punktfunk-cli " + "--no-default-features --features punktfunk-client-session/pyrowave"; - # pf-ffvk runs bindgen over libavutil/hwcontext_vulkan.h, which `#include `. - # There is no /usr/include on NixOS, so hand it the Vulkan-Headers include dir explicitly - # (build.rs turns this into a clang `-I`). libavutil's own include path comes from pkg-config. - PF_FFVK_VULKAN_INCLUDE = "${vulkan-headers}/include"; - nativeBuildInputs = commonArgs.nativeBuildInputs ++ [ wrapGAppsHook4 ]; + # No ffmpeg: the client decodes natively since M10 (pf-vkdecode / pf-vaadec — libva is + # dlopen'd, never linked — / openh264 + rav1d, both built from vendored source). The HOST + # derivation above still has it. buildInputs = [ - ffmpeg # FFmpeg decode (pf-client-core) + pf-ffvk links libavutil pipewire # PipeWire audio playback + mic capture libopus # audiopus_sys → system opus via pkg-config (Opus decode) sdl3 # window + gamepads (SDL3 HIDAPI: DualSense touchpad/motion/triggers) diff --git a/packaging/rpm/punktfunk.spec b/packaging/rpm/punktfunk.spec index 7e77dde6..d4d3eea9 100644 --- a/packaging/rpm/punktfunk.spec +++ b/packaging/rpm/punktfunk.spec @@ -103,10 +103,12 @@ BuildRequires: pkgconfig(gbm) BuildRequires: pkgconfig(gtk4) BuildRequires: pkgconfig(libadwaita-1) BuildRequires: pkgconfig(sdl3) -# The client's pf-ffvk crate runs bindgen over FFmpeg's libavutil/hwcontext_vulkan.h, which -# #include — provided by vulkan-headers (Fedora). -BuildRequires: vulkan-headers -# It ALSO links the NVIDIA CUDA driver lib (-lcuda) via FFI, so libcuda.so must be present +# No vulkan-headers BuildRequires: the only crate that ever needed the Vulkan C headers was the +# client's pf-ffvk, whose bindgen ran over FFmpeg's libavutil/hwcontext_vulkan.h — and M10 deleted +# it with the rest of the client's FFmpeg. Nothing has replaced that need: pf-vkdecode/pf-presenter +# reach Vulkan through ash (loader dlopen'd, no headers), and pyrowave-sys builds against its own +# vendored copy (crates/pyrowave-sys/build.rs). +# The HOST links the NVIDIA CUDA driver lib (-lcuda) via FFI, so libcuda.so must be present # at LINK time. A normal NVIDIA host (or Bazzite -nvidia) has it; a headless COPR/koji builder # without a GPU does NOT — point %build at the CUDA toolkit stub (…/stubs/libcuda.so) there, # e.g. `ln -s $(rpm -ql cuda-cudart-devel | grep stubs/libcuda.so | head -1) /usr/lib64/`. @@ -502,7 +504,11 @@ install -Dm0644 scripts/punktfunk-scripting.service %{buildroot}%{_userunitdir}/ %endif %files client -%license LICENSE-MIT LICENSE-APACHE THIRD-PARTY-NOTICES.txt +# The CLIENT-scoped notices, not the workspace-wide root file: the root one is the host's and still +# carries ffmpeg-next plus the full FFmpeg licence text, while this subpackage links no FFmpeg at +# all since M10. Same file the GTK shell shows on About → Legal (scripts/gen-third-party-notices.sh +# generates both). `%%license` installs it under its basename, so the path stays the usual one. +%license LICENSE-MIT LICENSE-APACHE clients/linux/THIRD-PARTY-NOTICES.txt %{_bindir}/punktfunk-client %{_bindir}/punktfunk-session %{_bindir}/punktfunk diff --git a/packaging/windows/licenses/FFmpeg-LGPL-NOTICE.txt b/packaging/windows/licenses/FFmpeg-LGPL-NOTICE.txt index bf409bba..99ecb925 100644 --- a/packaging/windows/licenses/FFmpeg-LGPL-NOTICE.txt +++ b/packaging/windows/licenses/FFmpeg-LGPL-NOTICE.txt @@ -4,8 +4,9 @@ FFmpeg — third-party component notice This product bundles unmodified shared libraries from the FFmpeg project (avcodec / avutil / avformat / swscale / swresample and their dependencies) as separate dynamic-link libraries (DLLs). punktfunk uses them only for hardware -video encode (AMD AMF / Intel QSV) on the host and hardware/software video -decode on the client. +video encode (AMD AMF / Intel QSV) in the streaming host. The punktfunk client +bundles no FFmpeg libraries at all — it decodes with its own native decoders — +so this notice concerns the host installer only. License ------- diff --git a/scripts/ci/docker-prune.sh b/scripts/ci/docker-prune.sh index b6b4f4dc..bff35bf6 100644 --- a/scripts/ci/docker-prune.sh +++ b/scripts/ci/docker-prune.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash -# CI runner disk hygiene — invoked by docker-prune.service (every 30 min). Lives in a real script +# CI runner disk hygiene — invoked by docker-prune.service (every 2 min). Lives in a real script # rather than inline ExecStart= lines because systemd does its OWN $-expansion on ExecStart and # empties shell vars / $(...) before /bin/sh sees them (silently breaking the logic under `|| true`). # -# See docker-prune.service for the full why. The headline: the act_runner cache server's blob store -# lives INSIDE the long-running runner container's writable layer, where `docker prune` can't reach -# it — left alone it grows to tens of GB and fills the disk on its own. +# See docker-prune.service for the full why. Sibling: docker-reclaim.sh (hourly) handles what +# act_runner *leaks* — per-job volumes, stale networks, old build cache. This one handles what +# CI legitimately *produces* and then abandons: per-SHA app tags and the layers they pin. set -u export PATH=/usr/bin:/bin:/usr/local/bin:$PATH @@ -23,11 +23,26 @@ MIN_FREE_GB=${MIN_FREE_GB:-60} # ...or this little is left, whichever t # 2026-07-29: zero burst clears fired in six hours # while deb still died of ENOSPC between polls. -# 1) Routine: trim aged images / build cache / stopped containers. sha- tags aren't -# dangling, so -a is required. until=2h, not 6h: on a busy day every image is younger than six -# hours, so the filter matched nothing and a run reclaimed 0B while `docker system df` was -# reporting 20+ GB reclaimable. Two hours still protects a re-run of the push being worked on. -docker image prune -af --filter until=2h || true +# 1) Routine: retire aged per-SHA app tags, then sweep what untagging released. +# ⚠ NEVER `docker image prune -a` on this tick. `until=` filters on image CREATION time, so a +# CI *base* image (built days ago) that merely has no container this instant counts as "aged" — +# including one a job JUST PULLED whose container does not exist yet. Measured 2026-08-07: +# this tick ran 07:36:09–:29 and a rust job's `docker create` failed at 07:36:29 with +# "No such image: …punktfunk-rust-ci:latest" — three sampled failures that morning, each +# coinciding with a prune run to the second — and every idle base image was re-pulled within +# minutes (4–7 GB each), churning the LAN registry for nothing. +# The only tag debris this host actually accretes is the per-SHA app tags (web/docs — their +# creation time IS the local build time, so a 2h age gate is exact), and a dangling-only prune +# cannot touch a tagged image, so neither step can race a starting job. +now=$(date +%s) +docker images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep ':sha-' | while read -r ref; do + created=$(docker image inspect -f '{{.Created}}' "$ref" 2>/dev/null) || continue + cts=$(date -d "$created" +%s 2>/dev/null) || continue + if [ $((now - cts)) -ge 7200 ]; then + docker rmi "$ref" >/dev/null 2>&1 || true + fi +done +docker image prune -f || true docker builder prune -af --filter until=2h || true docker buildx prune -af --filter until=2h || true docker container prune -f --filter until=2h || true @@ -44,7 +59,9 @@ docker network prune -f --filter until=2h || true # what matters is absolute headroom for three concurrent target/ dirs, not a ratio — and the # ratio moves whenever the disk is resized (it went 123 G -> 175 G on 2026-07-29) while the # headroom three jobs need does not. In-use images are protected by the daemon, so a burst clear -# cannot pull the rug from a live job. +# cannot pull the rug from a live job — but the blanket `-a` prune below CAN race an image that +# is pulled-but-not-yet-created (the section 1 lesson). That narrow window is accepted HERE +# only: when the alternative is every concurrent job dying of ENOSPC, one job re-pulling loses. PCT=$(df --output=pcent / | tr -dc '0-9') FREE_GB=$(df --output=avail -BG / | tr -dc '0-9') # Two flat tests into a flag rather than one multi-line `{ …; } || { …; }` condition: the brace-group diff --git a/scripts/ci/docker-reclaim.service b/scripts/ci/docker-reclaim.service new file mode 100644 index 00000000..7e794407 --- /dev/null +++ b/scripts/ci/docker-reclaim.service @@ -0,0 +1,20 @@ +# Hourly reclaim of Docker resources act_runner LEAKS (per-job volumes, stale networks, old build +# cache). Sibling of docker-prune.service, which handles what CI legitimately produces and then +# abandons; the split matters because this one must stay conservative enough to run while jobs are +# live (dangling-only volumes, age-gated networks) — see docker-reclaim.sh for the full why. +# +# Install: see the header of docker-reclaim.sh (note the installed unit name is +# ci-docker-reclaim.service — existing fleet hosts already run it under that name). + +[Unit] +Description=Reclaim disk leaked by Gitea act_runner (per-job volumes, networks, stale build cache) +Documentation=https://git.unom.io/unom/punktfunk +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/ci-docker-reclaim.sh +# Never let maintenance starve a running build. +Nice=10 +IOSchedulingClass=idle diff --git a/scripts/ci/docker-reclaim.sh b/scripts/ci/docker-reclaim.sh new file mode 100644 index 00000000..9b50423f --- /dev/null +++ b/scripts/ci/docker-reclaim.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Reclaim the disk that Gitea act_runner leaks on this host. +# +# Why this exists: act_runner creates a per-job network and a pair of named volumes, and leaks both +# when a job is killed or the runner restarts. By 2026-07-25 that had accumulated 252 unused volumes +# (11.7 GB) and 94 stale networks — some dating to task 5626 while current tasks were ~25233 — and +# concurrent builds then exhausted the disk, failing CI with "No space left on device" at both the +# cargo and the Docker/overlayfs layer. The stale networks are also what once broke the docs deploy +# by exhausting Docker's default address pool and swallowing the DMZ 192.168.50.0/24 range. +# +# This ran on home-runner-1 only, hand-installed; home-runner-2 went without it and by 2026-08-07 +# had re-accumulated 176 leaked volumes (~60 GB) + 22 GB build cache and spent two days failing +# jobs at ENOSPC. Hence checked in: BOTH runner hosts install it, from here. +# +# Install on a runner host (root): +# install -m755 scripts/ci/docker-reclaim.sh /usr/local/sbin/ci-docker-reclaim.sh +# install -m644 scripts/ci/docker-reclaim.service /etc/systemd/system/ci-docker-reclaim.service +# install -m644 scripts/ci/docker-reclaim.timer /etc/systemd/system/ci-docker-reclaim.timer +# systemctl daemon-reload && systemctl enable --now ci-docker-reclaim.timer +# +# Deliberately NOT `docker volume prune -a`: that would also delete any intentional named volume +# that merely has no container attached at the moment the timer fires — e.g. the `docker-mirror` +# pull-through registry cache or the runner cache during a restart — silently destroying it. Only +# volumes act_runner named are removed here. +# +# Also deliberately NOT pruning images: on this host the per-SHA CI tags share all their layers with +# `:latest`, so removing them reclaims nothing while forcing re-pulls. `docker system df`'s +# "RECLAIMABLE" column counts shared layers once per image and overstates the win badly. +# (docker-prune.sh owns tag retirement — age-gated and never `image prune -a`, see its header.) +set -uo pipefail + +log() { echo "ci-docker-reclaim: $*"; } + +before_avail=$(df --output=avail -BM / | tail -1 | tr -dc '0-9') + +# 1. Leaked per-job volumes — dangling AND named by act_runner. In-use volumes are never listed as +# dangling, so a running job's volumes cannot be hit. +mapfile -t stale_vols < <(docker volume ls -qf dangling=true 2>/dev/null | grep '^GITEA-ACTIONS-TASK-' || true) +if ((${#stale_vols[@]})); then + printf '%s\n' "${stale_vols[@]}" | xargs -r docker volume rm >/dev/null 2>&1 + log "removed ${#stale_vols[@]} leaked act_runner volumes" +else + log "no leaked act_runner volumes" +fi + +# 2. Unused networks older than 2h — never touches a live job's network (it is in use), and the age +# filter keeps a just-created one safe against a race with a starting job. +net_out=$(docker network prune -f --filter until=2h 2>&1 | grep -c '^GITEA-ACTIONS' || true) +log "removed ${net_out:-0} stale job networks" + +# 3. Build cache older than 48h. Recent cache is what makes builds fast, so it is kept. +cache_freed=$(docker builder prune -f --filter until=48h 2>&1 | awk '/^Total:/ {print $2}') +log "build cache freed: ${cache_freed:-0B}" + +after_avail=$(df --output=avail -BM / | tail -1 | tr -dc '0-9') +log "avail ${before_avail}M -> ${after_avail}M (reclaimed $((after_avail - before_avail))M)" +df -h / | tail -1 | sed 's/^/ci-docker-reclaim: /' diff --git a/scripts/ci/docker-reclaim.timer b/scripts/ci/docker-reclaim.timer new file mode 100644 index 00000000..ad4edfab --- /dev/null +++ b/scripts/ci/docker-reclaim.timer @@ -0,0 +1,16 @@ +# Hourly is the right cadence for LEAKS: they only accrue when jobs die abnormally, and the +# per-tick docker-prune.timer (every 2 min) already carries the burst guard for genuine +# disk-pressure emergencies. Install: see the header of docker-reclaim.sh. + +[Unit] +Description=Hourly reclaim of act_runner-leaked Docker disk + +[Timer] +OnCalendar=hourly +# Catch up after a reboot rather than waiting for the next slot. +Persistent=true +# Spread it off the hour so it does not collide with scheduled CI. +RandomizedDelaySec=300 + +[Install] +WantedBy=timers.target diff --git a/scripts/ci/ensure-windows-toolchain.ps1 b/scripts/ci/ensure-windows-toolchain.ps1 index 6201e5a0..2800d705 100644 --- a/scripts/ci/ensure-windows-toolchain.ps1 +++ b/scripts/ci/ensure-windows-toolchain.ps1 @@ -1,5 +1,5 @@ # Idempotent pre-flight for punktfunk's Windows CI dependencies: WDK + cargo-wdk (driver builds), -# FFmpeg x64/ARM64 trees, Inno Setup, and the aarch64-pc-windows-msvc rustup target. Run at the +# the x64 FFmpeg tree (host amf-qsv only), Inno Setup, and the aarch64-pc-windows-msvc rustup target. Run at the # start of every Windows CI job so ANY runner - freshly built from unom/infra's windows-runner/ # template, rebuilt, or a new one added later - self-provisions on first real use, instead of # needing a human to remember to dispatch a separate provisioning workflow first (and instead of diff --git a/scripts/ci/provision-windows-punktfunk-extras.ps1 b/scripts/ci/provision-windows-punktfunk-extras.ps1 index 2ba2042c..63fe3b42 100644 --- a/scripts/ci/provision-windows-punktfunk-extras.ps1 +++ b/scripts/ci/provision-windows-punktfunk-extras.ps1 @@ -1,5 +1,5 @@ -# Layers punktfunk-specific tooling onto the shared unom Windows CI runner: per-arch FFmpeg -# (host + client native builds), Inno Setup (the host installer), and the aarch64-pc-windows-msvc +# Layers punktfunk-specific tooling onto the shared unom Windows CI runner: FFmpeg (the HOST's +# amf-qsv encode leg, x64 only), Inno Setup (the host installer), and the aarch64-pc-windows-msvc # rustup target (windows-msix.yml's ARM64 leg). The runner itself - act_runner, Node, rustup, # VS Build Tools/NASM/CMake/LLVM - is provisioned generically by unom/infra # (windows-runner/windows-runner.pkr.hcl + proxmox/windows-runner's Terraform clone); this script @@ -26,14 +26,19 @@ if (Test-Path $rustup) { Write-Warning "rustup not found at $rustup - has unom/infra's setup-gitea-runner-base.ps1 run on this box yet?" } -# --- FFmpeg shared trees for the host (amf-qsv encode) + clients (decode). BtbN **lgpl-shared** +# --- FFmpeg shared tree for the HOST's amf-qsv encode leg (windows-host.yml). BtbN **lgpl-shared** # builds: the AMD/Intel AMF + Intel QSV encoders, swscale, and the HEVC decoder are all present in # the LGPL build, and punktfunk never calls the GPL-only encoders (x264/x265 - software encode is # the separate BSD-2 openh264 crate; NVENC is the direct NVIDIA SDK). lgpl-shared keeps the # bundled DLLs LGPL-2.1+ (dynamic linking satisfies the relink duty) rather than GPL, so the # shipped installer/MSIX stay consistent with punktfunk's MIT OR Apache-2.0 posture. -# MIGRATION: a runner previously provisioned with the old *gpl-shared* trees must be -# re-provisioned - delete C:\Users\Public\ffmpeg and C:\Users\Public\ffmpeg-arm64, then re-run. +# ⚠ The CLIENT no longer links FFmpeg at all (M10, design/client-native-decode.md §6): it decodes +# with pf-vkdecode / pf-dxvadec / openh264 + rav1d. windows.yml and windows-msix.yml set no +# FFMPEG_DIR and the MSIX bundles no libav* DLLs, so only the x64 tree is fetched now - the ARM64 +# one existed solely for the ARM64 client leg. Delete a stale C:\Users\Public\ffmpeg-arm64 by +# hand; this script does not remove what it no longer installs. +# MIGRATION: a runner previously provisioned with the old *gpl-shared* tree must be +# re-provisioned - delete C:\Users\Public\ffmpeg, then re-run. # These DLLs are bundled verbatim into the code-signed host installer/MSIX, so the download is # SHA-256-pinned (like VB-CABLE below): BtbN's `latest` tag is a ROLLING release whose assets are # re-uploaded over time, so an unverified fetch would let a hijacked/MITM'd upstream asset land @@ -42,7 +47,7 @@ if (Test-Path $rustup) { # that is intentional: re-download, re-verify the new archive, and update the two pins here. # Refresh a pin: (Get-FileHash .\ffmpeg-.zip -Algorithm SHA256).Hash function Get-BtbnFfmpeg { - param([string]$Dir, [string]$ZipTag, [string]$Sha) # ZipTag: 'win64' (x64) or 'winarm64' (ARM64 cross tree) + param([string]$Dir, [string]$ZipTag, [string]$Sha) # ZipTag: 'win64' (x64); BtbN also publishes 'winarm64' if (Test-Path (Join-Path $Dir 'lib\avcodec.lib')) { info "FFmpeg ($ZipTag) already present at $Dir"; return } info "fetching FFmpeg ($ZipTag, BtbN lgpl-shared, SHA-256 pinned)" $url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n7.1-latest-$ZipTag-lgpl-shared-7.1.zip" @@ -60,27 +65,13 @@ function Get-BtbnFfmpeg { Move-Item -Path $inner.FullName -Destination $Dir Remove-Item -Force $zip; Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } -Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg" -ZipTag 'win64' -Sha '89F3469706E5D53AEA5CF34AEE63E62CE746E6159D7AEE473D330B02A47558E6' -Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg-arm64" -ZipTag 'winarm64' -Sha 'D96B4CE08CEBDCC6AD0E3934A3F962915E440EEFB9D73831AFEA4D80E35129A5' +Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg" -ZipTag 'win64' -Sha '89F3469706E5D53AEA5CF34AEE63E62CE746E6159D7AEE473D330B02A47558E6' -# --- Vulkan-Headers (pf-ffvk's bindgen: libavutil/hwcontext_vulkan.h includes , -# and Windows has no system copy). Headers only - the loader (vulkan-1.dll) is a GPU-driver -# component and is never linked at build time, so the full Vulkan SDK is deliberately NOT -# required. Pinned Khronos tag; bump deliberately alongside FFmpeg/driver expectations. --- -$vkHdrDir = "C:\Users\Public\vulkan-headers" -$vkHdrTag = "v1.4.309" -if (-not (Test-Path (Join-Path $vkHdrDir 'include\vulkan\vulkan.h'))) { - info "fetching Vulkan-Headers $vkHdrTag" - $url = "https://github.com/KhronosGroup/Vulkan-Headers/archive/refs/tags/$vkHdrTag.zip" - $zip = "$vkHdrDir.zip"; $tmp = "$vkHdrDir-extract" - Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing - if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp } - Expand-Archive -Path $zip -DestinationPath $tmp -Force # one top-level Vulkan-Headers- folder - $inner = Get-ChildItem $tmp -Directory | Select-Object -First 1 - if (Test-Path $vkHdrDir) { Remove-Item -Recurse -Force $vkHdrDir } - Move-Item -Path $inner.FullName -Destination $vkHdrDir - Remove-Item -Force $zip; Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue -} else { info "Vulkan-Headers already present at $vkHdrDir" } +# --- No Vulkan-Headers here any more: they existed only for pf-ffvk's bindgen over +# libavutil/hwcontext_vulkan.h, and that crate is gone (M10). Nothing punktfunk builds on Windows +# needs Vulkan headers at compile time - ash generates its own bindings and dlopens vulkan-1.dll, +# which is a GPU-driver component. A stale C:\Users\Public\vulkan-headers is harmless; delete it +# by hand if you want the disk back. --- # --- Inno Setup (ISCC.exe) for the host installer build (windows-host.yml). pack-host-installer.ps1 # locates it at its fixed Program Files path, so it need not be on PATH - just present. The .iss @@ -101,13 +92,14 @@ if (-not (Test-Path $isccPath) -or ($innoVer -and [version]$innoVer -lt [version # --- Drop punktfunk's env vars into the generic runner's daemon wrapper extension point (see # unom/infra's scripts/setup-gitea-runner-base.ps1) so the act_runner daemon - and therefore every -# job it runs - sees FFMPEG_DIR without unom/infra needing to know punktfunk exists. --- +# job it runs - sees FFMPEG_DIR without unom/infra needing to know punktfunk exists. +# FFMPEG_DIR + the PATH prepend are the HOST's (windows-host.yml amf-qsv: import libs at link time, +# the DLLs at test time). The client workflows ignore both - they link no libav*. --- $projectEnv = "C:\Users\Public\act-runner\project-env.ps1" @' $env:FFMPEG_DIR = "C:\Users\Public\ffmpeg" -$env:PF_FFVK_VULKAN_INCLUDE = "C:\Users\Public\vulkan-headers\include" $env:PATH = "C:\Users\Public\ffmpeg\bin;" + $env:PATH '@ | Set-Content -Encoding UTF8 $projectEnv -info "wrote $projectEnv (FFMPEG_DIR, PF_FFVK_VULKAN_INCLUDE) - restart the gitea-act-runner scheduled task to pick it up" +info "wrote $projectEnv (FFMPEG_DIR) - restart the gitea-act-runner scheduled task to pick it up" info "punktfunk extras provisioned OK." diff --git a/scripts/gen-third-party-notices.py b/scripts/gen-third-party-notices.py index f072e70a..4a44f695 100755 --- a/scripts/gen-third-party-notices.py +++ b/scripts/gen-third-party-notices.py @@ -12,7 +12,16 @@ Apache/Unicode/etc.) crates linked into shipped punktfunk artifacts. `cargo abou about.toml) produces an equivalent, network-augmented result in CI; this is the dependency-free fallback that also runs locally and is committed as a baseline. +By default it covers the WHOLE workspace, which is what the root file must be (the host and +the desktop clients ship out of it). `--packages [,…]` restricts it to the transitive +dependency closure of the named workspace members instead — the Apple and Android clients link +exactly one Rust crate each (`punktfunk-core`, and the JNI bridge over it), so a workspace-wide +copy attributed them things they do not contain: FFmpeg, the NVENC SDK, GTK, windows-rs. Listing a +dependency that is not there is not a licence violation, but it is a false statement in a file +whose entire job is to be true. + Usage: python3 scripts/gen-third-party-notices.py [--out THIRD-PARTY-NOTICES.txt] + [--packages punktfunk-core,…] """ import argparse import hashlib @@ -82,21 +91,71 @@ VENDORED_TREES = [ ] +def closure(meta, roots): + """Package ids reachable from `roots` through `cargo metadata`'s resolve graph. + + Deliberately the WHOLE resolve graph, not a per-target one: `cargo metadata` resolves + every `cfg()`-gated dependency of every member, so this OVER-approximates (an + `cfg(windows)`-only crate is reachable from a root even on a Linux build). Over-listing an + attribution is the safe direction; under-listing one is the failure this file exists to + prevent. What it does NOT do is pull in crates reachable only from OTHER workspace members, + which is the whole point. + """ + by_name = {} + for p in meta["packages"]: + by_name.setdefault(p["name"], p["id"]) + nodes = {n["id"]: n for n in meta.get("resolve", {}).get("nodes", [])} + seen, stack = set(), [] + for r in roots: + pid = by_name.get(r) + if pid is None: + raise SystemExit(f"--packages: no package named {r!r} in this workspace") + stack.append(pid) + while stack: + pid = stack.pop() + if pid in seen: + continue + seen.add(pid) + stack.extend(nodes.get(pid, {}).get("dependencies", [])) + return seen + + def main(): ap = argparse.ArgumentParser() ap.add_argument("--out", default="THIRD-PARTY-NOTICES.txt") ap.add_argument("--manifest", default="Cargo.toml") + ap.add_argument( + "--packages", + default="", + help="comma-separated workspace member names; restrict the notices to their transitive " + "dependency closure instead of the whole workspace", + ) args = ap.parse_args() + # `--all-features` is what makes `--packages` a GUARANTEE rather than a coincidence. Without + # it, cargo resolves the workspace with default features and unifies them across members, so a + # scoped closure can pick up a crate only because some OTHER member turned the feature on — + # and, worse, can MISS one when no member does. punktfunk-core's `quic` is exactly that case: + # it is not a default feature, and quinn/opus/rustls reach the Apple file today only through + # the workspace-wide union. Resolving every feature over-approximates instead, which is the + # safe direction for an attribution file: listing a crate that is not linked is untidy, + # omitting one that is is the failure this file exists to prevent. meta = json.loads(subprocess.check_output( - ["cargo", "metadata", "--format-version", "1", "--offline", "--manifest-path", args.manifest], + ["cargo", "metadata", "--format-version", "1", "--offline", "--all-features", + "--manifest-path", args.manifest], text=True)) ws_members = set(meta.get("workspace_members", [])) + keep = None + if args.packages.strip(): + keep = closure(meta, [n.strip() for n in args.packages.split(",") if n.strip()]) + pkgs = [] for p in meta["packages"]: if p["id"] in ws_members: continue # first-party (covered by the root LICENSE-MIT / LICENSE-APACHE) + if keep is not None and p["id"] not in keep: + continue pkgs.append(p) pkgs.sort(key=lambda p: (p["name"].lower(), p["version"])) @@ -142,6 +201,9 @@ def main(): w("below. Each is distributed under its own permissive license; the full license texts") w("follow the manifest. This file is generated by scripts/gen-third-party-notices.py") w("(or `cargo about`, see about.toml) — do not edit by hand.") + if keep is not None: + w("") + w(f"Scope: the Rust crates linked by {args.packages} — not the whole punktfunk workspace.") w("") w(f"Total third-party crates: {len(pkgs)}") w("") diff --git a/scripts/gen-third-party-notices.sh b/scripts/gen-third-party-notices.sh index eea38e0d..569c4863 100755 --- a/scripts/gen-third-party-notices.sh +++ b/scripts/gen-third-party-notices.sh @@ -20,16 +20,39 @@ else fi echo "==> wrote $OUT" >&2 -# Keep the per-client in-tree copies in sync (the GUI apps bundle these as resources/assets and -# show them on their Acknowledgements / Open-source-licenses screen). The Linux/Windows Rust clients -# embed the root file directly via include_str!, so they need no copy. +# Regenerate the per-client in-tree copies. EVERY client has one now, because every client SHOWS +# it: the mobile apps bundle theirs as a resource/asset for their Acknowledgements screen, and the +# two desktop shells `include_str!` theirs onto their Licenses page (the MSIX and the client .deb +# ship the file as well). +# +# These are GENERATED, not copied. They used to be the workspace-wide file, which attributed to +# every client every crate anything in this repo links: FFmpeg, the NVENC SDK, GTK4, windows-rs. +# The Apple app links ONE Rust crate (punktfunk-core, through PunktfunkCore.xcframework — see +# scripts/build-xcframework.sh) and Android links the JNI bridge over it; everything else in those +# apps is Swift/Kotlin and platform frameworks. +# +# M10 — the client's FFmpeg excision — is what turned the same untidiness on the DESKTOP copies +# into a false statement a user can see: the shells print an `ffmpeg-next 8.1.0 — WTFPL` line and +# the full FFmpeg licence text three screens under a card saying no FFmpeg is bundled. So they are +# scoped too, each to the binaries its package actually installs — the shell, the session streamer, +# the headless CLI, and on Linux the update helper (`pf-update` ships as pf-update-client). +# +# The ROOT file stays workspace-wide on purpose: the HOST ships out of it, and the host does still +# link FFmpeg. +# +# Only the offline generator can scope a file (cargo-about renders the whole workspace), so these +# always go through it — the root file above still prefers cargo-about when installed. if [ "$OUT" = "THIRD-PARTY-NOTICES.txt" ]; then - for dest in \ - clients/apple/Sources/PunktfunkKit/Resources/THIRD-PARTY-NOTICES.txt \ - clients/android/app/src/main/assets/THIRD-PARTY-NOTICES.txt; do - if [ -d "$(dirname "$dest")" ]; then - cp "$OUT" "$dest" - echo "==> synced $dest" >&2 - fi - done + # + while read -r dest packages; do + [ -n "$dest" ] || continue + [ -d "$(dirname "$dest")" ] || continue + python3 scripts/gen-third-party-notices.py --packages "$packages" --out "$dest" + echo "==> generated $dest ($packages closure)" >&2 + done <<'CLIENTS' +clients/apple/Sources/PunktfunkKit/Resources/THIRD-PARTY-NOTICES.txt punktfunk-core +clients/android/app/src/main/assets/THIRD-PARTY-NOTICES.txt punktfunk-client-android +clients/linux/THIRD-PARTY-NOTICES.txt punktfunk-client-linux,punktfunk-client-session,punktfunk-cli,pf-update +clients/windows/THIRD-PARTY-NOTICES.txt punktfunk-client-windows,punktfunk-client-session,punktfunk-cli +CLIENTS fi