From 28cad8453dd6a4cf73fc39edfd4f26c3799c4155 Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Wed, 5 Aug 2026 16:29:27 -0400 Subject: [PATCH 1/6] refactor(hwdec): share HEVC and AV1 parsing --- crates/rawshift-hwdec/src/{vaapi => bitstream}/av1.rs | 9 ++++++--- crates/rawshift-hwdec/src/{vaapi => bitstream}/bits.rs | 2 +- crates/rawshift-hwdec/src/{vaapi => bitstream}/hevc.rs | 10 +++++++--- crates/rawshift-hwdec/src/bitstream/mod.rs | 5 +++++ crates/rawshift-hwdec/src/lib.rs | 7 +++++++ crates/rawshift-hwdec/src/vaapi/mod.rs | 6 ++---- 6 files changed, 28 insertions(+), 11 deletions(-) rename crates/rawshift-hwdec/src/{vaapi => bitstream}/av1.rs (99%) rename crates/rawshift-hwdec/src/{vaapi => bitstream}/bits.rs (99%) rename crates/rawshift-hwdec/src/{vaapi => bitstream}/hevc.rs (99%) create mode 100644 crates/rawshift-hwdec/src/bitstream/mod.rs diff --git a/crates/rawshift-hwdec/src/vaapi/av1.rs b/crates/rawshift-hwdec/src/bitstream/av1.rs similarity index 99% rename from crates/rawshift-hwdec/src/vaapi/av1.rs rename to crates/rawshift-hwdec/src/bitstream/av1.rs index 66b6d96..eac66e0 100644 --- a/crates/rawshift-hwdec/src/vaapi/av1.rs +++ b/crates/rawshift-hwdec/src/bitstream/av1.rs @@ -1,5 +1,5 @@ -//! AV1 still-picture parsing for the VAAPI backend — **safe Rust only** -//! (no `unsafe`; FFI stays in `sys.rs`/`mod.rs`). +//! AV1 still-picture parsing shared by hardware backends — **safe Rust only** +//! (no `unsafe`; platform FFI stays in backend modules). //! //! ## Scope //! @@ -16,7 +16,8 @@ //! `VASliceParameterBufferAV1` per tile. use super::bits::{BitReader, PResult, ParseError, clip3}; -use super::sys; +#[cfg(hwdec_backend = "vaapi")] +use crate::vaapi::sys; // ── OBU framing (§5.3) ────────────────────────────────────────────────────── @@ -1144,6 +1145,7 @@ pub fn parse_still_picture(config_obus: &[u8], payload: &[u8]) -> PResult sys::VASliceParameterBufferAV1 { sys::VASliceParameterBufferAV1 { slice_data_size: tile.data.len() as u32, diff --git a/crates/rawshift-hwdec/src/vaapi/bits.rs b/crates/rawshift-hwdec/src/bitstream/bits.rs similarity index 99% rename from crates/rawshift-hwdec/src/vaapi/bits.rs rename to crates/rawshift-hwdec/src/bitstream/bits.rs index 4e34c14..3f328d8 100644 --- a/crates/rawshift-hwdec/src/vaapi/bits.rs +++ b/crates/rawshift-hwdec/src/bitstream/bits.rs @@ -1,4 +1,4 @@ -//! Safe bitstream readers shared by the HEVC and AV1 header parsers. +//! Safe bitstream readers shared by the HEVC and AV1 still-picture parsers. //! //! **Safe Rust only** — this module (like `hevc.rs` / `av1.rs`) contains no //! `unsafe`; all FFI stays in `sys.rs` and the call sites in `mod.rs`. diff --git a/crates/rawshift-hwdec/src/vaapi/hevc.rs b/crates/rawshift-hwdec/src/bitstream/hevc.rs similarity index 99% rename from crates/rawshift-hwdec/src/vaapi/hevc.rs rename to crates/rawshift-hwdec/src/bitstream/hevc.rs index f9df68c..7ea38ec 100644 --- a/crates/rawshift-hwdec/src/vaapi/hevc.rs +++ b/crates/rawshift-hwdec/src/bitstream/hevc.rs @@ -1,5 +1,5 @@ -//! HEVC still-picture header parsing for the VAAPI backend — **safe Rust -//! only** (no `unsafe`; FFI stays in `sys.rs`/`mod.rs`). +//! HEVC still-picture header parsing shared by hardware backends — **safe +//! Rust only** (no `unsafe`; platform FFI stays in backend modules). //! //! ## Scope //! @@ -23,7 +23,8 @@ //! `slice_data()`) and the emulation-prevention-byte count VAAPI wants. use super::bits::{BitReader, PResult, ParseError, Rbsp, rbsp_from_nal_payload}; -use super::sys; +#[cfg(hwdec_backend = "vaapi")] +use crate::vaapi::sys; // ── NAL classification ────────────────────────────────────────────────────── @@ -743,6 +744,7 @@ fn ceil_log2(x: u32) -> u32 { /// Uniform tile partition of `total` CTBs into `count` parts, as the /// `minus1` sizes VAAPI wants (H.265 §6.5.1 derivation). +#[cfg(hwdec_backend = "vaapi")] fn uniform_partition_minus1(total: u32, count: u32) -> Vec { (0..count) .map(|i| (((i + 1) * total) / count - (i * total) / count - 1) as u16) @@ -752,6 +754,7 @@ fn uniform_partition_minus1(total: u32, count: u32) -> Vec { /// Fill `VAPictureParameterBufferHEVC` for a still picture decoded into /// `surface`. `nut` is the slice NAL type; `st_rps_bits` comes from the /// first independent slice header. +#[cfg(hwdec_backend = "vaapi")] pub fn build_pic_param( sps: &Sps, pps: &Pps, @@ -894,6 +897,7 @@ pub fn build_pic_param( /// Fill `VASliceParameterBufferHEVC` for one coded slice NAL of /// `slice_data_size` bytes at offset 0 of its own data buffer. +#[cfg(hwdec_backend = "vaapi")] pub fn build_slice_param( sh: &SliceHeader, pps: &Pps, diff --git a/crates/rawshift-hwdec/src/bitstream/mod.rs b/crates/rawshift-hwdec/src/bitstream/mod.rs new file mode 100644 index 0000000..548f1b4 --- /dev/null +++ b/crates/rawshift-hwdec/src/bitstream/mod.rs @@ -0,0 +1,5 @@ +//! Safe parsing and framing shared by the platform hardware-decode backends. + +pub(crate) mod av1; +pub(crate) mod bits; +pub(crate) mod hevc; diff --git a/crates/rawshift-hwdec/src/lib.rs b/crates/rawshift-hwdec/src/lib.rs index 5d77541..4a0bfd7 100644 --- a/crates/rawshift-hwdec/src/lib.rs +++ b/crates/rawshift-hwdec/src/lib.rs @@ -48,6 +48,13 @@ #![deny(unsafe_op_in_unsafe_fn)] +#[cfg(any( + hwdec_backend = "vaapi", + hwdec_backend = "mediacodec", + test +))] +mod bitstream; + // The VAAPI platform backend: compiled only when build.rs selected it // (`vaapi` explicit flag, or `hw` on a linux-gnu target). #[cfg(hwdec_backend = "vaapi")] diff --git a/crates/rawshift-hwdec/src/vaapi/mod.rs b/crates/rawshift-hwdec/src/vaapi/mod.rs index faf0aed..dc8e13f 100644 --- a/crates/rawshift-hwdec/src/vaapi/mod.rs +++ b/crates/rawshift-hwdec/src/vaapi/mod.rs @@ -36,10 +36,7 @@ //! invariant. The bitstream parsers ([`bits`], [`hevc`], [`av1`]) are safe //! Rust. -mod av1; -mod bits; -mod hevc; -mod sys; +pub(crate) mod sys; use std::ffi::c_int; use std::fs::File; @@ -50,6 +47,7 @@ use crate::{ CodecConfig, ColorRange, DecodedFrame, HwBackend, HwCodec, HwDecodeError, HwStillDecoder, PixelFormat, Plane, StillDecodeRequest, }; +use crate::bitstream::{av1, bits, hevc}; // ── Public backend surface (called from lib.rs) ───────────────────────────── From e86a5323d2b942876ecf9b4abbd28b20b1ef5250 Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Wed, 5 Aug 2026 16:55:58 -0400 Subject: [PATCH 2/6] feat(hwdec): implement Android MediaCodec decode --- CHANGELOG.md | 5 + Cargo.lock | 259 ++++- android/mediacodec-harness/fixtures/README.md | 38 + .../fixtures/data/av1-10.rshw | Bin 0 -> 14306 bytes .../fixtures/data/av1-8.rshw | Bin 0 -> 7679 bytes .../fixtures/data/hevc-10.rshw | Bin 0 -> 19680 bytes .../fixtures/data/hevc-8.rshw | Bin 0 -> 11253 bytes .../mediacodec-harness/fixtures/package.rs | 197 ++++ crates/rawshift-hwdec/Cargo.toml | 12 +- crates/rawshift-hwdec/src/bitstream/av1.rs | 2 + crates/rawshift-hwdec/src/bitstream/bits.rs | 1 + crates/rawshift-hwdec/src/bitstream/hevc.rs | 4 + crates/rawshift-hwdec/src/lib.rs | 82 +- crates/rawshift-hwdec/src/mediacodec/core.rs | 644 ++++++++++++ crates/rawshift-hwdec/src/mediacodec/mod.rs | 9 + .../rawshift-hwdec/src/mediacodec/platform.rs | 926 ++++++++++++++++++ crates/rawshift-hwdec/src/vaapi/mod.rs | 2 +- crates/rawshift-image/src/lib.rs | 3 + crates/rawshift-image/src/prelude.rs | 3 + 19 files changed, 2158 insertions(+), 29 deletions(-) create mode 100644 android/mediacodec-harness/fixtures/README.md create mode 100644 android/mediacodec-harness/fixtures/data/av1-10.rshw create mode 100644 android/mediacodec-harness/fixtures/data/av1-8.rshw create mode 100644 android/mediacodec-harness/fixtures/data/hevc-10.rshw create mode 100644 android/mediacodec-harness/fixtures/data/hevc-8.rshw create mode 100644 android/mediacodec-harness/fixtures/package.rs create mode 100644 crates/rawshift-hwdec/src/mediacodec/core.rs create mode 100644 crates/rawshift-hwdec/src/mediacodec/mod.rs create mode 100644 crates/rawshift-hwdec/src/mediacodec/platform.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8728a8b..290e593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,11 @@ All entries below are **breaking**, grouped by area. #### Per-codec migrations (`rawshift-image`) +- **Android hardware decode** now implements the fixed MediaCodec backend for + HEVC/HEIC and AV1/AVIF on API 29+. Applications initialize it once with the + process `JavaVM`; hardware, non-alias components are runtime-probed. Output + is dense I420, or strict P010 on API 33+ with no silent downconversion. + - **JPEG** → gamut-jpeg (pure Rust, baseline + progressive both ways). Decode replaces `zune-jpeg` (CMYK/YCCK conversion bit-identical); encode replaces `jpeg-encoder` **and** the vendored jpegli stack with one diff --git a/Cargo.lock b/Cargo.lock index f33e310..8a69de8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,6 +264,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "core_maths" version = "0.1.1" @@ -381,6 +391,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -539,7 +555,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b17c79c8672b675d538dceffda1dd5d00ba279cbcca4f75c7b4fa847741d5d31" dependencies = [ - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -575,7 +591,7 @@ checksum = "c9ce8735a1fc71d77b5361f4f126676abab8811cdd3797a6bacdb920cd20f85d" dependencies = [ "gamut-core", "gamut-ifd", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -598,7 +614,7 @@ checksum = "09916f0aea3f9dbe1c5ac67e0e29956b9af43c8c86a0fbe6b47c597b63fd8cee" dependencies = [ "gamut-core", "md-5", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -618,7 +634,7 @@ checksum = "99b7bb8412adf69f48657d207ff5358750ebe4efe81c2dd7b32091034f354d6b" dependencies = [ "gamut-core", "gamut-xmp", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -684,7 +700,7 @@ dependencies = [ "gamut-icc", "gamut-iptc", "gamut-xmp", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -730,7 +746,7 @@ checksum = "fd39a4ebd0d14800751e3ae64a0c86a07e00b77f3ac564570e0e8987d41945bb" dependencies = [ "gamut-core", "quick-xml", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -764,6 +780,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" @@ -798,6 +820,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "is-terminal" version = "0.4.17" @@ -830,6 +862,64 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "jpegxl-src" version = "0.12.0" @@ -862,7 +952,7 @@ dependencies = [ "jxl_transforms", "num-derive", "num-traits", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1001,6 +1091,29 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1030,6 +1143,28 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1124,6 +1259,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -1201,8 +1345,11 @@ name = "rawshift-hwdec" version = "0.1.1" dependencies = [ "gamut-color", + "jni", "libloading", - "thiserror", + "ndk", + "thiserror 2.0.18", + "tracing", ] [[package]] @@ -1255,7 +1402,7 @@ dependencies = [ "resvg", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "tiff", "tokio", "tracing", @@ -1297,7 +1444,7 @@ dependencies = [ "rawshift-core", "rawshift-hwdec", "serde", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1578,6 +1725,15 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1617,6 +1773,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -1691,6 +1853,22 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simplecss" version = "0.2.2" @@ -1767,13 +1945,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1889,6 +2087,36 @@ dependencies = [ "syn", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + [[package]] name = "tracing" version = "0.1.44" @@ -2153,6 +2381,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "xmlwriter" version = "0.1.0" diff --git a/android/mediacodec-harness/fixtures/README.md b/android/mediacodec-harness/fixtures/README.md new file mode 100644 index 0000000..94e01d2 --- /dev/null +++ b/android/mediacodec-harness/fixtures/README.md @@ -0,0 +1,38 @@ +# Fixture provenance + +The four `.rshw` files are 64×64, single-frame, lossless test vectors generated +with FFmpeg 8.1.2 from its deterministic `testsrc2` source. They contain the +container-style codec configuration, coded item payload, and expected dense +decoded pixels. The expected layouts are I420 for 8-bit and MSB-aligned, +interleaved P010 for 10-bit. + +Regeneration on Linux or macOS requires FFmpeg with `libx265` and `libaom-av1`: + +```sh +tmp=$(mktemp -d) +ffmpeg -f lavfi -i 'testsrc2=size=64x64:rate=1' -frames:v 1 -pix_fmt yuv420p \ + -c:v libx265 -preset ultrafast \ + -x265-params 'lossless=1:keyint=1:min-keyint=1:repeat-headers=1:annexb=1' \ + -f hevc "$tmp/hevc8.hevc" +ffmpeg -f lavfi -i 'testsrc2=size=64x64:rate=1' -frames:v 1 -pix_fmt yuv420p10le \ + -c:v libx265 -preset ultrafast \ + -x265-params 'lossless=1:keyint=1:min-keyint=1:repeat-headers=1:annexb=1' \ + -f hevc "$tmp/hevc10.hevc" +ffmpeg -f lavfi -i 'testsrc2=size=64x64:rate=1' -frames:v 1 -pix_fmt yuv420p \ + -c:v libaom-av1 -cpu-used 8 -still-picture 1 -crf 0 -b:v 0 -f obu "$tmp/av1-8.obu" +ffmpeg -f lavfi -i 'testsrc2=size=64x64:rate=1' -frames:v 1 -pix_fmt yuv420p10le \ + -c:v libaom-av1 -cpu-used 8 -still-picture 1 -crf 0 -b:v 0 -f obu "$tmp/av1-10.obu" +ffmpeg -f lavfi -i 'testsrc2=size=64x64:rate=1' -frames:v 1 -pix_fmt yuv420p \ + -f rawvideo "$tmp/yuv8.raw" +ffmpeg -f lavfi -i 'testsrc2=size=64x64:rate=1' -frames:v 1 -pix_fmt yuv420p10le \ + -f rawvideo "$tmp/yuv10.raw" +rustc --edition 2024 package.rs -o "$tmp/package" +"$tmp/package" hevc 8 "$tmp/hevc8.hevc" "$tmp/yuv8.raw" data/hevc-8.rshw +"$tmp/package" hevc 10 "$tmp/hevc10.hevc" "$tmp/yuv10.raw" data/hevc-10.rshw +"$tmp/package" av1 8 "$tmp/av1-8.obu" "$tmp/yuv8.raw" data/av1-8.rshw +"$tmp/package" av1 10 "$tmp/av1-10.obu" "$tmp/yuv10.raw" data/av1-10.rshw +``` + +The host unit tests parse all four packages and exercise codec preparation plus +stride/crop normalization. The physical-device harness uses the same bytes and +compares every decoded output byte with the embedded expected frame. diff --git a/android/mediacodec-harness/fixtures/data/av1-10.rshw b/android/mediacodec-harness/fixtures/data/av1-10.rshw new file mode 100644 index 0000000000000000000000000000000000000000..340055f7d81400060ffec3b4c290dfaef2769b2c GIT binary patch literal 14306 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 literal 0 HcmV?d00001 diff --git a/android/mediacodec-harness/fixtures/data/hevc-8.rshw b/android/mediacodec-harness/fixtures/data/hevc-8.rshw new file mode 100644 index 0000000000000000000000000000000000000000..fd88d1fb73efca0a0f0e8c8c61614a733263c803 GIT binary patch literal 11253 zcmeHrc|25o`1Y}62_=d|_JojV>{%ibvW0BfY3%zFnZekz4Q4D8N|ulat->U0iy~2! zLfNx~P@MN1Ql6gbeS4pNf4ra1dws^4G1s~7>%Q;r{XKJL^z@G!iHeFIAfZwO6ahy7 z06Z9>!%hGofu1&EdSL+oK(ZW&U{)ldB>_QlDgYn@vJ2rO0Ny55yZ~`~50jAmlgQx= z*@PPr5Ut*n!wH|ddf>VUolBuYQs)uP004|G{3InzZ;s7Rf*_dfj7^u2y3+5~K33hd z=h6$E7IC>*raCp*N4?|K`;ZzRHT9MaA9flT=%YDv`o5^WHGf%DR8$-&OU05^PQ%8#uz`R%qpRK56Sw?s0rsF5!!yDP_}JUYh2> zylTr}34B**bu{I^{L=EYU%=TbT*5O!_&p3}*dvD1>~EQfF-f7}F)28E!gN$W@}4`! z-rDx<;N%uHS0109WI;5o=)K7YbCq4obPdtquc0 zYG1m(_pxY7K;a3n|8S!rM##=iJpugk)EK+zP`dKjyHaFld)Wh~i9})@FVsf#dr-WC z(c@ZNT--(LRp@x5;&<)`za);gXYmE|wFGl}p3Y$&<}C@fr$T`P)Fa2c3olp9kw3mC zmde~P483*oDnT+Eb>hprYTe{pmS|N@p;ysntS+PdW_Dd3FMMA4nO}JuX1R4{sBLg@ zI<~g1e@c#gfkjU>$2_$xfYe3INiTCw$Sw5_j-sauP(E&-;l{Fg<(>1hp-XWZ75e3Z zX(T#d-rifdbD~S^`g`XWZr-l4ce7aUZK3j+b>S$I9AuUaRr_L;y6@X4*VTzQ^FjcD8s@ltL+I)&!Y4oyYGWUD&$`|=!)k=w=Ic-f7nI%kQbU8%*ml!ifdT z6DTY5!ka4nS4OvS36vSfOiOqTmK5kW;>I{n#ttfu*V#oM81U<3*)-oB|B=}=AiSY# zzR~X9?wkqqsQU%AYpfE}XtoIbT1D|>p+_YnjxngwYO6iy!8B%pR`ROoNN~CH;2|Rs zkuio?HcV=d621J^6?||9HoM-tN^FshUYaxHq|6u-;@*8k3vDw`?kCmRJniOFoAb(Q zC9iciJx8^-rOIggvArN986a9fP$8nwy0ORQruRrk!EwSH6!^k=)c<46=c#SnD$xwQ zk_F)6yAM+N(gA-IM&EDkfK0FV~a~kD!i6Bb6%|4Rd6LG#Yyre-4011wJ%CsTwEO2z?D3m zkHnO8G252%w`&NzgIcj!)|8&bQ0wI#bK?LjF!aq%7fIf`*%vYY7%GM+S5o>_A{!d)O+6E3Kr$_g&jVc zW0@#_=5c8anIOf1^JPlM0Mcj#mukV`8^>pi(czw*)%Tpl)23q69Jwo;Esjs)OJ8TF za$qn4R0wi_G!SIrVy7^=qDeR7dr-K$t(IVvY_YXX);T!OvGMrKwi($&XD%+GspwZf zTOOT>EcP(E9@%N%<5HCezh!O-0KzEnbA^z8aOlcgrLuaOIrQo$5E($iOI^EkR*tmW z{6U0Br(ctEc16Kwt z^l=Yuf}fkZT<;bG!EL}C{T26jr;6kpi;7|f%gA24_r91JOH5Gjqa;oRvG;QVyQ<`h zl=CVV$cUzfZHfRXYZj_kkQwZ^0~ep}W#|SBY!FO(%;318r+EAR&t_tZ3|Mvcz2Ne8 zb{pR%{?eq&`N;f?-0hZ>!mLI7LwAMmuhyZDrY3Nv3HOB#x9X)31kJJU_MoX>WQ>e1 zUj86?QrsrWM_9wUJ2wP;9?e^5Z*N!C2WD01y-85fj?+hdu4x0`jhiM-FMR(C7*OAq0bc zOZVs8p0USpH>z#B@4Yz{q*}6;@v+H z;|Avs3OgCI$ba0eSJl~Vyo>L)o*wQgFLi_NEy}i9H2ROwz^6H^jMRImbht@%U zaHTml;dEs?e~^RpLp4mi(MtYN{rlOhh-xE2VTF>&@M3l!pJUo!YoDU4*LkWT>Bk-) z!dgT8QkXdhwr$(Sb%Xr!mg$2C{xk}b>Ne3!c>>+2m5<(SscMIHFSGEmr5tzRI5nD7 zba6X7V}L-ZOa4wzP5l`pM{=?v2r?)bOF#A&Ck#mw3uwNi1TYV1Q#+r2aa|(L=8&p? zqeN>?9xs*Ea6fmTYrVv$>b|>OKKIUgGPPzYw`UN#0AP1ug#Z4oAsy0l^QuASJDx9l zhS@NBJKG?{4C2PZ%&}IwRS2yPV9L0_F?JHiTO#D8xMk@ zNBO8_QuTB6p3JOdr@XE{8Y|}JMv^xaTp$?viB5-xoBHbT^=M`X)!BWQ^QK-c(!4*i z%{C+^hjgI+Rrj$&@zQkAEt$s^3X4HTi>EHz;^%0+{5dmF1EhfCOH)ErVU#xj(2xTp z93Tj7#Ld(<1Ovhyk{D0NY%&^F>D5ZTc=Ykfjn5eFcZGY37F%di3zT}nXXg#(!WKT&W({Apx#7wq$OWRI>4>u;V9Pd4J z5d;`7DIA{~)%Y+Jkf9(E&Y4N2d1l`{_XAI%Y7-0d1dg$1=ToydsiLf^L;=PWH%EkC zYjIe>rn|+ozBadfm$uY_?oL!p%MDs|JZH0oXIue-Ekoy}F3p!!YFx(pZ{$5MUiL$i z4Jdz(_rJK=3WH>ot1r$utZ>)0Z+Yv`)>dFa5#ShH$*eK*MwWIWwWs_$-`m|}!S(SU zXwAK0(>1VcYk_gTL@`oV-DiP`T3VvDOfnP5>bN3n^1M`sltpG>s}wgvCxvdD^a*bo z*VUoX#XCs_Iu_on4ci)d@o6-nN`VJWCNIA;EmV@{x%mtGM8Y4!ALRIE7BFXw#6#3-%m z7cF1!m1dtnCTMT*Q=KbN?*gwjY3Lzz@ zqUW@h%`8>Y%_pjjD^177I-D!W61DfO1#$7apYix~oiY?US#I5cM~yes)^v{tNxXoh zbto_$R~=b9)RrwDv1JWBWh-7omig|`$&qc~Vk6zblu$VQqczwZx0sbnuTo(VSExxO6UJryVkwR`kAA=h&V|aJIZBaI$u@ z_vMFepC8;fTWzL}urQdj&}Y{K!8ar$C6kF7vowm8G$fYB^cXIx!}K4>-=Zc1Ul-5K z*}S{uYKnW`UXV=>kc1eq7fS@8Un%LF*s;mj*Bo#60y#9Z_!}5UoNB}^5EL|9r&OF&`f&^a>~|`c=g*A$XkTHQ;8x!zxa9d zDwCVzQq9%kF_otWwHTp;#sZ*h?RWn)^*-VoVUh*9yMwMhE?4R{MT)#U$XWUbuT2wD zNXDQ`x`&D^Z5QX+YMTjm`P>qc>k1!&oNiyUwRA1nxvw{f{q|T-9qHRca+@ok_2<3D zYo>d9w+Jrc@>rjSje9GokmPL9j?izlCW;g_NP=JysNcKWL7h}Fmzs1v5iKf~ris46 zWR~Q~h*Jy47jtU1Ck;<>Ov*NjziV{srTW+=@A>2z`y5-|9L|XqO-`V&NVq9@eTO^T z&%&!Z=xyh`Y_9X*cCosvZsoOS;G2Ft>~E_?PS5GyUSaK@GSQ zU^j>#bvVA=U(ngg*x}y0(M;Or50(ATaA&j8J5)n8_v_H_K$aGpmmk+Y3ofZkP|g_e z>*%=in^TGiC1=-M8!?4$Xg++#OEK$tJ~<(zJa$?%^i8T7^v)IPc|hGN6{)#n{QND;%nypLRn7i(SdRWL$Qp?tRSK z=F5pE8*-;v-Q?1h5GA7H+@VLK z&PnMr!!%9kAQ${fR;FJ4EPH}*1+OY{e!NIaPath9r*F|n-vWK2MRu;kk9l;+f!%)&s>=p3RH|lfmQuwvx)u;)<-Qee=+W((B=TnP43M* z(Z*>^;sT)*C>6iU4@ptHUW9|5S`<4Q_fXqwa3t%I?s?W6%X%6xV8P=U-(W?96OwXy z6(8BodAWGrma$Feu{5l_eZDSf!9b)fvVPV-dqPNg--893*}|&(!k_2qT2iPNWSXpJ zPXt`>r_?MeD%oM-=)Mu|HY+D9r%tj~~lE-L4)^EZ2I=) zx6eFic}m)1)Y2|S`&gG{w~80_jo~GsFGM*7uv0v{ajc_A0aqktaMNE>%8Xv1xC!nhLYU?FBdrG;OQ0 zND1o$o5BZng;S%?;v5lPJW;v(?;5@{qe~c>wSJ!qg0pTqDZooRaC=6hUERc8|K0E0 z*e;f&ybi8)jOo~mXmWO@3m6r!Z;fsPK_{2pY~bgR0@dK?%hTti4GmK57*1y?={c1? ztKvVE5q{yc$(E{{(h)6>I40EQQyVnPA^(-%sQ(`9jX09sLAJ$Sn zl z;A{J^SVc$*%UA!t>0$BLKV4luD4Lj<6chl9m64H@gaar-O2yT~y7$+>SS%D|-5Y-X zOL{DJ%^!<}{Kdr9@`sXuGKcem4r_%$%~|z_tl{@1uwVWEtNtJLi-|$Mf*<&4{qW*I znXL8xSM@`BEcWO6)z$e?F9enRb^T-V+%RsCuLa1iJZu3rcW@O^#| zupY`E(nA5@rC(da)zrTp;P=;RSkuGn56>UG{u}iOgoFgFhp|x)YuE7K_3+OBoxiB4 zpx}=gth>YW_c!!#j9=9c`}6X$vokUA@vZmZNB_T}hp#_B>;Jm{P6+Ee_-FoqLl4*Y z}T5;raW~pNor^_jmq(Ll0kn;QjL}e?GqT9M+}CJ|G%MERD}2a+Wf=w2d_Wef4F|wpNs3)^~3&uLl5tLc>n$1 z`u`0*9OJ+E|4t9zd9C5w?N`#b+pqQww_gH|j`H%yju{$STO*OSwiXtqriO;=TKM_D z>Vv-`=_mdw>QTPIF@dp|sDzjZ91`<0|KI5YwG6z&BZB>${0Jdu9RnTzRDZ&N;2G^j zi1cxdN^$jcMg6H>!%kW@EyOt*73Lh~nd%<$r+N$ZV-Y?cSp1oQ(DclJtgNDfdKMSDdDrUk|1T>ewNgRHB4Xi}7Wawvh|gopiM{B!+(=kF|sbVQx=^NdY# zPYz1BaOqF=+VV&cbpMEu=z#NuYx<(1jq~?Af31`1ArUD5sPNQ?1cGxuRR0?O zPxS^S5jv@ESZ`l^1UeFvo&Tr$V8OE*1bjwld{S&ugm?ZQ=YJk=W*Hvjgbl}eMfjY# zl=G+lM=?eYzW$NeAg?nqnUT5Kf2vnTxgO07a!SMpM7p5e&R_Uby`-69GS0~po9PxD z8R_+Pe4rp3`~Sbceyo21TzCKf`v2Ga_X>iE$?E+SJ&dVUw8F@T@d}T}`}w)Tu!TWb z1^oR18w~^Ik-z0pT8E8|m6a`F{YfS5urO`yFjyXV?3lOrF%vI@GZKMtKq6cCY>zE*)PipQh#e-WQpm|7ki zbv_6h@1Od&5Hqq9a9N4){QoS!nxg3Bgp{0wU-1c*MQK@=GDCjF*OsN_;`6U;;Q!U1 zZ~Je}|9_5$bDWyuh5^qcc6Fz_zJ{^79{JD=5eP^atD7Qd70{-~i3u@8VvrvZnVX9tVuGO8UPP~q5wDFWf2Jkn+Tgms=k*r-Rkc=!h4H|nA5 z1@e1*m_LL1);}md%p;NO{%brY`fGo`@kr!ae(QWf%6I?u{=@ly=hM=lGdAeq_4_BE zp7A4(3EHSn&&d2ikAaGZ^T$Ldq@<<&oDT}x$gh5${A<3F^6xz9>wiBovG#cb@Bi=f zrzPbdPmpu3{3UBtrP40CrT`d`G?mUk{N(mBfn4fs#}PiXsvSh|HU x!W-bH`6-?_ZGpu6k{40U@IuF)+3`!gidU>hVh-&8HU7W)^KJjF`Tx)H{|oSnA$|Y= literal 0 HcmV?d00001 diff --git a/android/mediacodec-harness/fixtures/package.rs b/android/mediacodec-harness/fixtures/package.rs new file mode 100644 index 0000000..55dc734 --- /dev/null +++ b/android/mediacodec-harness/fixtures/package.rs @@ -0,0 +1,197 @@ +//! Packages ffmpeg elementary streams and their source YUV into `.rshw` +//! fixtures consumed by the host tests and Android device harness. +//! +//! This deliberately uses only `std` so fixture regeneration does not alter +//! the workspace dependency graph. See `README.md` for exact commands. + +use std::env; +use std::fs; +use std::path::Path; + +const WIDTH: u16 = 64; +const HEIGHT: u16 = 64; + +fn main() { + let args: Vec<_> = env::args().collect(); + assert_eq!( + args.len(), + 6, + "package <8|10> " + ); + let codec = &args[1]; + let depth: u8 = args[2].parse().expect("bit depth"); + let stream = fs::read(&args[3]).expect("stream"); + let source = fs::read(&args[4]).expect("source YUV"); + let (codec_id, config, payload) = match codec.as_str() { + "hevc" => { + let (config, payload) = package_hevc(&stream, depth); + (1, config, payload) + } + "av1" => { + let (config, payload) = package_av1(&stream, depth); + (2, config, payload) + } + _ => panic!("unknown codec"), + }; + let expected = expected_frame(depth, &source); + let mut output = b"RSHW0001".to_vec(); + output.extend_from_slice(&[codec_id, depth]); + output.extend_from_slice(&WIDTH.to_le_bytes()); + output.extend_from_slice(&HEIGHT.to_le_bytes()); + output.extend_from_slice(&(config.len() as u32).to_le_bytes()); + output.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + output.extend_from_slice(&(expected.len() as u32).to_le_bytes()); + output.extend_from_slice(&config); + output.extend_from_slice(&payload); + output.extend_from_slice(&expected); + fs::write(Path::new(&args[5]), output).expect("fixture output"); +} + +fn package_hevc(stream: &[u8], depth: u8) -> (Vec, Vec) { + let units = annex_b_units(stream); + let mut parameter_sets = Vec::new(); + let mut payload = Vec::new(); + for unit in units { + let kind = (unit[0] >> 1) & 0x3f; + if matches!(kind, 32..=34) { + parameter_sets.push((kind, unit)); + } else if kind < 32 { + payload.extend_from_slice(&(unit.len() as u32).to_be_bytes()); + payload.extend_from_slice(unit); + } + } + for required in 32..=34 { + assert!(parameter_sets.iter().any(|(kind, _)| *kind == required)); + } + assert!(!payload.is_empty(), "no coded HEVC slice"); + + let mut hvcc = vec![0; 23]; + hvcc[0] = 1; + hvcc[16] = 0xf1; // reserved bits + 4:2:0 chroma_format_idc + hvcc[17] = 0xf8 | depth.saturating_sub(8); + hvcc[18] = hvcc[17]; + hvcc[21] = 0xff; // four-byte NAL length fields + hvcc[22] = 3; + for kind in 32..=34 { + let matching: Vec<_> = parameter_sets + .iter() + .filter(|(found, _)| *found == kind) + .map(|(_, bytes)| *bytes) + .collect(); + hvcc.push(0x80 | kind); + hvcc.extend_from_slice(&(matching.len() as u16).to_be_bytes()); + for unit in matching { + hvcc.extend_from_slice(&(unit.len() as u16).to_be_bytes()); + hvcc.extend_from_slice(unit); + } + } + (hvcc, payload) +} + +fn annex_b_units(stream: &[u8]) -> Vec<&[u8]> { + fn start_code(data: &[u8], from: usize) -> Option<(usize, usize)> { + (from..data.len().saturating_sub(2)).find_map(|index| { + if data[index..].starts_with(&[0, 0, 0, 1]) { + Some((index, 4)) + } else if data[index..].starts_with(&[0, 0, 1]) { + Some((index, 3)) + } else { + None + } + }) + } + let mut units = Vec::new(); + let Some((first, first_prefix)) = start_code(stream, 0) else { + return units; + }; + let mut begin = first + first_prefix; + loop { + match start_code(stream, begin) { + Some((next, next_prefix)) => { + if next > begin { + units.push(&stream[begin..next]); + } + begin = next + next_prefix; + } + None => { + if begin < stream.len() { + units.push(&stream[begin..]); + } + break; + } + } + } + units +} + +fn package_av1(stream: &[u8], depth: u8) -> (Vec, Vec) { + let mut config_obus = Vec::new(); + let mut payload = Vec::new(); + let mut offset = 0; + while offset < stream.len() { + let begin = offset; + let header = stream[offset]; + offset += 1; + assert_eq!(header & 0x80, 0, "AV1 forbidden bit"); + let kind = (header >> 3) & 0x0f; + if header & 0x04 != 0 { + offset += 1; + } + assert_ne!(header & 0x02, 0, "fixture OBU needs an explicit size"); + let (size, leb_bytes) = read_leb128(&stream[offset..]); + offset += leb_bytes; + offset += size; + assert!(offset <= stream.len(), "truncated AV1 OBU"); + if kind == 1 { + config_obus.extend_from_slice(&stream[begin..offset]); + } else { + payload.extend_from_slice(&stream[begin..offset]); + } + } + assert!(!config_obus.is_empty(), "no AV1 sequence header"); + assert!(!payload.is_empty(), "no AV1 coded frame"); + let high_bitdepth = if depth == 10 { 0x40 } else { 0 }; + // Profile 0, level 0, 4:2:0, no initial presentation delay. The sequence + // header remains authoritative, but these av1C summary bits must agree. + let mut av1c = vec![0x81, 0, high_bitdepth | 0x0c, 0]; + av1c.extend_from_slice(&config_obus); + (av1c, payload) +} + +fn read_leb128(data: &[u8]) -> (usize, usize) { + let mut value = 0usize; + for (index, byte) in data.iter().copied().take(8).enumerate() { + value |= usize::from(byte & 0x7f) << (7 * index); + if byte & 0x80 == 0 { + return (value, index + 1); + } + } + panic!("invalid leb128") +} + +fn expected_frame(depth: u8, source: &[u8]) -> Vec { + let pixels = usize::from(WIDTH) * usize::from(HEIGHT); + match depth { + 8 => { + assert_eq!(source.len(), pixels * 3 / 2); + source.to_vec() + } + 10 => { + assert_eq!(source.len(), pixels * 3); + let (y, chroma) = source.split_at(pixels * 2); + let (u, v) = chroma.split_at(pixels / 2); + let mut output = Vec::with_capacity(source.len()); + for word in y.chunks_exact(2) { + output.extend_from_slice( + &(u16::from_le_bytes([word[0], word[1]]) << 6).to_le_bytes(), + ); + } + for (u, v) in u.chunks_exact(2).zip(v.chunks_exact(2)) { + output.extend_from_slice(&(u16::from_le_bytes([u[0], u[1]]) << 6).to_le_bytes()); + output.extend_from_slice(&(u16::from_le_bytes([v[0], v[1]]) << 6).to_le_bytes()); + } + output + } + _ => panic!("unsupported depth"), + } +} diff --git a/crates/rawshift-hwdec/Cargo.toml b/crates/rawshift-hwdec/Cargo.toml index c24b58e..45038ec 100644 --- a/crates/rawshift-hwdec/Cargo.toml +++ b/crates/rawshift-hwdec/Cargo.toml @@ -30,6 +30,11 @@ thiserror = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] libloading = "0.8" +[target.'cfg(target_os = "android")'.dependencies] +jni = "0.22.4" +ndk = { version = "0.9", default-features = false, features = ["media", "api-level-29"] } +tracing = { workspace = true } + [features] # Verified backend feature flags — see docs/SUPPORT.md for the permanent # target/API matrix. Each explicit backend flag hard-fails the compile @@ -39,9 +44,10 @@ libloading = "0.8" # (windows-msvc, linux-musl, wasm) emits a build-script warning and compiles # the no-backend stub. # -# Backends implemented: VAAPI (linux-gnu; dlopen'd libva, HEVC Main/Main10 + -# AV1 Profile 0 still pictures). VideoToolbox / MediaCodec land as separate -# issues; builds without a selected backend compile the no-backend stub — +# Backends implemented: VAAPI (linux-gnu; dlopen'd libva) and MediaCodec +# (Android API 29+, explicitly initialized through JNI), both for HEVC +# Main/Main10 + AV1 Profile 0 still pictures. VideoToolbox lands separately; +# builds without a selected backend compile the no-backend stub — # `decoder()` returns `None`, `backend()` returns `None`, and # `available_codecs()` is empty. videotoolbox = [] diff --git a/crates/rawshift-hwdec/src/bitstream/av1.rs b/crates/rawshift-hwdec/src/bitstream/av1.rs index eac66e0..89ea799 100644 --- a/crates/rawshift-hwdec/src/bitstream/av1.rs +++ b/crates/rawshift-hwdec/src/bitstream/av1.rs @@ -1,3 +1,4 @@ +#![cfg_attr(not(hwdec_backend = "vaapi"), allow(dead_code))] //! AV1 still-picture parsing shared by hardware backends — **safe Rust only** //! (no `unsafe`; platform FFI stays in backend modules). //! @@ -1520,6 +1521,7 @@ mod tests { } #[test] + #[cfg(hwdec_backend = "vaapi")] fn pic_param_maps_seq_and_frame_fields() { let pic = parse_still_picture(&[], AV1_64X64_LIBAOM).unwrap(); let p = build_pic_param(&pic, 5, sys::VA_INVALID_SURFACE).unwrap(); diff --git a/crates/rawshift-hwdec/src/bitstream/bits.rs b/crates/rawshift-hwdec/src/bitstream/bits.rs index 3f328d8..fb67c6b 100644 --- a/crates/rawshift-hwdec/src/bitstream/bits.rs +++ b/crates/rawshift-hwdec/src/bitstream/bits.rs @@ -1,3 +1,4 @@ +#![cfg_attr(not(hwdec_backend = "vaapi"), allow(dead_code))] //! Safe bitstream readers shared by the HEVC and AV1 still-picture parsers. //! //! **Safe Rust only** — this module (like `hevc.rs` / `av1.rs`) contains no diff --git a/crates/rawshift-hwdec/src/bitstream/hevc.rs b/crates/rawshift-hwdec/src/bitstream/hevc.rs index 7ea38ec..97c1d66 100644 --- a/crates/rawshift-hwdec/src/bitstream/hevc.rs +++ b/crates/rawshift-hwdec/src/bitstream/hevc.rs @@ -1,3 +1,4 @@ +#![cfg_attr(not(hwdec_backend = "vaapi"), allow(dead_code))] //! HEVC still-picture header parsing shared by hardware backends — **safe //! Rust only** (no `unsafe`; platform FFI stays in backend modules). //! @@ -1024,6 +1025,7 @@ mod tests { } #[test] + #[cfg(hwdec_backend = "vaapi")] fn pic_param_packs_bitfields() { let sps = parse_sps(SPS_64X64_X265).unwrap(); let pps = parse_pps(PPS_64X64_X265).unwrap(); @@ -1049,6 +1051,7 @@ mod tests { } #[test] + #[cfg(hwdec_backend = "vaapi")] fn uniform_tile_partition_covers_exactly() { // 10 CTBs into 3 columns: 3+3+4 (spec derivation gives 3,3,4). let parts = uniform_partition_minus1(10, 3); @@ -1057,6 +1060,7 @@ mod tests { } #[test] + #[cfg(hwdec_backend = "vaapi")] fn slice_param_marks_last_slice_and_i_type() { let sh = SliceHeader { slice_type: 2, diff --git a/crates/rawshift-hwdec/src/lib.rs b/crates/rawshift-hwdec/src/lib.rs index 4a0bfd7..f95ce4c 100644 --- a/crates/rawshift-hwdec/src/lib.rs +++ b/crates/rawshift-hwdec/src/lib.rs @@ -11,8 +11,9 @@ //! crate and nowhere else: `#![deny(unsafe_op_in_unsafe_fn)]`, every public //! item is safe, and every `unsafe` block documents its invariants inside the //! platform backend module that owns it. The **VAAPI backend** (linux-gnu, -//! dlopen'd libva — see the `vaapi` module) is implemented; VideoToolbox and -//! MediaCodec land as separate issues. On targets/builds with no backend +//! dlopen'd libva — see the `vaapi` module) and **MediaCodec backend** +//! (Android API 29+, explicitly initialized from the process VM) are +//! implemented; VideoToolbox lands separately. On targets/builds with no backend //! every entry point reports "no decoder" — [`decoder`] returns `None`, //! [`backend`] returns `None`, and [`available_codecs`] is empty. //! @@ -48,13 +49,12 @@ #![deny(unsafe_op_in_unsafe_fn)] -#[cfg(any( - hwdec_backend = "vaapi", - hwdec_backend = "mediacodec", - test -))] +#[cfg(any(hwdec_backend = "vaapi", hwdec_backend = "mediacodec", test))] mod bitstream; +#[cfg(any(hwdec_backend = "mediacodec", test))] +mod mediacodec; + // The VAAPI platform backend: compiled only when build.rs selected it // (`vaapi` explicit flag, or `hw` on a linux-gnu target). #[cfg(hwdec_backend = "vaapi")] @@ -94,6 +94,48 @@ use thiserror::Error; pub use gamut_color::{ChromaSubsampling, ColorRange}; +/// The process Java VM used to discover Android codecs. +/// +/// This is re-exported so Android applications do not need to align a second +/// `jni` crate version merely to initialize hardware decode. +#[cfg(hwdec_backend = "mediacodec")] +pub use jni::JavaVM as AndroidJavaVm; + +/// Errors from [`initialize_android_hw_decode`]. +#[cfg(hwdec_backend = "mediacodec")] +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum AndroidHwDecodeInitError { + /// Initialization already succeeded with another VM in this process. + #[error("Android hardware decode is already initialized with a different Java VM")] + DifferentJavaVm, + /// JNI could not enumerate the platform's hardware codecs. + #[error("Android MediaCodec initialization failed during {operation}: {message}")] + Jni { + /// The discovery operation that failed. + operation: &'static str, + /// The JNI error or Java exception summary. + message: String, + }, +} + +/// Initializes Android hardware decode from the application's process VM. +/// +/// Call this once from an Android/JNI entry point before querying +/// [`available_codecs`] or decoding HEIC/AVIF pixels. Repeating the call with +/// the same VM is harmless. A failed attempt does not latch state and may be +/// retried; a successful initialization cannot be replaced with another VM. +/// +/// # Errors +/// +/// Returns [`AndroidHwDecodeInitError::DifferentJavaVm`] if a different VM +/// was already installed, or [`AndroidHwDecodeInitError::Jni`] when codec +/// discovery fails. +#[cfg(hwdec_backend = "mediacodec")] +pub fn initialize_android_hw_decode(vm: AndroidJavaVm) -> Result<(), AndroidHwDecodeInitError> { + mediacodec::initialize(vm) +} + // ── Codec / backend identity ──────────────────────────────────────────────── /// A codec this crate can decode still frames of. @@ -486,15 +528,19 @@ pub enum HwDecodeError { /// dlopens libva on first use and answers from the driver's actual /// profile/entrypoint list; missing libraries, render nodes, or driver /// support all degrade to `None` (never a link or startup failure). -/// VideoToolbox and MediaCodec land as separate issues; without a backend -/// this returns `None` everywhere. +/// Android MediaCodec additionally requires an explicit successful +/// [`initialize_android_hw_decode`] call before this can return a decoder. #[must_use] pub fn decoder(codec: HwCodec) -> Option> { #[cfg(hwdec_backend = "vaapi")] { vaapi::decoder(codec) } - #[cfg(not(hwdec_backend = "vaapi"))] + #[cfg(hwdec_backend = "mediacodec")] + { + mediacodec::decoder(codec) + } + #[cfg(not(any(hwdec_backend = "vaapi", hwdec_backend = "mediacodec")))] { let _ = codec; None @@ -513,7 +559,11 @@ pub fn backend() -> Option { { vaapi::backend() } - #[cfg(not(hwdec_backend = "vaapi"))] + #[cfg(hwdec_backend = "mediacodec")] + { + mediacodec::backend() + } + #[cfg(not(any(hwdec_backend = "vaapi", hwdec_backend = "mediacodec")))] { None } @@ -529,7 +579,11 @@ pub fn available_codecs() -> &'static [HwCodec] { { vaapi::available_codecs() } - #[cfg(not(hwdec_backend = "vaapi"))] + #[cfg(hwdec_backend = "mediacodec")] + { + mediacodec::available_codecs() + } + #[cfg(not(any(hwdec_backend = "vaapi", hwdec_backend = "mediacodec")))] { &[] } @@ -541,14 +595,14 @@ mod tests { // ── stub behaviour (builds with no selected backend) ──────────────────── - #[cfg(not(hwdec_backend = "vaapi"))] + #[cfg(not(any(hwdec_backend = "vaapi", hwdec_backend = "mediacodec")))] #[test] fn stub_has_no_decoder_for_any_codec() { assert!(decoder(HwCodec::Hevc).is_none()); assert!(decoder(HwCodec::Av1).is_none()); } - #[cfg(not(hwdec_backend = "vaapi"))] + #[cfg(not(any(hwdec_backend = "vaapi", hwdec_backend = "mediacodec")))] #[test] fn stub_reports_no_backend_and_no_codecs() { assert_eq!(backend(), None); diff --git a/crates/rawshift-hwdec/src/mediacodec/core.rs b/crates/rawshift-hwdec/src/mediacodec/core.rs new file mode 100644 index 0000000..f9a533e --- /dev/null +++ b/crates/rawshift-hwdec/src/mediacodec/core.rs @@ -0,0 +1,644 @@ +//! Platform-independent MediaCodec request preparation and image-plane copy. + +#![cfg_attr(not(hwdec_backend = "mediacodec"), allow(dead_code))] + +use crate::bitstream::{av1, bits::ParseError, hevc}; +use crate::{ + CodecConfig, ColorRange, DecodedFrame, HwCodec, HwDecodeError, PixelFormat, Plane, + StillDecodeRequest, +}; + +pub(super) const MIME_HEVC: &str = "video/hevc"; +pub(super) const MIME_AV1: &str = "video/av01"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct SessionKey { + pub codec: HwCodec, + pub csd0: Vec, + pub sequence_header: Vec, + pub width: u32, + pub height: u32, + pub bit_depth: u8, +} + +#[derive(Debug, Clone)] +pub(super) struct PreparedRequest { + pub codec: HwCodec, + pub mime: &'static str, + pub csd0: Vec, + pub access_unit: Vec, + pub width: u32, + pub height: u32, + pub bit_depth: u8, + pub range: ColorRange, + pub key: SessionKey, +} + +fn parse_error(codec: HwCodec, error: ParseError) -> HwDecodeError { + HwDecodeError::Decode { + codec, + message: error.0.to_string(), + } +} + +pub(super) fn prepare(request: &StillDecodeRequest<'_>) -> Result { + match request.config { + CodecConfig::Hvcc(hvcc) => prepare_hevc(hvcc, request.payload), + CodecConfig::Av1c(av1c) => prepare_av1(av1c, request.payload), + } +} + +fn prepare_hevc(hvcc_bytes: &[u8], payload: &[u8]) -> Result { + const NAL_VPS: u8 = 32; + let codec = HwCodec::Hevc; + let hvcc = hevc::parse_hvcc(hvcc_bytes).map_err(|e| parse_error(codec, e))?; + let payload_nals = hevc::split_length_prefixed(payload, hvcc.nal_length_size) + .map_err(|e| parse_error(codec, e))?; + + let mut sps = None; + let mut has_pps = false; + let mut has_vps = false; + let mut has_irap = false; + let mut csd0 = Vec::new(); + for nal in hvcc + .nal_units + .iter() + .map(Vec::as_slice) + .chain(payload_nals.iter().copied()) + { + let kind = hevc::nal_type(nal).map_err(|e| parse_error(codec, e))?; + match kind { + NAL_VPS => has_vps = true, + hevc::NAL_SPS => { + sps = Some(hevc::parse_sps(nal).map_err(|e| parse_error(codec, e))?); + } + hevc::NAL_PPS => { + hevc::parse_pps(nal).map_err(|e| parse_error(codec, e))?; + has_pps = true; + } + kind if hevc::is_irap(kind) => has_irap = true, + kind if kind < 32 => { + return Err(HwDecodeError::Decode { + codec, + message: "HEVC non-IRAP coded slice is outside the still-picture scope" + .to_string(), + }); + } + _ => {} + } + if matches!(kind, NAL_VPS | hevc::NAL_SPS | hevc::NAL_PPS) { + csd0.extend_from_slice(&[0, 0, 0, 1]); + csd0.extend_from_slice(nal); + } + } + let sps = sps.ok_or_else(|| HwDecodeError::Decode { + codec, + message: "HEVC stream carries no SPS".to_string(), + })?; + if !has_vps || !has_pps || !has_irap { + let missing = if !has_vps { + "VPS" + } else if !has_pps { + "PPS" + } else { + "IRAP coded slice" + }; + return Err(HwDecodeError::Decode { + codec, + message: format!("HEVC stream carries no {missing}"), + }); + } + if sps.chroma_format_idc > 1 { + return Err(HwDecodeError::Decode { + codec, + message: "HEVC 4:2:2/4:4:4 is outside the Main/Main10 scope".to_string(), + }); + } + let bit_depth = sps.bit_depth_luma.max(sps.bit_depth_chroma); + if !matches!(bit_depth, 8..=10) { + return Err(HwDecodeError::Decode { + codec, + message: "HEVC bit depth is outside the Main/Main10 scope".to_string(), + }); + } + let (width, height) = sps.cropped_size(); + if width == 0 || height == 0 { + return Err(HwDecodeError::Decode { + codec, + message: "HEVC conformance window crops away the frame".to_string(), + }); + } + let mut access_unit = Vec::with_capacity(payload.len() + payload_nals.len() * 4); + for nal in payload_nals { + access_unit.extend_from_slice(&[0, 0, 0, 1]); + access_unit.extend_from_slice(nal); + } + let range = if sps.video_full_range { + ColorRange::Full + } else { + ColorRange::Limited + }; + let key = SessionKey { + codec, + csd0: csd0.clone(), + sequence_header: csd0.clone(), + width, + height, + bit_depth, + }; + Ok(PreparedRequest { + codec, + mime: MIME_HEVC, + csd0, + access_unit, + width, + height, + bit_depth, + range, + key, + }) +} + +fn prepare_av1(av1c_bytes: &[u8], payload: &[u8]) -> Result { + let codec = HwCodec::Av1; + let av1c = av1::parse_av1c(av1c_bytes).map_err(|e| parse_error(codec, e))?; + if av1c.seq_profile != 0 { + return Err(HwDecodeError::Decode { + codec, + message: "AV1 profile 1/2 is outside the Profile 0 scope".to_string(), + }); + } + let picture = + av1::parse_still_picture(av1c.config_obus, payload).map_err(|e| parse_error(codec, e))?; + if !(picture.seq.mono_chrome || picture.seq.subsampling_x && picture.seq.subsampling_y) { + return Err(HwDecodeError::Decode { + codec, + message: "AV1 4:2:2/4:4:4 is outside the Profile 0 scope".to_string(), + }); + } + if !matches!(picture.seq.bit_depth, 8 | 10) { + return Err(HwDecodeError::Decode { + codec, + message: "AV1 bit depth is outside 8/10".to_string(), + }); + } + let width = picture.fh.upscaled_width; + let height = picture.fh.frame_height; + if width == 0 || height == 0 { + return Err(HwDecodeError::Decode { + codec, + message: "AV1 frame dimensions are zero".to_string(), + }); + } + let sequence_header = av1::split_obus(av1c.config_obus) + .map_err(|e| parse_error(codec, e))? + .into_iter() + .chain(av1::split_obus(payload).map_err(|e| parse_error(codec, e))?) + .find(|obu| obu.obu_type == av1::OBU_SEQUENCE_HEADER) + .map(|obu| obu.payload.to_vec()) + .ok_or_else(|| HwDecodeError::Decode { + codec, + message: "AV1 stream carries no sequence header".to_string(), + })?; + let range = if picture.seq.color_range_full { + ColorRange::Full + } else { + ColorRange::Limited + }; + // Android's MediaCodec contract defines AV1 csd-0 as the complete + // AV1CodecConfigurationRecord (`av1C`) data. + let csd0 = av1c_bytes.to_vec(); + let key = SessionKey { + codec, + csd0: csd0.clone(), + sequence_header, + width, + height, + bit_depth: picture.seq.bit_depth, + }; + Ok(PreparedRequest { + codec, + mime: MIME_AV1, + csd0, + access_unit: payload.to_vec(), + width, + height, + bit_depth: picture.seq.bit_depth, + range, + key, + }) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ImageLayout { + Yuv420, + P010, +} + +#[derive(Debug, Clone, Copy)] +pub(super) struct PlaneView<'a> { + pub data: &'a [u8], + pub row_stride: usize, + pub pixel_stride: usize, +} + +#[derive(Debug)] +pub(super) struct ImageView<'a> { + pub layout: ImageLayout, + pub width: u32, + pub height: u32, + pub crop: (u32, u32, u32, u32), + pub planes: Vec>, +} + +fn copy_samples( + plane: PlaneView<'_>, + x: usize, + y: usize, + width: usize, + height: usize, + sample_bytes: usize, +) -> Option> { + if plane.pixel_stride < sample_bytes || plane.row_stride == 0 { + return None; + } + let capacity = width.checked_mul(height)?.checked_mul(sample_bytes)?; + let mut out = Vec::with_capacity(capacity); + for row in 0..height { + for column in 0..width { + let start = (y + row) + .checked_mul(plane.row_stride)? + .checked_add((x + column).checked_mul(plane.pixel_stride)?)?; + out.extend_from_slice(plane.data.get(start..start + sample_bytes)?); + } + } + Some(out) +} + +pub(super) fn normalize_image( + image: &ImageView<'_>, + expected_size: (u32, u32), + bit_depth: u8, + range: ColorRange, +) -> Result { + let invalid = |message: &str| HwDecodeError::InvalidFrame { + message: message.to_string(), + }; + let (left, top, right, bottom) = image.crop; + if left > right || top > bottom || right > image.width || bottom > image.height { + return Err(invalid("AImage crop rectangle is outside the image")); + } + let width = right - left; + let height = bottom - top; + if (width, height) != expected_size { + return Err(invalid( + "AImage crop dimensions differ from the coded picture", + )); + } + if left % 2 != 0 || top % 2 != 0 { + return Err(invalid("4:2:0 AImage crop origin must be even")); + } + let (w, h) = (width as usize, height as usize); + let (cw, ch) = (width.div_ceil(2) as usize, height.div_ceil(2) as usize); + match image.layout { + ImageLayout::Yuv420 => { + if bit_depth != 8 || image.planes.len() != 3 { + return Err(invalid("YUV_420_888 requires three 8-bit planes")); + } + let y = copy_samples(image.planes[0], left as usize, top as usize, w, h, 1) + .ok_or_else(|| invalid("AImage luma plane is too small"))?; + let u = copy_samples( + image.planes[1], + left as usize / 2, + top as usize / 2, + cw, + ch, + 1, + ) + .ok_or_else(|| invalid("AImage Cb plane is too small"))?; + let v = copy_samples( + image.planes[2], + left as usize / 2, + top as usize / 2, + cw, + ch, + 1, + ) + .ok_or_else(|| invalid("AImage Cr plane is too small"))?; + DecodedFrame::new( + PixelFormat::I420, + width, + height, + bit_depth, + range, + vec![ + Plane { data: y, stride: w }, + Plane { + data: u, + stride: cw, + }, + Plane { + data: v, + stride: cw, + }, + ], + ) + } + ImageLayout::P010 => { + if bit_depth <= 8 || !matches!(image.planes.len(), 2 | 3) { + return Err(invalid("P010 requires two or three high-bit-depth planes")); + } + let y = copy_samples(image.planes[0], left as usize, top as usize, w, h, 2) + .ok_or_else(|| invalid("AImage P010 luma plane is too small"))?; + let chroma = if image.planes.len() == 2 { + copy_samples( + image.planes[1], + left as usize / 2, + top as usize / 2, + cw, + ch, + 4, + ) + .ok_or_else(|| invalid("AImage P010 chroma plane is too small"))? + } else { + let u = copy_samples( + image.planes[1], + left as usize / 2, + top as usize / 2, + cw, + ch, + 2, + ) + .ok_or_else(|| invalid("AImage P010 Cb plane is too small"))?; + let v = copy_samples( + image.planes[2], + left as usize / 2, + top as usize / 2, + cw, + ch, + 2, + ) + .ok_or_else(|| invalid("AImage P010 Cr plane is too small"))?; + let mut interleaved = Vec::with_capacity(cw * ch * 4); + for (u, v) in u.chunks_exact(2).zip(v.chunks_exact(2)) { + interleaved.extend_from_slice(u); + interleaved.extend_from_slice(v); + } + interleaved + }; + DecodedFrame::new( + PixelFormat::P010, + width, + height, + bit_depth, + range, + vec![ + Plane { + data: y, + stride: w * 2, + }, + Plane { + data: chroma, + stride: cw * 4, + }, + ], + ) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const HEVC_8: &[u8] = + include_bytes!("../../../../android/mediacodec-harness/fixtures/data/hevc-8.rshw"); + const HEVC_10: &[u8] = + include_bytes!("../../../../android/mediacodec-harness/fixtures/data/hevc-10.rshw"); + const AV1_8: &[u8] = + include_bytes!("../../../../android/mediacodec-harness/fixtures/data/av1-8.rshw"); + const AV1_10: &[u8] = + include_bytes!("../../../../android/mediacodec-harness/fixtures/data/av1-10.rshw"); + + struct Fixture<'a> { + codec: HwCodec, + depth: u8, + width: u32, + height: u32, + config: &'a [u8], + payload: &'a [u8], + expected: &'a [u8], + } + + fn fixture(bytes: &[u8]) -> Fixture<'_> { + assert_eq!(&bytes[..8], b"RSHW0001"); + let codec = match bytes[8] { + 1 => HwCodec::Hevc, + 2 => HwCodec::Av1, + value => panic!("unknown fixture codec {value}"), + }; + let depth = bytes[9]; + let width = u32::from(u16::from_le_bytes(bytes[10..12].try_into().unwrap())); + let height = u32::from(u16::from_le_bytes(bytes[12..14].try_into().unwrap())); + let config_len = u32::from_le_bytes(bytes[14..18].try_into().unwrap()) as usize; + let payload_len = u32::from_le_bytes(bytes[18..22].try_into().unwrap()) as usize; + let expected_len = u32::from_le_bytes(bytes[22..26].try_into().unwrap()) as usize; + let config_end = 26 + config_len; + let payload_end = config_end + payload_len; + let expected_end = payload_end + expected_len; + assert_eq!(expected_end, bytes.len()); + Fixture { + codec, + depth, + width, + height, + config: &bytes[26..config_end], + payload: &bytes[config_end..payload_end], + expected: &bytes[payload_end..expected_end], + } + } + + fn request<'a>(fixture: &'a Fixture<'a>) -> StillDecodeRequest<'a> { + StillDecodeRequest { + config: match fixture.codec { + HwCodec::Hevc => CodecConfig::Hvcc(fixture.config), + HwCodec::Av1 => CodecConfig::Av1c(fixture.config), + }, + payload: fixture.payload, + width: fixture.width, + height: fixture.height, + bit_depth: fixture.depth, + chroma: gamut_color::ChromaSubsampling::Cs420, + } + } + + #[test] + fn checked_in_codec_fixtures_prepare_with_authoritative_geometry() { + for bytes in [HEVC_8, HEVC_10, AV1_8, AV1_10] { + let fixture = fixture(bytes); + let prepared = prepare(&request(&fixture)).expect("fixture prepares"); + assert_eq!(prepared.codec, fixture.codec); + assert_eq!(prepared.bit_depth, fixture.depth); + assert_eq!((prepared.width, prepared.height), (64, 64)); + assert!(!prepared.csd0.is_empty()); + assert!(!prepared.access_unit.is_empty()); + match fixture.codec { + HwCodec::Hevc => assert!(prepared.csd0.starts_with(&[0, 0, 0, 1])), + HwCodec::Av1 => assert_eq!(prepared.csd0, fixture.config), + } + } + } + + #[test] + fn checked_in_expected_planes_round_trip_through_normalization() { + for bytes in [HEVC_8, AV1_8] { + let fixture = fixture(bytes); + let pixels = (fixture.width * fixture.height) as usize; + let chroma = pixels / 4; + let image = ImageView { + layout: ImageLayout::Yuv420, + width: fixture.width, + height: fixture.height, + crop: (0, 0, fixture.width, fixture.height), + planes: vec![ + PlaneView { + data: &fixture.expected[..pixels], + row_stride: fixture.width as usize, + pixel_stride: 1, + }, + PlaneView { + data: &fixture.expected[pixels..pixels + chroma], + row_stride: fixture.width as usize / 2, + pixel_stride: 1, + }, + PlaneView { + data: &fixture.expected[pixels + chroma..], + row_stride: fixture.width as usize / 2, + pixel_stride: 1, + }, + ], + }; + let frame = normalize_image(&image, (64, 64), 8, ColorRange::Limited).unwrap(); + let flattened: Vec<_> = frame + .planes() + .iter() + .flat_map(|plane| plane.data.iter().copied()) + .collect(); + assert_eq!(flattened, fixture.expected); + } + + for bytes in [HEVC_10, AV1_10] { + let fixture = fixture(bytes); + let luma_bytes = (fixture.width * fixture.height * 2) as usize; + let image = ImageView { + layout: ImageLayout::P010, + width: fixture.width, + height: fixture.height, + crop: (0, 0, fixture.width, fixture.height), + planes: vec![ + PlaneView { + data: &fixture.expected[..luma_bytes], + row_stride: fixture.width as usize * 2, + pixel_stride: 2, + }, + PlaneView { + data: &fixture.expected[luma_bytes..], + row_stride: fixture.width as usize * 2, + pixel_stride: 4, + }, + ], + }; + let frame = normalize_image(&image, (64, 64), 10, ColorRange::Limited).unwrap(); + let flattened: Vec<_> = frame + .planes() + .iter() + .flat_map(|plane| plane.data.iter().copied()) + .collect(); + assert_eq!(flattened, fixture.expected); + } + } + + #[test] + fn normalizes_padded_semiplanar_yuv_to_i420() { + let y = [1, 2, 3, 4, 0, 0, 5, 6, 7, 8, 0, 0]; + let uv = [10, 20, 11, 21, 0, 0]; + let image = ImageView { + layout: ImageLayout::Yuv420, + width: 4, + height: 2, + crop: (0, 0, 4, 2), + planes: vec![ + PlaneView { + data: &y, + row_stride: 6, + pixel_stride: 1, + }, + PlaneView { + data: &uv, + row_stride: 6, + pixel_stride: 2, + }, + PlaneView { + data: &uv[1..], + row_stride: 6, + pixel_stride: 2, + }, + ], + }; + let frame = normalize_image(&image, (4, 2), 8, ColorRange::Limited).unwrap(); + assert_eq!(frame.format(), PixelFormat::I420); + assert_eq!(frame.planes()[0].data, [1, 2, 3, 4, 5, 6, 7, 8]); + assert_eq!(frame.planes()[1].data, [10, 11]); + assert_eq!(frame.planes()[2].data, [20, 21]); + } + + #[test] + fn normalizes_three_plane_p010_to_interleaved_msb_words() { + let y = [0x00, 0x04, 0x00, 0x08, 0, 0, 0x00, 0x0c, 0x00, 0x10]; + let u = [0x00, 0x14, 0, 0]; + let v = [0x00, 0x18, 0, 0]; + let image = ImageView { + layout: ImageLayout::P010, + width: 2, + height: 2, + crop: (0, 0, 2, 2), + planes: vec![ + PlaneView { + data: &y, + row_stride: 6, + pixel_stride: 2, + }, + PlaneView { + data: &u, + row_stride: 4, + pixel_stride: 4, + }, + PlaneView { + data: &v, + row_stride: 4, + pixel_stride: 4, + }, + ], + }; + let frame = normalize_image(&image, (2, 2), 10, ColorRange::Full).unwrap(); + assert_eq!(frame.format(), PixelFormat::P010); + assert_eq!(frame.planes()[1].data, [0x00, 0x14, 0x00, 0x18]); + } + + #[test] + fn rejects_mismatched_crop_and_odd_origin() { + let plane = PlaneView { + data: &[0; 64], + row_stride: 8, + pixel_stride: 1, + }; + let image = ImageView { + layout: ImageLayout::Yuv420, + width: 8, + height: 8, + crop: (1, 0, 5, 4), + planes: vec![plane; 3], + }; + assert!(normalize_image(&image, (4, 4), 8, ColorRange::Limited).is_err()); + } +} diff --git a/crates/rawshift-hwdec/src/mediacodec/mod.rs b/crates/rawshift-hwdec/src/mediacodec/mod.rs new file mode 100644 index 0000000..cc07b28 --- /dev/null +++ b/crates/rawshift-hwdec/src/mediacodec/mod.rs @@ -0,0 +1,9 @@ +//! Android MediaCodec still-frame decoder. + +mod core; + +#[cfg(hwdec_backend = "mediacodec")] +mod platform; + +#[cfg(hwdec_backend = "mediacodec")] +pub(crate) use platform::{available_codecs, backend, decoder, initialize}; diff --git a/crates/rawshift-hwdec/src/mediacodec/platform.rs b/crates/rawshift-hwdec/src/mediacodec/platform.rs new file mode 100644 index 0000000..baf465d --- /dev/null +++ b/crates/rawshift-hwdec/src/mediacodec/platform.rs @@ -0,0 +1,926 @@ +//! Android MediaCodec backend. +//! +//! Java is used only for the definitive `MediaCodecList` capability query. +//! Decode and output acquisition use the NDK `AMediaCodec`/`AImageReader` +//! APIs. All NDK wrappers stay on one worker because their raw handles are +//! intentionally not `Send`. + +use std::collections::HashSet; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::mpsc::{self, Receiver, SyncSender}; +use std::sync::{Arc, Condvar, Mutex, OnceLock}; +use std::thread::{self, JoinHandle}; +use std::time::{Duration, Instant}; + +use jni::objects::{JIntArray, JObject, JObjectArray, JString}; +use jni::{Env, JValue, JavaVM, jni_sig, jni_str}; +use ndk::hardware_buffer::HardwareBufferUsage; +use ndk::media::image_reader::{AcquireResult, Image, ImageFormat, ImageReader}; +use ndk::media::media_codec::{ + DequeuedInputBufferResult, DequeuedOutputBufferInfoResult, MediaCodec, MediaCodecDirection, +}; +use ndk::media::media_format::MediaFormat; + +use super::core::{ + ImageLayout, ImageView, MIME_AV1, MIME_HEVC, PlaneView, PreparedRequest, SessionKey, + normalize_image, prepare, +}; +use crate::{ + AndroidHwDecodeInitError, DecodedFrame, HwBackend, HwCodec, HwDecodeError, HwStillDecoder, + StillDecodeRequest, +}; + +const API_P010: i32 = 33; +const COLOR_FORMAT_P010: i32 = 54; +const COLOR_FORMAT_YUV420_FLEXIBLE: i32 = 0x7f42_0888; +const BUFFER_FLAG_CODEC_CONFIG: u32 = 2; +const BUFFER_FLAG_END_OF_STREAM: u32 = 4; +const DECODE_DEADLINE: Duration = Duration::from_secs(5); +const POLL_SLICE: Duration = Duration::from_millis(25); + +const HEVC_ONLY: &[HwCodec] = &[HwCodec::Hevc]; +const AV1_ONLY: &[HwCodec] = &[HwCodec::Av1]; +const BOTH_CODECS: &[HwCodec] = &[HwCodec::Hevc, HwCodec::Av1]; + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Candidate { + name: String, + p010: bool, +} + +#[derive(Debug)] +struct Runtime { + vm: JavaVM, + sdk: i32, + hevc: Vec, + av1: Vec, +} + +#[derive(Debug, Clone)] +struct RuntimeSnapshot { + sdk: i32, + candidates: Vec, +} + +static RUNTIME: OnceLock>> = OnceLock::new(); + +fn runtime() -> &'static Mutex> { + RUNTIME.get_or_init(|| Mutex::new(None)) +} + +fn lock_runtime() -> std::sync::MutexGuard<'static, Option> { + runtime() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +pub(crate) fn initialize(vm: JavaVM) -> Result<(), AndroidHwDecodeInitError> { + let mut state = lock_runtime(); + if let Some(current) = state.as_ref() { + return if current.vm.get_raw() == vm.get_raw() { + Ok(()) + } else { + Err(AndroidHwDecodeInitError::DifferentJavaVm) + }; + } + + // Do not publish partial state. JNI/Java failures leave the slot empty so + // applications can retry after their runtime is fully initialized. + let discovery = vm.attach_current_thread(discover_codecs).map_err(|error| { + AndroidHwDecodeInitError::Jni { + operation: "MediaCodecList enumeration", + message: error.to_string(), + } + })?; + *state = Some(Runtime { + vm, + sdk: discovery.sdk, + hevc: discovery.hevc, + av1: discovery.av1, + }); + Ok(()) +} + +#[derive(Debug)] +struct Discovery { + sdk: i32, + hevc: Vec, + av1: Vec, +} + +fn discover_codecs(env: &mut Env<'_>) -> jni::errors::Result { + let sdk = env + .get_static_field( + jni_str!("android/os/Build$VERSION"), + jni_str!("SDK_INT"), + jni_sig!(int), + )? + .i()?; + let list = env.new_object( + jni_str!("android/media/MediaCodecList"), + jni_sig!((int) -> void), + &[JValue::Int(1)], // MediaCodecList.ALL_CODECS + )?; + let infos = env + .call_method( + &list, + jni_str!("getCodecInfos"), + jni_sig!(() -> [android.media.MediaCodecInfo]), + &[], + )? + .l()?; + let infos: JObjectArray<'_, JObject<'_>> = JObjectArray::::cast_local(env, infos)?; + + let mut hevc = Vec::new(); + let mut av1 = Vec::new(); + for index in 0..infos.len(env)? { + match env.with_local_frame(32, |env| { + let info = infos.get_element(env, index)?; + inspect_codec(env, &info) + }) { + Ok(found) => { + for (mime, candidate) in found { + match mime { + MIME_HEVC => hevc.push(candidate), + MIME_AV1 => av1.push(candidate), + _ => unreachable!("only requested MIME types are returned"), + } + } + } + Err(error) => tracing::debug!( + target: "rawshift_hwdec::mediacodec", + codec_index = index, + %error, + "skipping MediaCodecInfo after capability-query failure" + ), + } + } + Ok(Discovery { + sdk, + hevc: deduplicate(hevc), + av1: deduplicate(av1), + }) +} + +fn inspect_codec<'local>( + env: &mut Env<'local>, + info: &JObject<'_>, +) -> jni::errors::Result> { + if env + .call_method(info, jni_str!("isEncoder"), jni_sig!(() -> boolean), &[])? + .z()? + || env + .call_method(info, jni_str!("isAlias"), jni_sig!(() -> boolean), &[])? + .z()? + || !env + .call_method( + info, + jni_str!("isHardwareAccelerated"), + jni_sig!(() -> boolean), + &[], + )? + .z()? + { + return Ok(Vec::new()); + } + + let name = env + .call_method(info, jni_str!("getName"), jni_sig!(() -> JString), &[])? + .l()?; + let name = JString::cast_local(env, name)?.try_to_string(env)?; + let types = env + .call_method( + info, + jni_str!("getSupportedTypes"), + jni_sig!(() -> [JString]), + &[], + )? + .l()?; + let types: JObjectArray<'_, JString<'_>> = JObjectArray::::cast_local(env, types)?; + let mut found = Vec::new(); + for index in 0..types.len(env)? { + let java_mime = types.get_element(env, index)?; + let mime = java_mime.try_to_string(env)?; + let mime = match mime.as_str() { + MIME_HEVC => MIME_HEVC, + MIME_AV1 => MIME_AV1, + _ => continue, + }; + let capabilities = env + .call_method( + info, + jni_str!("getCapabilitiesForType"), + jni_sig!("(Ljava/lang/String;)Landroid/media/MediaCodecInfo$CodecCapabilities;"), + &[JValue::Object(java_mime.as_ref())], + )? + .l()?; + if feature_required(env, &capabilities, "secure-playback")? + || feature_required(env, &capabilities, "tunneled-playback")? + { + continue; + } + let formats = env + .get_field(&capabilities, jni_str!("colorFormats"), jni_sig!([int]))? + .l()?; + let formats = JIntArray::cast_local(env, formats)?; + let mut values = vec![0; formats.len(env)?]; + formats.get_region(env, 0, &mut values)?; + found.push(( + mime, + Candidate { + name: name.clone(), + p010: values.contains(&COLOR_FORMAT_P010), + }, + )); + } + Ok(found) +} + +fn feature_required( + env: &mut Env<'_>, + capabilities: &JObject<'_>, + feature: &str, +) -> jni::errors::Result { + let feature = env.new_string(feature)?; + env.call_method( + capabilities, + jni_str!("isFeatureRequired"), + jni_sig!((JString) -> boolean), + &[JValue::Object(feature.as_ref())], + )? + .z() +} + +fn deduplicate(candidates: Vec) -> Vec { + let mut names = HashSet::new(); + candidates + .into_iter() + .filter(|candidate| names.insert(candidate.name.clone())) + .collect() +} + +fn snapshot(codec: HwCodec) -> Option { + let state = lock_runtime(); + let runtime = state.as_ref()?; + Some(RuntimeSnapshot { + sdk: runtime.sdk, + candidates: match codec { + HwCodec::Hevc => runtime.hevc.clone(), + HwCodec::Av1 => runtime.av1.clone(), + }, + }) +} + +pub(crate) fn available_codecs() -> &'static [HwCodec] { + let state = lock_runtime(); + let Some(runtime) = state.as_ref() else { + return &[]; + }; + match (runtime.hevc.is_empty(), runtime.av1.is_empty()) { + (false, false) => BOTH_CODECS, + (false, true) => HEVC_ONLY, + (true, false) => AV1_ONLY, + (true, true) => &[], + } +} + +pub(crate) fn backend() -> Option { + (!available_codecs().is_empty()).then_some(HwBackend::MediaCodec) +} + +pub(crate) fn decoder(codec: HwCodec) -> Option> { + let snapshot = snapshot(codec)?; + if snapshot.candidates.is_empty() { + None + } else { + MediaCodecStillDecoder::new(codec, snapshot) + .map(|decoder| Box::new(decoder) as Box) + } +} + +enum WorkerMessage { + Decode { + request: PreparedRequest, + reply: SyncSender>, + }, + Shutdown, +} + +struct MediaCodecStillDecoder { + codec: HwCodec, + runtime: RuntimeSnapshot, + sender: SyncSender, + worker: Option>, +} + +impl MediaCodecStillDecoder { + fn new(codec: HwCodec, runtime: RuntimeSnapshot) -> Option { + let (sender, receiver) = mpsc::sync_channel(1); + let worker_runtime = runtime.clone(); + let worker = match thread::Builder::new() + .name(format!("rawshift-{codec}-mediacodec")) + .spawn(move || worker_main(receiver, worker_runtime)) + { + Ok(worker) => worker, + Err(error) => { + tracing::debug!( + target: "rawshift_hwdec::mediacodec", + %error, + "failed to create MediaCodec worker" + ); + return None; + } + }; + Some(Self { + codec, + runtime, + sender, + worker: Some(worker), + }) + } + + fn restart_worker(&mut self) -> Result<(), String> { + // A timed-out NDK call may still own thread-affine handles. Detach the + // old thread and create a completely fresh worker/session. + self.worker.take(); + let (sender, receiver) = mpsc::sync_channel(1); + let runtime = self.runtime.clone(); + self.sender = sender; + self.worker = Some( + thread::Builder::new() + .name(format!("rawshift-{}-mediacodec", self.codec)) + .spawn(move || worker_main(receiver, runtime)) + .map_err(|error| error.to_string())?, + ); + Ok(()) + } + + fn restart_suffix(&mut self) -> String { + match self.restart_worker() { + Ok(()) => "; a fresh worker is ready".to_string(), + Err(error) => format!("; creating a fresh worker also failed: {error}"), + } + } +} + +impl HwStillDecoder for MediaCodecStillDecoder { + fn decode_still( + &mut self, + request: &StillDecodeRequest<'_>, + ) -> Result { + let prepared = prepare(request)?; + debug_assert_eq!(prepared.codec, self.codec); + let (reply, response) = mpsc::sync_channel(1); + if self + .sender + .send(WorkerMessage::Decode { + request: prepared, + reply, + }) + .is_err() + { + let suffix = self.restart_suffix(); + return Err(HwDecodeError::Decode { + codec: self.codec, + message: format!("MediaCodec worker exited unexpectedly{suffix}"), + }); + } + match response.recv_timeout(DECODE_DEADLINE) { + Ok(result) => result, + Err(mpsc::RecvTimeoutError::Timeout) => { + let suffix = self.restart_suffix(); + Err(HwDecodeError::Decode { + codec: self.codec, + message: format!( + "MediaCodec decode exceeded the fixed 5 second deadline{suffix}" + ), + }) + } + Err(mpsc::RecvTimeoutError::Disconnected) => { + let suffix = self.restart_suffix(); + Err(HwDecodeError::Decode { + codec: self.codec, + message: format!("MediaCodec worker exited without a result{suffix}"), + }) + } + } + } +} + +impl Drop for MediaCodecStillDecoder { + fn drop(&mut self) { + let _ = self.sender.send(WorkerMessage::Shutdown); + if let Some(worker) = self.worker.take() { + let _ = worker.join(); + } + } +} + +fn worker_main(receiver: Receiver, runtime: RuntimeSnapshot) { + let mut state = WorkerState { + runtime, + session: None, + }; + while let Ok(message) = receiver.recv() { + match message { + WorkerMessage::Decode { request, reply } => { + let _ = reply.send(state.decode(request)); + } + WorkerMessage::Shutdown => break, + } + } +} + +struct WorkerState { + runtime: RuntimeSnapshot, + session: Option, +} + +impl WorkerState { + fn decode(&mut self, request: PreparedRequest) -> Result { + if request.bit_depth > 8 && self.runtime.sdk < API_P010 { + return Err(HwDecodeError::Unavailable { + codec: request.codec, + reason: format!( + "10-bit output requires Android API {API_P010}+ (device is API {})", + self.runtime.sdk + ), + }); + } + let mut candidates: Vec = self + .runtime + .candidates + .iter() + .filter(|candidate| request.bit_depth == 8 || candidate.p010) + .cloned() + .collect(); + if candidates.is_empty() { + return Err(HwDecodeError::Unavailable { + codec: request.codec, + reason: if request.bit_depth > 8 { + "no hardware decoder advertises COLOR_FormatYUVP010".to_string() + } else { + "no hardware decoder candidate remains".to_string() + }, + }); + } + + if let Some(session) = self.session.as_ref() + && let Some(index) = candidates + .iter() + .position(|candidate| candidate.name == session.candidate.name) + { + candidates.swap(0, index); + } + + let total_deadline = Instant::now() + DECODE_DEADLINE; + let mut failures = Vec::new(); + for (index, candidate) in candidates.iter().enumerate() { + let now = Instant::now(); + if now >= total_deadline { + failures.push(format!( + "{}@deadline: total deadline exhausted", + candidate.name + )); + continue; + } + // Reserve a fair portion of the remaining fixed budget for every + // candidate, so a broken first codec cannot prevent fallback. + let remaining_candidates = (candidates.len() - index) as u32; + let candidate_deadline = now + (total_deadline - now) / remaining_candidates; + let result = catch_unwind(AssertUnwindSafe(|| { + self.decode_candidate(candidate, &request, candidate_deadline) + })) + .unwrap_or_else(|panic| { + Err(PlatformFailure::message( + "panic", + panic_message(panic.as_ref()), + )) + }); + match result { + Ok(frame) => return Ok(frame), + Err(failure) => { + tracing::debug!( + target: "rawshift_hwdec::mediacodec", + codec = %request.codec, + component = %candidate.name, + stage = failure.stage, + error = %failure.message, + "MediaCodec hardware candidate failed" + ); + failures.push(format!( + "{}@{}: {}", + candidate.name, failure.stage, failure.message + )); + self.discard_session(); + } + } + } + tracing::warn!( + target: "rawshift_hwdec::mediacodec", + codec = %request.codec, + failures = %failures.join("; "), + "all MediaCodec hardware candidates failed" + ); + Err(HwDecodeError::Decode { + codec: request.codec, + message: format!( + "all MediaCodec hardware candidates failed: {}", + failures.join("; ") + ), + }) + } + + fn decode_candidate( + &mut self, + candidate: &Candidate, + request: &PreparedRequest, + deadline: Instant, + ) -> Result { + let reusable = self.session.as_ref().is_some_and(|session| { + session.candidate.name == candidate.name && session.key == request.key + }); + if reusable { + let session = self.session.as_mut().expect("checked above"); + session + .codec + .flush() + .map_err(|e| PlatformFailure::new("flush", e))?; + session.drain_images()?; + } else { + self.discard_session(); + self.session = Some(Session::create(candidate.clone(), request)?); + } + self.session + .as_mut() + .expect("session was created") + .decode(request, deadline) + } + + fn discard_session(&mut self) { + if let Some(session) = self.session.take() + && let Err(panic) = catch_unwind(AssertUnwindSafe(|| drop(session))) + { + tracing::debug!( + target: "rawshift_hwdec::mediacodec", + error = %panic_message(panic.as_ref()), + "MediaCodec session teardown panicked" + ); + } + } +} + +#[derive(Debug)] +struct PlatformFailure { + stage: &'static str, + message: String, +} + +impl PlatformFailure { + fn new(stage: &'static str, error: impl std::fmt::Display) -> Self { + Self { + stage, + message: error.to_string(), + } + } + + fn message(stage: &'static str, message: impl Into) -> Self { + Self { + stage, + message: message.into(), + } + } +} + +struct Session { + candidate: Candidate, + key: SessionKey, + codec: MediaCodec, + reader: ImageReader, + image_signal: Arc<(Mutex, Condvar)>, +} + +impl Session { + fn create(candidate: Candidate, request: &PreparedRequest) -> Result { + let image_format = if request.bit_depth == 8 { + ImageFormat::YUV_420_888 + } else { + ImageFormat::__Unknown(COLOR_FORMAT_P010) + }; + let mut reader = ImageReader::new_with_usage( + request + .width + .try_into() + .map_err(|_| PlatformFailure::message("reader", "width exceeds i32"))?, + request + .height + .try_into() + .map_err(|_| PlatformFailure::message("reader", "height exceeds i32"))?, + image_format, + HardwareBufferUsage::CPU_READ_OFTEN, + 2, + ) + .map_err(|e| PlatformFailure::new("reader", e))?; + let image_signal = Arc::new((Mutex::new(0u64), Condvar::new())); + let callback_signal = Arc::clone(&image_signal); + reader + .set_image_listener(Box::new(move |_| { + let (lock, ready) = &*callback_signal; + let mut generation = lock + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + *generation = (*generation).wrapping_add(1); + ready.notify_all(); + })) + .map_err(|e| PlatformFailure::new("reader-listener", e))?; + let window = reader + .window() + .map_err(|e| PlatformFailure::new("reader-window", e))?; + let codec = MediaCodec::from_codec_name(&candidate.name).ok_or_else(|| { + PlatformFailure::message("create", "AMediaCodec_createCodecByName returned null") + })?; + let mut format = MediaFormat::new(); + format.set_str("mime", request.mime); + format.set_i32("width", request.width as i32); + format.set_i32("height", request.height as i32); + format.set_i32("max-input-size", request.access_unit.len() as i32); + format.set_i32( + "color-format", + if request.bit_depth == 8 { + COLOR_FORMAT_YUV420_FLEXIBLE + } else { + COLOR_FORMAT_P010 + }, + ); + format.set_buffer("csd-0", &request.csd0); + codec + .configure(&format, Some(&window), MediaCodecDirection::Decoder) + .map_err(|e| PlatformFailure::new("configure", e))?; + codec + .start() + .map_err(|e| PlatformFailure::new("start", e))?; + Ok(Self { + candidate, + key: request.key.clone(), + codec, + reader, + image_signal, + }) + } + + fn drain_images(&self) -> Result<(), PlatformFailure> { + loop { + match self + .reader + .acquire_next_image() + .map_err(|e| PlatformFailure::new("drain-image", e))? + { + AcquireResult::Image(image) => drop(image), + AcquireResult::NoBufferAvailable => return Ok(()), + AcquireResult::MaxImagesAcquired => { + return Err(PlatformFailure::message( + "drain-image", + "AImageReader reports max images acquired", + )); + } + } + } + } + + fn decode( + &mut self, + request: &PreparedRequest, + deadline: Instant, + ) -> Result { + self.queue_access_unit(request, deadline)?; + let mut frame = None; + let mut eos = false; + while Instant::now() < deadline && (!eos || frame.is_none()) { + let timeout = poll_timeout(deadline); + match self + .codec + .dequeue_output_buffer(timeout) + .map_err(|e| PlatformFailure::new("dequeue-output", e))? + { + DequeuedOutputBufferInfoResult::Buffer(output) => { + let flags = output.info().flags(); + let render = flags & BUFFER_FLAG_CODEC_CONFIG == 0 && output.info().size() > 0; + self.codec + .release_output_buffer(output, render) + .map_err(|e| PlatformFailure::new("release-output", e))?; + if render && frame.is_none() { + frame = Some(self.wait_for_image(request, deadline)?); + } + eos |= flags & BUFFER_FLAG_END_OF_STREAM != 0; + } + DequeuedOutputBufferInfoResult::TryAgainLater + | DequeuedOutputBufferInfoResult::OutputFormatChanged + | DequeuedOutputBufferInfoResult::OutputBuffersChanged => {} + } + } + frame.ok_or_else(|| PlatformFailure::message("output", "deadline elapsed before a frame")) + } + + fn queue_access_unit( + &self, + request: &PreparedRequest, + deadline: Instant, + ) -> Result<(), PlatformFailure> { + while Instant::now() < deadline { + match self + .codec + .dequeue_input_buffer(poll_timeout(deadline)) + .map_err(|e| PlatformFailure::new("dequeue-input", e))? + { + DequeuedInputBufferResult::TryAgainLater => {} + DequeuedInputBufferResult::Buffer(mut input) => { + let buffer = input.buffer_mut(); + if buffer.len() < request.access_unit.len() { + return Err(PlatformFailure::message( + "input-buffer", + format!( + "codec supplied {} bytes for a {} byte access unit", + buffer.len(), + request.access_unit.len() + ), + )); + } + for (slot, byte) in buffer.iter_mut().zip(&request.access_unit) { + slot.write(*byte); + } + self.codec + .queue_input_buffer( + input, + 0, + request.access_unit.len(), + 0, + BUFFER_FLAG_END_OF_STREAM, + ) + .map_err(|e| PlatformFailure::new("queue-input", e))?; + return Ok(()); + } + } + } + Err(PlatformFailure::message( + "input-buffer", + "deadline elapsed before an input buffer", + )) + } + + fn wait_for_image( + &self, + request: &PreparedRequest, + deadline: Instant, + ) -> Result { + loop { + match self + .reader + .acquire_next_image() + .map_err(|e| PlatformFailure::new("acquire-image", e))? + { + AcquireResult::Image(image) => return image_to_frame(&image, request), + AcquireResult::MaxImagesAcquired => { + return Err(PlatformFailure::message( + "acquire-image", + "AImageReader reports max images acquired", + )); + } + AcquireResult::NoBufferAvailable => {} + } + let now = Instant::now(); + if now >= deadline { + return Err(PlatformFailure::message( + "acquire-image", + "deadline elapsed before AImageReader delivered a frame", + )); + } + let (lock, ready) = &*self.image_signal; + let generation = lock + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let _ = ready + .wait_timeout(generation, (deadline - now).min(POLL_SLICE)) + .unwrap_or_else(std::sync::PoisonError::into_inner); + } + } +} + +impl Drop for Session { + fn drop(&mut self) { + let _ = self.codec.stop(); + } +} + +fn poll_timeout(deadline: Instant) -> Duration { + deadline + .saturating_duration_since(Instant::now()) + .min(POLL_SLICE) +} + +fn panic_message(payload: &(dyn std::any::Any + Send)) -> String { + payload + .downcast_ref::<&str>() + .map(|message| (*message).to_string()) + .or_else(|| payload.downcast_ref::().cloned()) + .unwrap_or_else(|| "non-string panic payload".to_string()) +} + +fn image_to_frame( + image: &Image, + request: &PreparedRequest, +) -> Result { + let format = image + .format() + .map_err(|e| PlatformFailure::new("image-format", e))?; + let layout = match format { + ImageFormat::YUV_420_888 if request.bit_depth == 8 => ImageLayout::Yuv420, + ImageFormat::__Unknown(COLOR_FORMAT_P010) if request.bit_depth > 8 => ImageLayout::P010, + other => { + return Err(PlatformFailure::message( + "image-format", + format!("unexpected AImage format {other:?}"), + )); + } + }; + let width = positive_u32("image-width", image.width())?; + let height = positive_u32("image-height", image.height())?; + let crop = image + .crop_rect() + .map_err(|e| PlatformFailure::new("image-crop", e))?; + let plane_count = image + .number_of_planes() + .map_err(|e| PlatformFailure::new("image-planes", e))?; + if plane_count < 0 { + return Err(PlatformFailure::message( + "image-planes", + "AImage returned a negative plane count", + )); + } + let mut planes = Vec::with_capacity(plane_count as usize); + for index in 0..plane_count { + let data = image + .plane_data(index) + .map_err(|e| PlatformFailure::new("image-plane-data", e))?; + let row_stride = positive_usize("image-row-stride", image.plane_row_stride(index))?; + let pixel_stride = positive_usize("image-pixel-stride", image.plane_pixel_stride(index))?; + planes.push(PlaneView { + data, + row_stride, + pixel_stride, + }); + } + let view = ImageView { + layout, + width, + height, + crop: ( + nonnegative_u32("crop-left", crop.left)?, + nonnegative_u32("crop-top", crop.top)?, + nonnegative_u32("crop-right", crop.right)?, + nonnegative_u32("crop-bottom", crop.bottom)?, + ), + planes, + }; + normalize_image( + &view, + (request.width, request.height), + request.bit_depth, + request.range, + ) + .map_err(|e| PlatformFailure::new("normalize-image", e)) +} + +fn positive_u32( + stage: &'static str, + value: Result, +) -> Result { + let value = value.map_err(|e| PlatformFailure::new(stage, e))?; + u32::try_from(value) + .ok() + .filter(|value| *value > 0) + .ok_or_else(|| PlatformFailure::message(stage, format!("invalid value {value}"))) +} + +fn nonnegative_u32(stage: &'static str, value: i32) -> Result { + u32::try_from(value) + .map_err(|_| PlatformFailure::message(stage, format!("invalid value {value}"))) +} + +fn positive_usize( + stage: &'static str, + value: Result, +) -> Result { + let value = value.map_err(|e| PlatformFailure::new(stage, e))?; + usize::try_from(value) + .ok() + .filter(|value| *value > 0) + .ok_or_else(|| PlatformFailure::message(stage, format!("invalid value {value}"))) +} + +// Compile-time assertion: prepared jobs and replies are the only values that +// cross threads; no NDK wrapper is made Send with unsafe code. +const _: fn() = || { + fn assert_send() {} + assert_send::(); + assert_send::>(); +}; diff --git a/crates/rawshift-hwdec/src/vaapi/mod.rs b/crates/rawshift-hwdec/src/vaapi/mod.rs index dc8e13f..52fff27 100644 --- a/crates/rawshift-hwdec/src/vaapi/mod.rs +++ b/crates/rawshift-hwdec/src/vaapi/mod.rs @@ -43,11 +43,11 @@ use std::fs::File; use std::os::fd::AsRawFd; use std::sync::OnceLock; +use crate::bitstream::{av1, bits, hevc}; use crate::{ CodecConfig, ColorRange, DecodedFrame, HwBackend, HwCodec, HwDecodeError, HwStillDecoder, PixelFormat, Plane, StillDecodeRequest, }; -use crate::bitstream::{av1, bits, hevc}; // ── Public backend surface (called from lib.rs) ───────────────────────────── diff --git a/crates/rawshift-image/src/lib.rs b/crates/rawshift-image/src/lib.rs index 0d0cf60..ffe987f 100644 --- a/crates/rawshift-image/src/lib.rs +++ b/crates/rawshift-image/src/lib.rs @@ -110,3 +110,6 @@ pub mod processing; pub mod transforms; pub mod prelude; + +#[cfg(all(target_os = "android", feature = "hw"))] +pub use rawshift_hwdec::{AndroidHwDecodeInitError, AndroidJavaVm, initialize_android_hw_decode}; diff --git a/crates/rawshift-image/src/prelude.rs b/crates/rawshift-image/src/prelude.rs index 7de57c6..c7134ef 100644 --- a/crates/rawshift-image/src/prelude.rs +++ b/crates/rawshift-image/src/prelude.rs @@ -22,6 +22,9 @@ //! `apply_white_balance_raw`, `apply_color_matrix`, `apply_tone_reproduction`, //! `apply_tonemap`, `compute_camera_to_srgb`, `ColorSpaceTransform`, and more. +#[cfg(all(target_os = "android", feature = "hw"))] +pub use crate::{AndroidHwDecodeInitError, AndroidJavaVm, initialize_android_hw_decode}; + // core pub use crate::core::image::{CfaPattern, RawImage, Rect, XTransPattern}; pub use crate::core::metadata::{ From 427ebd0ac898ae1b231e4843e8874291ac75e7b8 Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Wed, 5 Aug 2026 16:57:34 -0400 Subject: [PATCH 3/6] test(android): add physical MediaCodec gate --- .github/workflows/ci.yml | 8 +- .gitignore | 4 + README.md | 5 + android/mediacodec-harness/README.md | 71 +++ .../mediacodec-harness/app/build.gradle.kts | 51 ++ .../harness/MediaCodecDeviceTest.java | 36 ++ .../app/src/main/AndroidManifest.xml | 15 + .../rawshift/harness/MainActivity.java | 18 + .../rawshift/harness/NativeHarness.java | 11 + android/mediacodec-harness/build-rust.sh | 39 ++ android/mediacodec-harness/build.gradle.kts | 3 + android/mediacodec-harness/gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48966 bytes .../gradle/wrapper/gradle-wrapper.properties | 8 + android/mediacodec-harness/gradlew | 248 +++++++++ android/mediacodec-harness/gradlew.bat | 93 ++++ android/mediacodec-harness/native/Cargo.lock | 493 ++++++++++++++++++ android/mediacodec-harness/native/Cargo.toml | 14 + android/mediacodec-harness/native/src/lib.rs | 224 ++++++++ .../mediacodec-harness/settings.gradle.kts | 18 + crates/rawshift-hwdec/README.md | 24 +- docs/SUPPORT.md | 14 +- 22 files changed, 1393 insertions(+), 6 deletions(-) create mode 100644 android/mediacodec-harness/README.md create mode 100644 android/mediacodec-harness/app/build.gradle.kts create mode 100644 android/mediacodec-harness/app/src/androidTest/java/org/visualcommons/rawshift/harness/MediaCodecDeviceTest.java create mode 100644 android/mediacodec-harness/app/src/main/AndroidManifest.xml create mode 100644 android/mediacodec-harness/app/src/main/java/org/visualcommons/rawshift/harness/MainActivity.java create mode 100644 android/mediacodec-harness/app/src/main/java/org/visualcommons/rawshift/harness/NativeHarness.java create mode 100755 android/mediacodec-harness/build-rust.sh create mode 100644 android/mediacodec-harness/build.gradle.kts create mode 100644 android/mediacodec-harness/gradle.properties create mode 100644 android/mediacodec-harness/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/mediacodec-harness/gradle/wrapper/gradle-wrapper.properties create mode 100755 android/mediacodec-harness/gradlew create mode 100644 android/mediacodec-harness/gradlew.bat create mode 100644 android/mediacodec-harness/native/Cargo.lock create mode 100644 android/mediacodec-harness/native/Cargo.toml create mode 100644 android/mediacodec-harness/native/src/lib.rs create mode 100644 android/mediacodec-harness/settings.gradle.kts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3f853d..6376a52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,7 +153,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: "1.92.0" - targets: aarch64-apple-darwin,x86_64-unknown-linux-musl + targets: aarch64-apple-darwin,aarch64-linux-android,x86_64-unknown-linux-musl - uses: Swatinem/rust-cache@v2 - name: Invalid combos must fail with the compile_error text run: | @@ -182,6 +182,12 @@ jobs: run: | # Explicit pin on its native target. cargo check -p rawshift-image --no-default-features --features hw-vaapi --target x86_64-unknown-linux-gnu + # Android's Java API is needed only at runtime; `cargo check` on a + # Linux runner fully type-checks the JNI/NDK implementation and the + # public re-exports without an emulator or cross linker. + cargo check -p rawshift-hwdec --features mediacodec --target aarch64-linux-android + cargo check -p rawshift-image --no-default-features --features hw-mediacodec --target aarch64-linux-android + cargo check -p rawshift --no-default-features --features image,hw-mediacodec --target aarch64-linux-android # The portable `hw` is valid everywhere: it selects the native # backend where one exists and compiles the no-backend stub (with a # build-script warning) where none does (musl). diff --git a/.gitignore b/.gitignore index e4cc6ec..8dce5c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ /target +/android/mediacodec-harness/.gradle/ +/android/mediacodec-harness/app/build/ +/android/mediacodec-harness/native/target/ +/android/mediacodec-harness/local.properties .DS_Store diff --git a/README.md b/README.md index 8595071..c4dd628 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,11 @@ The `rawshift` facade deliberately exposes only coarse features: and fail the compile elsewhere (see [docs/SUPPORT.md](./docs/SUPPORT.md)). - `full` — every image format, `serde`, and `hw`. +Android API 29+ applications call the safely re-exported +`initialize_android_hw_decode(JavaVM)` before querying or decoding. The +physical-device acceptance matrix is in the +[`android/mediacodec-harness`](./android/mediacodec-harness/README.md). + There is no `video` feature: video is parked for v1 (see [Video](#video)). The facade does **not** re-export per-format flags. Cargo cannot forward a child diff --git a/android/mediacodec-harness/README.md b/android/mediacodec-harness/README.md new file mode 100644 index 0000000..40051ce --- /dev/null +++ b/android/mediacodec-harness/README.md @@ -0,0 +1,71 @@ +# MediaCodec physical-device harness + +This Gradle project is the merge gate for rawshift's Android hardware decoder. +It builds the Rust JNI library for `arm64-v8a`, installs an instrumentation +test, rejects emulators, and decodes checked-in lossless HEVC and AV1 fixtures. + +The test verifies all decoded bytes, not merely that MediaCodec returned an +output buffer. It decodes every required fixture twice through one decoder: +the first duration is reported as `cold_us`, and the second (exact-format +session reuse) as `warm_us`. These are diagnostic numbers, not thresholds. + +## Toolchain + +- JDK 17 +- Android SDK Platform 36 and Build Tools 36.0.0 +- Android NDK `29.0.14206865` +- Rust 1.92.0 with `aarch64-linux-android` +- Gradle 9.4.1 (checked wrapper) and Android Gradle Plugin 9.2.1 + +Install the Android pieces with: + +```sh +sdkmanager "platforms;android-36" "build-tools;36.0.0" "ndk;29.0.14206865" +rustup target add aarch64-linux-android +``` + +Set `ANDROID_HOME` and set `ANDROID_NDK_HOME` to the NDK directory. Then +connect exactly one unlocked ARM64 physical device with USB debugging enabled: + +```sh +adb devices -l +cd android/mediacodec-harness +./gradlew connectedDebugAndroidTest +``` + +The full acceptance matrix is two physical-device runs: + +| Run | Device requirements | Required result | +| --- | --- | --- | +| Compatibility floor | API 29, ARM64, hardware HEVC and AV1 decoders | byte-exact 8-bit HEVC + AV1, cold and warm | +| High bit depth | API 33+, ARM64, hardware HEVC and AV1 decoders | the same 8-bit checks plus byte-exact P010 for at least one codec | + +Attach both `rawshift-hwdec` report blocks from logcat/test output to the pull +request before changing it from draft to ready. An emulator is intentionally +not accepted: its advertised codec inventory and acceleration path depend on +host passthrough, so it cannot establish the physical hardware contract. + +## Why this is the Android boundary + +`AMediaCodec` is the decoder. `AImageReader` supplies its CPU-readable output +surface. A small JNI classification step calls `MediaCodecList` because API 29 +is the first public Android API that definitively exposes +`MediaCodecInfo.isHardwareAccelerated()` and `isAlias()`. JNI therefore does +not introduce a second decode backend; it identifies the exact named hardware +components that the NDK `AMediaCodec_createCodecByName` path opens. + +This is the only Android decode API rawshift targets. Media3/ExoPlayer are +playback orchestration layers over MediaCodec, while `ImageDecoder` accepts +complete supported image sources rather than the HEIF/AVIF item codestream +seam rawshift must implement. The relevant platform contracts are: + +- +- +- +- +- + +P010 is deliberately API 33+ in rawshift: although `ImageFormat.YCBCR_P010` +appeared earlier, `MediaCodecInfo.CodecCapabilities.COLOR_FormatYUVP010` is a +public codec capability only from API 33. The backend never silently +downconverts 10-bit input. diff --git a/android/mediacodec-harness/app/build.gradle.kts b/android/mediacodec-harness/app/build.gradle.kts new file mode 100644 index 0000000..7f406e2 --- /dev/null +++ b/android/mediacodec-harness/app/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + id("com.android.application") +} + +android { + namespace = "org.visualcommons.rawshift.harness" + compileSdk = 36 + ndkVersion = "29.0.14206865" + + defaultConfig { + applicationId = "org.visualcommons.rawshift.harness" + minSdk = 29 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + ndk { + abiFilters += "arm64-v8a" + } + } + + sourceSets.getByName("main").jniLibs.srcDir(layout.buildDirectory.dir("rust-jni-libs")) +} + +val buildRust by tasks.registering(Exec::class) { + val output = layout.buildDirectory.dir("rust-jni-libs/arm64-v8a") + inputs.files( + fileTree("../../..") { + include("Cargo.toml", "Cargo.lock") + include("crates/rawshift-hwdec/src/**") + include("crates/rawshift-hwdec/Cargo.toml") + include("android/mediacodec-harness/native/**") + include("android/mediacodec-harness/fixtures/data/**") + } + ) + outputs.dir(output) + commandLine( + "bash", + rootProject.file("build-rust.sh").absolutePath, + output.get().asFile.absolutePath, + ) +} + +tasks.named("preBuild").configure { + dependsOn(buildRust) +} + +dependencies { + androidTestImplementation("androidx.test.ext:junit:1.3.0") + androidTestImplementation("androidx.test:runner:1.7.0") +} diff --git a/android/mediacodec-harness/app/src/androidTest/java/org/visualcommons/rawshift/harness/MediaCodecDeviceTest.java b/android/mediacodec-harness/app/src/androidTest/java/org/visualcommons/rawshift/harness/MediaCodecDeviceTest.java new file mode 100644 index 0000000..3dbc19a --- /dev/null +++ b/android/mediacodec-harness/app/src/androidTest/java/org/visualcommons/rawshift/harness/MediaCodecDeviceTest.java @@ -0,0 +1,36 @@ +package org.visualcommons.rawshift.harness; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.os.Build; +import android.util.Log; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public final class MediaCodecDeviceTest { + @Test + public void hardwareStillDecodeFixtures() { + assertFalse("This is a physical-device gate; emulator results are not accepted", isEmulator()); + assertTrue("rawshift supports Android API 29+", Build.VERSION.SDK_INT >= 29); + + String report = NativeHarness.runSuite(Build.VERSION.SDK_INT); + Log.i("rawshift-hwdec", "\n" + report); + assertTrue(report, report.startsWith("PASS\n") || report.startsWith("PASS sdk=")); + } + + private static boolean isEmulator() { + return Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.toLowerCase().contains("emulator") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for") + || Build.MANUFACTURER.contains("Genymotion") + || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) + || Build.PRODUCT.contains("sdk_gphone") + || Build.PRODUCT.contains("google_sdk") + || Build.PRODUCT.contains("simulator"); + } +} diff --git a/android/mediacodec-harness/app/src/main/AndroidManifest.xml b/android/mediacodec-harness/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2b5ce12 --- /dev/null +++ b/android/mediacodec-harness/app/src/main/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/android/mediacodec-harness/app/src/main/java/org/visualcommons/rawshift/harness/MainActivity.java b/android/mediacodec-harness/app/src/main/java/org/visualcommons/rawshift/harness/MainActivity.java new file mode 100644 index 0000000..f931d77 --- /dev/null +++ b/android/mediacodec-harness/app/src/main/java/org/visualcommons/rawshift/harness/MainActivity.java @@ -0,0 +1,18 @@ +package org.visualcommons.rawshift.harness; + +import android.app.Activity; +import android.os.Build; +import android.os.Bundle; +import android.widget.TextView; + +public final class MainActivity extends Activity { + @Override + protected void onCreate(Bundle state) { + super.onCreate(state); + TextView report = new TextView(this); + report.setText(NativeHarness.runSuite(Build.VERSION.SDK_INT)); + report.setTextIsSelectable(true); + report.setPadding(32, 32, 32, 32); + setContentView(report); + } +} diff --git a/android/mediacodec-harness/app/src/main/java/org/visualcommons/rawshift/harness/NativeHarness.java b/android/mediacodec-harness/app/src/main/java/org/visualcommons/rawshift/harness/NativeHarness.java new file mode 100644 index 0000000..a7e1b21 --- /dev/null +++ b/android/mediacodec-harness/app/src/main/java/org/visualcommons/rawshift/harness/NativeHarness.java @@ -0,0 +1,11 @@ +package org.visualcommons.rawshift.harness; + +final class NativeHarness { + static { + System.loadLibrary("rawshift_mediacodec_harness_native"); + } + + private NativeHarness() {} + + static native String runSuite(int sdk); +} diff --git a/android/mediacodec-harness/build-rust.sh b/android/mediacodec-harness/build-rust.sh new file mode 100755 index 0000000..d6fa7d6 --- /dev/null +++ b/android/mediacodec-harness/build-rust.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "usage: build-rust.sh " >&2 + exit 2 +fi + +ndk_root=${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-}} +if [[ -z "${ndk_root}" ]]; then + echo "ANDROID_NDK_HOME must point to NDK 29.0.14206865" >&2 + exit 2 +fi + +case "$(uname -s)-$(uname -m)" in + Linux-x86_64) host_tag=linux-x86_64 ;; + Darwin-x86_64|Darwin-arm64) host_tag=darwin-x86_64 ;; + *) echo "unsupported build host: $(uname -s)-$(uname -m)" >&2; exit 2 ;; +esac + +linker="${ndk_root}/toolchains/llvm/prebuilt/${host_tag}/bin/aarch64-linux-android29-clang" +if [[ ! -x "${linker}" ]]; then + echo "Android API 29 linker not found: ${linker}" >&2 + exit 2 +fi + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +output_dir=$1 + +rustup target add aarch64-linux-android +CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="${linker}" \ + cargo build \ + --manifest-path "${script_dir}/native/Cargo.toml" \ + --target aarch64-linux-android \ + --release + +mkdir -p "${output_dir}" +cp "${script_dir}/native/target/aarch64-linux-android/release/librawshift_mediacodec_harness_native.so" \ + "${output_dir}/librawshift_mediacodec_harness_native.so" diff --git a/android/mediacodec-harness/build.gradle.kts b/android/mediacodec-harness/build.gradle.kts new file mode 100644 index 0000000..fd4b881 --- /dev/null +++ b/android/mediacodec-harness/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("com.android.application") version "9.2.1" apply false +} diff --git a/android/mediacodec-harness/gradle.properties b/android/mediacodec-harness/gradle.properties new file mode 100644 index 0000000..344ead5 --- /dev/null +++ b/android/mediacodec-harness/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 diff --git a/android/mediacodec-harness/gradle/wrapper/gradle-wrapper.jar b/android/mediacodec-harness/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d997cfc60f4cff0e7451d19d49a82fa986695d07 GIT binary patch literal 48966 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

`Lem|V$A>eR0uN8Y&sdjtu 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

1bvi{DS7E@q1{$_8->K{_OWv2 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 literal 0 HcmV?d00001 diff --git a/android/mediacodec-harness/gradle/wrapper/gradle-wrapper.properties b/android/mediacodec-harness/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8e61ef1 --- /dev/null +++ b/android/mediacodec-harness/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,8 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/mediacodec-harness/gradlew b/android/mediacodec-harness/gradlew new file mode 100755 index 0000000..4bb327e --- /dev/null +++ b/android/mediacodec-harness/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original 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 +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/mediacodec-harness/gradlew.bat b/android/mediacodec-harness/gradlew.bat new file mode 100644 index 0000000..bd8a8c0 --- /dev/null +++ b/android/mediacodec-harness/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/mediacodec-harness/native/Cargo.lock b/android/mediacodec-harness/native/Cargo.lock new file mode 100644 index 0000000..afaf4e1 --- /dev/null +++ b/android/mediacodec-harness/native/Cargo.lock @@ -0,0 +1,493 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "gamut-color" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde55ae76aee646990fd4f353574fbcfaf9f37a5f3e54ba0907c7d384936e56a" +dependencies = [ + "gamut-core", +] + +[[package]] +name = "gamut-core" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b17c79c8672b675d538dceffda1dd5d00ba279cbcca4f75c7b4fa847741d5d31" +dependencies = [ + "thiserror 2.0.19", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.19", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rawshift-hwdec" +version = "0.1.1" +dependencies = [ + "gamut-color", + "jni", + "libloading", + "ndk", + "thiserror 2.0.19", + "tracing", +] + +[[package]] +name = "rawshift-mediacodec-harness-native" +version = "0.0.0" +dependencies = [ + "jni", + "rawshift-hwdec", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] diff --git a/android/mediacodec-harness/native/Cargo.toml b/android/mediacodec-harness/native/Cargo.toml new file mode 100644 index 0000000..b2cb6a2 --- /dev/null +++ b/android/mediacodec-harness/native/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rawshift-mediacodec-harness-native" +version = "0.0.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] + +[target.'cfg(target_os = "android")'.dependencies] +jni = "0.22.4" +rawshift-hwdec = { path = "../../../crates/rawshift-hwdec", features = ["mediacodec"] } + +[workspace] diff --git a/android/mediacodec-harness/native/src/lib.rs b/android/mediacodec-harness/native/src/lib.rs new file mode 100644 index 0000000..0a66eba --- /dev/null +++ b/android/mediacodec-harness/native/src/lib.rs @@ -0,0 +1,224 @@ +#![cfg_attr(not(target_os = "android"), allow(dead_code))] + +#[cfg(target_os = "android")] +mod android { + use std::time::Instant; + + use jni::errors::ThrowRuntimeExAndDefault; + use jni::objects::{JClass, JString}; + use jni::{Env, EnvUnowned}; + use rawshift_hwdec::{ + ChromaSubsampling, CodecConfig, HwCodec, HwDecodeError, PixelFormat, StillDecodeRequest, + available_codecs, backend, decoder, initialize_android_hw_decode, + }; + + const HEVC_8: &[u8] = include_bytes!("../../fixtures/data/hevc-8.rshw"); + const HEVC_10: &[u8] = include_bytes!("../../fixtures/data/hevc-10.rshw"); + const AV1_8: &[u8] = include_bytes!("../../fixtures/data/av1-8.rshw"); + const AV1_10: &[u8] = include_bytes!("../../fixtures/data/av1-10.rshw"); + + struct Fixture<'a> { + codec: HwCodec, + depth: u8, + width: u32, + height: u32, + config: &'a [u8], + payload: &'a [u8], + expected: &'a [u8], + } + + enum Outcome { + Pass(String), + Unavailable(String), + } + + #[unsafe(no_mangle)] + pub extern "system" fn Java_org_visualcommons_rawshift_harness_NativeHarness_runSuite< + 'local, + >( + mut unowned_env: EnvUnowned<'local>, + _class: JClass<'local>, + sdk: i32, + ) -> JString<'local> { + unowned_env + .with_env(|env| -> jni::errors::Result<_> { + let report = run_suite(env, sdk).unwrap_or_else(|error| format!("FAIL\n{error}")); + JString::from_str(env, report) + }) + .resolve::() + } + + fn run_suite(env: &Env<'_>, sdk: i32) -> Result { + initialize_android_hw_decode(env.get_java_vm().map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; + // Prove same-VM initialization is idempotent, not merely documented. + initialize_android_hw_decode(env.get_java_vm().map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; + let found = available_codecs(); + for codec in [HwCodec::Hevc, HwCodec::Av1] { + if !found.contains(&codec) { + return Err(format!("no hardware {codec} decoder was discovered")); + } + } + + let mut lines = vec![format!( + "PASS sdk={sdk} backend={} codecs={found:?}", + backend().ok_or_else(|| "backend() returned None after discovery".to_string())? + )]; + for bytes in [HEVC_8, AV1_8] { + match run_fixture(bytes)? { + Outcome::Pass(line) => lines.push(line), + Outcome::Unavailable(reason) => { + return Err(format!("required 8-bit fixture unavailable: {reason}")); + } + } + } + + if sdk >= 33 { + let mut p010_passes = 0; + for bytes in [HEVC_10, AV1_10] { + match run_fixture(bytes)? { + Outcome::Pass(line) => { + p010_passes += 1; + lines.push(line); + } + Outcome::Unavailable(reason) => lines.push(format!("SKIP p010 {reason}")), + } + } + if p010_passes == 0 { + return Err("API 33+ device exposed no working hardware P010 path".to_string()); + } + } else { + lines.push("SKIP p010 requires API 33+".to_string()); + } + Ok(lines.join("\n")) + } + + fn run_fixture(bytes: &[u8]) -> Result { + let fixture = fixture(bytes)?; + let request = StillDecodeRequest { + config: match fixture.codec { + HwCodec::Hevc => CodecConfig::Hvcc(fixture.config), + HwCodec::Av1 => CodecConfig::Av1c(fixture.config), + }, + payload: fixture.payload, + width: fixture.width, + height: fixture.height, + bit_depth: fixture.depth, + chroma: ChromaSubsampling::Cs420, + }; + let Some(mut decoder) = decoder(fixture.codec) else { + return Ok(Outcome::Unavailable(format!( + "{} {}-bit decoder() returned None", + fixture.codec, fixture.depth + ))); + }; + + let cold_start = Instant::now(); + let cold = match decoder.decode_still(&request) { + Ok(frame) => frame, + Err(HwDecodeError::Unavailable { reason, .. }) => { + return Ok(Outcome::Unavailable(format!( + "{} {}-bit: {reason}", + fixture.codec, fixture.depth + ))); + } + Err(error) => return Err(error.to_string()), + }; + let cold_us = cold_start.elapsed().as_micros(); + verify_frame(&fixture, &cold)?; + + // The second request is byte-for-byte identical and therefore must + // exercise the exact-format session reuse path. + let warm_start = Instant::now(); + let warm = decoder.decode_still(&request).map_err(|e| e.to_string())?; + let warm_us = warm_start.elapsed().as_micros(); + verify_frame(&fixture, &warm)?; + Ok(Outcome::Pass(format!( + "{} {}-bit cold_us={cold_us} warm_us={warm_us}", + fixture.codec, fixture.depth + ))) + } + + fn verify_frame( + fixture: &Fixture<'_>, + frame: &rawshift_hwdec::DecodedFrame, + ) -> Result<(), String> { + if (frame.width(), frame.height(), frame.bit_depth()) + != (fixture.width, fixture.height, fixture.depth) + { + return Err(format!( + "{} output geometry/depth mismatch: {}x{} {}-bit", + fixture.codec, + frame.width(), + frame.height(), + frame.bit_depth() + )); + } + let expected_format = if fixture.depth == 8 { + PixelFormat::I420 + } else { + PixelFormat::P010 + }; + if frame.format() != expected_format { + return Err(format!( + "{} expected {expected_format:?}, got {:?}", + fixture.codec, + frame.format() + )); + } + let actual: Vec<_> = frame + .planes() + .iter() + .flat_map(|plane| plane.data.iter().copied()) + .collect(); + if actual != fixture.expected { + return Err(format!( + "{} {}-bit decoded pixels differ (expected fnv={:016x}, actual fnv={:016x})", + fixture.codec, + fixture.depth, + fnv1a(fixture.expected), + fnv1a(&actual) + )); + } + Ok(()) + } + + fn fnv1a(bytes: &[u8]) -> u64 { + bytes.iter().fold(0xcbf2_9ce4_8422_2325, |hash, byte| { + (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3) + }) + } + + fn fixture(bytes: &[u8]) -> Result, String> { + if bytes.get(..8) != Some(b"RSHW0001") { + return Err("bad fixture magic".to_string()); + } + let codec = match bytes[8] { + 1 => HwCodec::Hevc, + 2 => HwCodec::Av1, + value => return Err(format!("bad fixture codec {value}")), + }; + let depth = bytes[9]; + let width = u32::from(u16::from_le_bytes(bytes[10..12].try_into().unwrap())); + let height = u32::from(u16::from_le_bytes(bytes[12..14].try_into().unwrap())); + let config_len = u32::from_le_bytes(bytes[14..18].try_into().unwrap()) as usize; + let payload_len = u32::from_le_bytes(bytes[18..22].try_into().unwrap()) as usize; + let expected_len = u32::from_le_bytes(bytes[22..26].try_into().unwrap()) as usize; + let config_end = 26 + config_len; + let payload_end = config_end + payload_len; + let expected_end = payload_end + expected_len; + if expected_end != bytes.len() { + return Err("bad fixture lengths".to_string()); + } + Ok(Fixture { + codec, + depth, + width, + height, + config: &bytes[26..config_end], + payload: &bytes[config_end..payload_end], + expected: &bytes[payload_end..expected_end], + }) + } +} diff --git a/android/mediacodec-harness/settings.gradle.kts b/android/mediacodec-harness/settings.gradle.kts new file mode 100644 index 0000000..e356950 --- /dev/null +++ b/android/mediacodec-harness/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "rawshift-mediacodec-harness" +include(":app") diff --git a/crates/rawshift-hwdec/README.md b/crates/rawshift-hwdec/README.md index 2098724..a8dd48e 100644 --- a/crates/rawshift-hwdec/README.md +++ b/crates/rawshift-hwdec/README.md @@ -8,8 +8,8 @@ MediaCodec (Android). This is the **only** crate in the workspace where platform FFI may live (`#![deny(unsafe_op_in_unsafe_fn)]`, safe public items, documented invariants -on every unsafe block). The **VAAPI backend is implemented**; VideoToolbox -and MediaCodec land as separate issues. On builds/targets with no backend the +on every unsafe block). The **VAAPI and Android MediaCodec backends are +implemented**; VideoToolbox lands separately. On builds/targets with no backend the crate compiles a no-backend stub: `decoder()` returns `None`, `backend()` returns `None`, `available_codecs()` is empty, and dependants surface `HwDecoderUnavailable`. @@ -40,11 +40,29 @@ picks it up through the same dlopen path with no rawshift changes. This is why rawshift has no separate NVDEC backend; see the permanent matrix and justification in [`docs/SUPPORT.md`](../../docs/SUPPORT.md). +## MediaCodec backend (Android API 29+) + +Call `initialize_android_hw_decode(JavaVM)` once before capability queries or +decode. Initialization is idempotent for the same VM, rejects another VM, and +can be retried after a JNI discovery failure. The API is also re-exported by +`rawshift-image` and the `rawshift` facade on Android hardware builds. + +The backend uses Java `MediaCodecList` only to select hardware, non-alias +components, then performs decode with NDK `AMediaCodec` into an `AImageReader` +surface. It tries every eligible component within one five-second deadline, +keeps NDK handles on a dedicated worker, and reuses a session only when the +codec configuration, authoritative sequence header, dimensions, and bit depth +are identical. Output is dense I420 at 8-bit and strict P010 at 9/10-bit; +P010 requires Android API 33+ and is never silently downconverted. + +The reproducible physical-device gate and lossless fixtures live in +[`android/mediacodec-harness`](../../android/mediacodec-harness/README.md). + ## Feature flags (verified) | Feature | Meaning | | --- | --- | -| `hw` | Portable: select the native backend for the compile target (VAAPI on linux-gnu; build-script warning + stub on targets with no hardware decode API). | +| `hw` | Portable: select the native backend for the compile target (VAAPI on linux-gnu, MediaCodec on Android; build-script warning + stub on targets with no hardware decode API). | | `videotoolbox` | Pin VideoToolbox; `compile_error!` on non-Apple targets. | | `vaapi` | Pin VAAPI; `compile_error!` off linux-gnu. | | `mediacodec` | Pin MediaCodec; `compile_error!` off Android. | diff --git a/docs/SUPPORT.md b/docs/SUPPORT.md index 24768e7..7b4adf9 100644 --- a/docs/SUPPORT.md +++ b/docs/SUPPORT.md @@ -21,7 +21,7 @@ upstream dependencies require — it is never raised independently. | `aarch64-unknown-linux-gnu` | 1 (CI build + test) | VAAPI (runtime dlopen) | | | `aarch64-apple-darwin` | 1 (CI build + test) | VideoToolbox | | | `aarch64-apple-ios` | 1 (CI build) | VideoToolbox | device tests are manual | -| `aarch64-linux-android` | 1 (CI build) | MediaCodec | minimum API level fixed when the backend lands | +| `aarch64-linux-android` | 1 (CI build) | MediaCodec | Android API 29+; physical-device gate is manual | | `x86_64-pc-windows-msvc` | 1 (CI build + test) | none (justified below) | HEIC/AVIF pixel decode unavailable until a software AV1 decoder lands upstream; everything else works | | `x86_64-unknown-linux-musl` | 2 (CI build) | none | static deploys; no dlopen | | `wasm32-unknown-unknown` | 2 (CI build) | none | in-memory API only; no hardware decode, threads, or file IO | @@ -39,12 +39,22 @@ fixed: | --- | --- | --- | --- | --- | --- | | VideoToolbox | ✅ in | macOS 11+, iOS 14+ | ✅ | ✅ runtime-probed (M3+ / A17 Pro+ hardware) | system framework | | VAAPI (libva) | ✅ in | Linux (gnu) | ✅ Main / Main10 | ✅ AV1 Main (driver-dependent) | dlopen at runtime — absence degrades to "no decoder", never a link failure | -| MediaCodec (NDK) | ✅ in | Android | ✅ | ✅ (device codec; mandated on newer API levels) | NDK | +| MediaCodec (NDK) | ✅ in | Android API 29+ | ✅ hardware-probed | ✅ hardware-probed | NDK decode; JNI `MediaCodecList` discovery | VAAPI covers Intel and AMD natively, **and NVIDIA via the maintained [`nvidia-vaapi-driver`](https://github.com/elFarto/nvidia-vaapi-driver) translation layer over NVDEC**. +Android uses one decode backend only: NDK `AMediaCodec`, with an +`AImageReader` output surface. A caller explicitly supplies its process +`JavaVM` once so rawshift can use the API-29 `MediaCodecInfo` hardware and +alias classification methods; the selected component is then opened by exact +name through `AMediaCodec`. Eight-bit output is normalized to dense I420. +P010 is strict (no downconversion) and requires API 33+, where the public +MediaCodec P010 capability constant is defined; it is normalized to dense, +MSB-aligned biplanar P010. See `android/mediacodec-harness/README.md` for the +formal physical-device acceptance matrix. + ### Excluded APIs, with justification - **Windows Media Foundation** — deliberate scope decision: HEVC decode From 14729a8b9b35cf104c25d6d15ac496f0c6e354dc Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Wed, 5 Aug 2026 16:59:05 -0400 Subject: [PATCH 4/6] fix(hwdec): resume MediaCodec after flush --- crates/rawshift-hwdec/src/mediacodec/platform.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/rawshift-hwdec/src/mediacodec/platform.rs b/crates/rawshift-hwdec/src/mediacodec/platform.rs index baf465d..f082b0c 100644 --- a/crates/rawshift-hwdec/src/mediacodec/platform.rs +++ b/crates/rawshift-hwdec/src/mediacodec/platform.rs @@ -546,6 +546,12 @@ impl WorkerState { .codec .flush() .map_err(|e| PlatformFailure::new("flush", e))?; + // The NDK contract requires AMediaCodec_start after every flush, + // including for this synchronous, buffer-input decoder. + session + .codec + .start() + .map_err(|e| PlatformFailure::new("restart-after-flush", e))?; session.drain_images()?; } else { self.discard_session(); From 4cb6f5c5fb0065025f23c68e77cbc7b135d0a865 Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Wed, 5 Aug 2026 17:18:53 -0400 Subject: [PATCH 5/6] build(android): pin Java 17 with mise --- android/mediacodec-harness/mise.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 android/mediacodec-harness/mise.toml diff --git a/android/mediacodec-harness/mise.toml b/android/mediacodec-harness/mise.toml new file mode 100644 index 0000000..18090eb --- /dev/null +++ b/android/mediacodec-harness/mise.toml @@ -0,0 +1,2 @@ +[tools] +java = "17" From ce05453c9ac992891ec66cf840408d1b818fae0c Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Wed, 5 Aug 2026 17:26:34 -0400 Subject: [PATCH 6/6] fix(android): register generated JNI libraries --- .../mediacodec-harness/app/build.gradle.kts | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/android/mediacodec-harness/app/build.gradle.kts b/android/mediacodec-harness/app/build.gradle.kts index 7f406e2..95a35d9 100644 --- a/android/mediacodec-harness/app/build.gradle.kts +++ b/android/mediacodec-harness/app/build.gradle.kts @@ -2,6 +2,11 @@ plugins { id("com.android.application") } +abstract class BuildRustTask : Exec() { + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty +} + android { namespace = "org.visualcommons.rawshift.harness" compileSdk = 36 @@ -19,11 +24,9 @@ android { } } - sourceSets.getByName("main").jniLibs.srcDir(layout.buildDirectory.dir("rust-jni-libs")) } -val buildRust by tasks.registering(Exec::class) { - val output = layout.buildDirectory.dir("rust-jni-libs/arm64-v8a") +val buildRust by tasks.registering(BuildRustTask::class) { inputs.files( fileTree("../../..") { include("Cargo.toml", "Cargo.lock") @@ -31,18 +34,26 @@ val buildRust by tasks.registering(Exec::class) { include("crates/rawshift-hwdec/Cargo.toml") include("android/mediacodec-harness/native/**") include("android/mediacodec-harness/fixtures/data/**") + exclude("android/mediacodec-harness/native/target/**") } ) - outputs.dir(output) - commandLine( - "bash", - rootProject.file("build-rust.sh").absolutePath, - output.get().asFile.absolutePath, - ) + outputDirectory.set(layout.buildDirectory.dir("rust-jni-libs")) + doFirst { + commandLine( + "bash", + rootProject.file("build-rust.sh").absolutePath, + outputDirectory.get().dir("arm64-v8a").asFile.absolutePath, + ) + } } -tasks.named("preBuild").configure { - dependsOn(buildRust) +androidComponents { + onVariants { variant -> + variant.sources.jniLibs?.addGeneratedSourceDirectory( + buildRust, + BuildRustTask::outputDirectory, + ) + } } dependencies {