Merge branch 'worktree-agent-a5ec24c82e394a621'
This commit is contained in:
commit
c005493c84
|
|
@ -1164,6 +1164,13 @@ dependencies = [
|
|||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-av1enc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"filmcraft-av1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-cli"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1238,7 +1245,9 @@ dependencies = [
|
|||
name = "effectcraft-export"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"effectcraft-av1enc",
|
||||
"effectcraft-color",
|
||||
"effectcraft-hevcenc",
|
||||
"effectcraft-keyframe",
|
||||
"effectcraft-media",
|
||||
"effectcraft-opusenc",
|
||||
|
|
@ -1305,6 +1314,13 @@ dependencies = [
|
|||
"wgpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-hevcenc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"filmcraft-hevc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-host"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1393,6 +1409,9 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "effectcraft-opusenc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"filmcraft-opus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-path"
|
||||
|
|
|
|||
11
Cargo.toml
11
Cargo.toml
|
|
@ -39,6 +39,8 @@ effectcraft-ui-egui = { path = "crates/ui-egui" }
|
|||
effectcraft-automation = { path = "crates/automation" }
|
||||
effectcraft-opusenc = { path = "crates/opusenc" }
|
||||
effectcraft-script = { path = "crates/script" }
|
||||
effectcraft-hevcenc = { path = "crates/hevcenc" }
|
||||
effectcraft-av1enc = { path = "crates/av1enc" }
|
||||
effectcraft-plugin = { path = "crates/plugin" }
|
||||
|
||||
# foundation
|
||||
|
|
@ -80,6 +82,9 @@ filmcraft-isobmff = { git = "https://github.com/storytold/filmcraft", rev = "89c
|
|||
# decoder used as the round-trip oracle of effectcraft-opusenc (crates/export dev-dependency)
|
||||
filmcraft-opus = { git = "https://github.com/storytold/filmcraft", rev = "89cfbf24f698b0f0e538eb7795929f19bbfb1dbb" }
|
||||
filmcraft-vp9 = { git = "https://github.com/storytold/filmcraft", rev = "89cfbf24f698b0f0e538eb7795929f19bbfb1dbb" }
|
||||
# decoders used as the round-trip oracles of effectcraft-hevcenc / effectcraft-av1enc (dev-dependencies only)
|
||||
filmcraft-hevc = { git = "https://github.com/storytold/filmcraft", rev = "89cfbf24f698b0f0e538eb7795929f19bbfb1dbb" }
|
||||
filmcraft-av1 = { git = "https://github.com/storytold/filmcraft", rev = "89cfbf24f698b0f0e538eb7795929f19bbfb1dbb" }
|
||||
|
||||
# gpu (the same wgpu eframe / egui-wgpu use)
|
||||
wgpu = { version = "30.0.1", features = ["fragile-send-sync-non-atomic-wasm"] }
|
||||
|
|
@ -152,6 +157,12 @@ opt-level = 3
|
|||
[profile.dev.package.effectcraft-vp9enc]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.effectcraft-hevcenc]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.effectcraft-av1enc]
|
||||
opt-level = 3
|
||||
|
||||
# FilmCraft codec hot loops (same settings as FilmCraft's own workspace).
|
||||
[profile.dev.package.filmcraft-h264]
|
||||
opt-level = 3
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@
|
|||
//! effectcraft-cli get <comp> <layer> <path> [--time S] read a property
|
||||
//! effectcraft-cli set <comp> <layer> <path> <value> [--time S] [--expression E]
|
||||
//! effectcraft-cli render-frame [--comp C] [--time S|--frame N] [--max-side PX|--scale K] [--out F.png]
|
||||
//! effectcraft-cli render [--comp C] --out FILE [--format h264|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff] [--start S] [--end S]
|
||||
//! effectcraft-cli render [--comp C] --out FILE [--format h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff] [--start S] [--end S]
|
||||
//! [--work-area] [--fps N] [--resolution full|half|third|quarter|K] [--quality best|draft] [--channels rgb|rgba]
|
||||
//! [--jpeg-quality N] [--bitrate KBPS] [--prores proxy|lt|standard|hq|4444|4444xq] [--audio auto|on|off]
|
||||
//! [--profile main|main10] [--level auto|4.1] [--rate-control bitrate|quality] [--video-quality 1-100]
|
||||
//! [--keyint FRAMES] [--webm-codec vp9|av1] [--audio-bitrate KBPS] [--opus-app audio|voice]
|
||||
//! effectcraft-cli render F.ecproj --queue render the project's Render Queue
|
||||
//! effectcraft-cli bench [--comp C] [--time S] [--scale K] [--n N] [--play N] [--gpu] render timings
|
||||
//! (--gpu: CPU vs GPU ms/frame for every comp at Full and Half)
|
||||
|
|
@ -44,7 +46,11 @@ const USAGE: &str = "usage: effectcraft-cli <info|commands|exec|run|props|get|se
|
|||
render-frame [--comp C] [--time S | --frame N] [--max-side PX | --scale K] [--out F.png]
|
||||
render [--comp C] --out FILE [--format F] [--start S] [--end S] [--work-area] [--fps N]
|
||||
[--resolution full|half|third|quarter|K] [--quality best|draft] [--channels rgb|rgba]
|
||||
[--jpeg-quality N] [--bitrate KBPS] [--prores PROFILE] [--audio auto|on|off] | --queue
|
||||
[--jpeg-quality N] [--bitrate KBPS] [--prores PROFILE] [--audio auto|on|off]
|
||||
[--profile main|main10] [--level auto|4.1] [--rate-control bitrate|quality] [--video-quality N]
|
||||
[--keyint FRAMES] [--webm-codec vp9|av1] [--audio-bitrate KBPS] [--opus-app audio|voice] | --queue
|
||||
(formats h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff;
|
||||
--profile..--keyint: HEVC / AV1, --audio-bitrate/--opus-app: WebM Opus)
|
||||
bench [--comp C] [--time S] [--scale K] [--n N] [--play N] [--gpu] per-layer/effect render timings;
|
||||
--play N renders N consecutive frames with/without the layer cache;
|
||||
--gpu compares CPU and GPU ms/frame for every comp at Full and Half
|
||||
|
|
@ -85,6 +91,14 @@ const VALUED: &[&str] = &[
|
|||
"--bitrate",
|
||||
"--prores",
|
||||
"--audio",
|
||||
"--profile",
|
||||
"--level",
|
||||
"--rate-control",
|
||||
"--video-quality",
|
||||
"--keyint",
|
||||
"--webm-codec",
|
||||
"--audio-bitrate",
|
||||
"--opus-app",
|
||||
];
|
||||
|
||||
struct Args {
|
||||
|
|
@ -546,6 +560,22 @@ fn render(args: &Args, json_out: bool) -> Result<(), Failure> {
|
|||
if let Some(v) = args.num("--bitrate")? {
|
||||
p["bitrate"] = json!(v);
|
||||
}
|
||||
for (flag, key) in [
|
||||
("--profile", "profile"),
|
||||
("--level", "level"),
|
||||
("--rate-control", "rateControl"),
|
||||
("--webm-codec", "webmCodec"),
|
||||
("--opus-app", "opusApplication"),
|
||||
] {
|
||||
if let Some(v) = args.opt(flag) {
|
||||
p[key] = json!(v);
|
||||
}
|
||||
}
|
||||
for (flag, key) in [("--video-quality", "quality"), ("--keyint", "keyframeInterval"), ("--audio-bitrate", "audioBitrate")] {
|
||||
if let Some(v) = args.num(flag)? {
|
||||
p[key] = json!(v);
|
||||
}
|
||||
}
|
||||
// Render exactly this item: unqueue whatever the project's queue already holds.
|
||||
for k in 0..s.project.render_queue.len() {
|
||||
let _ = s.execute("renderQueue.setRender", json!({"index": k + 1, "render": false}));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "effectcraft-av1enc"
|
||||
description = "Clean-room pure-Rust AV1 encoder (Main profile, key and inter frames) for MP4/WebM export, from the AV1 Bitstream & Decoding Process Specification"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dev-dependencies]
|
||||
filmcraft-av1 = { workspace = true }
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# effectcraft-av1enc
|
||||
|
||||
Clean-room, pure-Rust AV1 encoder for EffectCraft's MP4 / WebM export (layer L0, `std` only,
|
||||
no `unsafe`, builds for `wasm32-unknown-unknown`).
|
||||
|
||||
Output is decoded **bit-exactly** by libdav1d (`ffmpeg -c:v libdav1d`, checked in
|
||||
`tests/ffmpeg_oracle.rs` when ffmpeg is installed) and by FilmCraft's spec-derived
|
||||
`filmcraft-av1` decoder (`tests/roundtrip.rs`): every decoded frame equals the encoder's own
|
||||
reconstruction, so prediction never drifts.
|
||||
|
||||
## What is implemented
|
||||
|
||||
- **Stream**: Main profile (`seq_profile` 0), 4:2:0, 8 or 10 bit, BT.709 colour description,
|
||||
limited or full range. Sequence header with one operating point; `seq_level_idx` given or the
|
||||
lowest Annex A level whose MaxPicSize / MaxH/VSize / MaxDisplayRate (and, in bitrate mode, Main
|
||||
tier MaxBitrate) fit. Order hints, film grain, superres, CDEF, loop restoration, intra edge
|
||||
filter, filter intra, inter-intra, masked / distance compound, warped motion and dual filter
|
||||
are disabled in the sequence header.
|
||||
- **Packets**: one temporal unit per frame as stored in MP4 / Matroska samples: OBUs with size
|
||||
fields, no temporal delimiter; key frames start with the sequence header OBU, followed by one
|
||||
`OBU_FRAME` (frame header + tile group). Every frame is shown, in input order.
|
||||
`Encoder::av1c()` returns the `AV1CodecConfigurationRecord` (also the WebM `CodecPrivate`).
|
||||
- **Frames**: a `KEY_FRAME` every `keyint` frames, `INTER_FRAME`s otherwise. Inter frames use
|
||||
slot 0 (the previous reconstruction) for all seven references and refresh slot 0 only, so
|
||||
`LAST_FRAME` is always the previous frame. CDFs adapt per symbol and are saved at the end of
|
||||
every frame; inter frames load them from `LAST_FRAME` (`primary_ref_frame` = 0). One tile,
|
||||
`TX_MODE_LARGEST`, `reduced_tx_set`, no segmentation, no delta q / lf.
|
||||
- **Deblocking**: the normative loop filter (7.14) runs on the reconstruction; the frame level
|
||||
(the same for both luma directions and both chroma planes, no deltas, sharpness 0) is searched
|
||||
per frame for the lowest luma error.
|
||||
- **Tiles**: the multi-symbol arithmetic encoder is the exact inverse of the spec's symbol
|
||||
decoder (8.2), including the exit process padding; CDF adaptation as in 8.2.6.
|
||||
- **Partitions**: 64x64 superblocks, `PARTITION_NONE` / `PARTITION_SPLIT` down to 8x8 chosen by
|
||||
a rate-distortion cost (SSE + λ·estimated bits); blocks crossing the frame edge are split as
|
||||
the syntax requires. 64x64 blocks are only used as inter skip blocks.
|
||||
- **Intra**: DC, V, H, Paeth, smooth, smooth-V, smooth-H for luma and chroma,
|
||||
ranked by SATD with a rate-distortion choice among the best three; the spec's edge preparation
|
||||
and above-right / below-left availability.
|
||||
- **Inter**: single reference (`LAST_FRAME`), `NEWMV` / `NEARESTMV` / `NEARMV` / `GLOBALMV`
|
||||
chosen from the spec's motion vector prediction stack (7.10.2, mirrored exactly); motion search
|
||||
from neighbour / co-located / parent candidates, a whole-sample diamond search and half- then
|
||||
quarter-sample refinement (`allow_high_precision_mv` = 0); the normative 8-tap `EIGHTTAP`
|
||||
prediction for luma and chroma; skip blocks; per-block intra / inter choice.
|
||||
- **Residual**: one transform per plane and block (4x4 ... 32x32): for intra luma `DCT_DCT` or
|
||||
the mode's preferred `ADST` combination (rate-distortion choice over the two best modes), the
|
||||
mode-dependent type for intra chroma, `DCT_DCT` for inter blocks; dead-zone quantisation with the
|
||||
spec's `Dc_Qlookup` / `Ac_Qlookup`; the normative dequantisation and inverse transforms in the
|
||||
reconstruction loop; full coefficient coding (all_zero, tx type, eob_pt, eob_extra,
|
||||
coeff_base_eob, coeff_base, coeff_br, dc_sign, Golomb) with the exact contexts.
|
||||
- **Rate control**: `ConstantQ(base_q_idx)`; `Bitrate { kbps }` adapts `base_q_idx` per frame
|
||||
towards the average target (key frames get a lower quantizer and a larger share; the first
|
||||
key frame is searched in a few passes).
|
||||
|
||||
## Limits
|
||||
|
||||
- One tile: width and height up to 4096 and at most 2304 superblocks (4096x2304 samples).
|
||||
- No CDEF or loop restoration, no compound prediction, no rectangular or 4xN blocks, no
|
||||
directional intra modes other than V / H, no CfL or palette, no transform-size splits, so
|
||||
compression is below mature encoders.
|
||||
- 4:2:0 only (Main profile), no 12 bit.
|
||||
|
||||
## Specification and provenance
|
||||
|
||||
Implemented from the **AV1 Bitstream & Decoding Process Specification, version 1.0.0 with
|
||||
Errata 1** (AOMediaCodec, 2019; the edition FilmCraft's decoder follows) and the **AV1 Codec
|
||||
ISO Media File Format Binding** (v1.2.0) for `av1C`.
|
||||
|
||||
The constant tables (default CDFs, scan orders, quantizer lookup tables, interpolation filter
|
||||
taps, smooth weights, block / transform size tables, ...) and the decoder-side processes the
|
||||
encoder must reproduce exactly (inverse transforms, intra and inter prediction rounding, context
|
||||
derivations, motion vector prediction) are mirrored from FilmCraft's first-party, spec-derived
|
||||
`filmcraft-av1` decoder (MIT OR Apache-2.0), which is also the round-trip oracle. No source of
|
||||
libaom, dav1d, rav1e, SVT-AV1, ffmpeg or any other third-party encoder or decoder was consulted;
|
||||
ffmpeg / libdav1d is used only as an external test oracle.
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
//! MSB-first bit writer for the OBU headers (spec 4.10 `f(n)` descriptors) and `leb128()`.
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct BitWriter {
|
||||
pub buf: Vec<u8>,
|
||||
nbits: usize,
|
||||
}
|
||||
|
||||
impl BitWriter {
|
||||
pub fn new() -> Self {
|
||||
BitWriter::default()
|
||||
}
|
||||
|
||||
/// `f(n)`: writes the low `n` bits of `v`, most significant first.
|
||||
pub fn f(&mut self, n: u32, v: u32) {
|
||||
for i in (0..n).rev() {
|
||||
self.bit((v >> i) & 1 != 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bit(&mut self, b: bool) {
|
||||
if self.nbits.is_multiple_of(8) {
|
||||
self.buf.push(0);
|
||||
}
|
||||
if b {
|
||||
let last = self.buf.len() - 1;
|
||||
self.buf[last] |= 0x80 >> (self.nbits % 8);
|
||||
}
|
||||
self.nbits += 1;
|
||||
}
|
||||
|
||||
/// `byte_alignment()`: zero bits up to the next byte boundary.
|
||||
pub fn byte_align(&mut self) {
|
||||
while !self.nbits.is_multiple_of(8) {
|
||||
self.bit(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// `trailing_bits()`: a one bit followed by zero bits up to the byte boundary.
|
||||
pub fn trailing_bits(&mut self) {
|
||||
self.bit(true);
|
||||
self.byte_align();
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
self.buf
|
||||
}
|
||||
}
|
||||
|
||||
/// `leb128()` encoding of an OBU size.
|
||||
pub(crate) fn leb128(mut v: usize, out: &mut Vec<u8>) {
|
||||
loop {
|
||||
let mut b = (v & 0x7f) as u8;
|
||||
v >>= 7;
|
||||
if v != 0 {
|
||||
b |= 0x80;
|
||||
}
|
||||
out.push(b);
|
||||
if v == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An OBU with `obu_has_size_field` = 1 and no extension header.
|
||||
pub(crate) fn obu(obu_type: u8, payload: &[u8], out: &mut Vec<u8>) {
|
||||
out.push((obu_type << 3) | 0x02);
|
||||
leb128(payload.len(), out);
|
||||
out.extend_from_slice(payload);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bits_and_leb128() {
|
||||
let mut w = BitWriter::new();
|
||||
w.f(3, 0b101);
|
||||
w.f(6, 0b110011);
|
||||
w.trailing_bits();
|
||||
assert_eq!(w.into_bytes(), vec![0b1011_1001, 0b1100_0000]);
|
||||
let mut v = Vec::new();
|
||||
leb128(300, &mut v);
|
||||
assert_eq!(v, vec![0xAC, 0x02]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
//! The adaptive CDFs this encoder codes with (the subset of spec 6.8.2 init_non_coeff_cdfs /
|
||||
//! init_coeff_cdfs that its syntax uses). Layout as in filmcraft-av1: each innermost array
|
||||
//! holds N cumulative values (the last is 32768) plus the adaptation counter.
|
||||
|
||||
use crate::tables::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Cdfs {
|
||||
pub y_mode: [[u16; 14]; 4],
|
||||
pub uv_mode_cfl_not_allowed: [[u16; 14]; 13],
|
||||
pub uv_mode_cfl_allowed: [[u16; 15]; 13],
|
||||
pub angle_delta: [[u16; 8]; 8],
|
||||
pub partition_w8: [[u16; 5]; 4],
|
||||
pub partition_w16: [[u16; 11]; 4],
|
||||
pub partition_w32: [[u16; 11]; 4],
|
||||
pub partition_w64: [[u16; 11]; 4],
|
||||
pub skip: [[u16; 3]; 3],
|
||||
pub is_inter: [[u16; 3]; 4],
|
||||
pub single_ref: [[[u16; 3]; 6]; 3],
|
||||
pub new_mv: [[u16; 3]; 6],
|
||||
pub zero_mv: [[u16; 3]; 2],
|
||||
pub ref_mv: [[u16; 3]; 6],
|
||||
pub drl_mode: [[u16; 3]; 3],
|
||||
pub mv_joint: [u16; 5],
|
||||
pub mv_class: [[u16; 12]; 2],
|
||||
pub mv_class0_bit: [[u16; 3]; 2],
|
||||
pub mv_fr: [[u16; 5]; 2],
|
||||
pub mv_class0_fr: [[[u16; 5]; 2]; 2],
|
||||
pub mv_sign: [[u16; 3]; 2],
|
||||
pub mv_bit: [[[u16; 3]; 10]; 2],
|
||||
pub intra_tx_type_set2: [[[u16; 6]; 13]; 3],
|
||||
pub inter_tx_type_set3: [[u16; 3]; 4],
|
||||
pub txb_skip: [[[u16; 3]; 13]; 5],
|
||||
pub eob_pt_16: [[[u16; 6]; 2]; 2],
|
||||
pub eob_pt_32: [[[u16; 7]; 2]; 2],
|
||||
pub eob_pt_64: [[[u16; 8]; 2]; 2],
|
||||
pub eob_pt_128: [[[u16; 9]; 2]; 2],
|
||||
pub eob_pt_256: [[[u16; 10]; 2]; 2],
|
||||
pub eob_pt_512: [[u16; 11]; 2],
|
||||
pub eob_pt_1024: [[u16; 12]; 2],
|
||||
pub eob_extra: [[[[u16; 3]; 9]; 2]; 5],
|
||||
pub dc_sign: [[[u16; 3]; 3]; 2],
|
||||
pub coeff_base_eob: [[[[u16; 4]; 4]; 2]; 5],
|
||||
pub coeff_base: [[[[u16; 5]; 42]; 2]; 5],
|
||||
pub coeff_br: [[[[u16; 5]; 21]; 2]; 5],
|
||||
}
|
||||
|
||||
impl Cdfs {
|
||||
/// init_non_coeff_cdfs( ) followed by init_coeff_cdfs( ) for `base_q_idx`.
|
||||
pub fn new(base_q_idx: u32) -> Box<Cdfs> {
|
||||
let idx = match base_q_idx {
|
||||
0..=20 => 0,
|
||||
21..=60 => 1,
|
||||
61..=120 => 2,
|
||||
_ => 3,
|
||||
};
|
||||
Box::new(Cdfs {
|
||||
y_mode: DEFAULT_Y_MODE_CDF,
|
||||
uv_mode_cfl_not_allowed: DEFAULT_UV_MODE_CFL_NOT_ALLOWED_CDF,
|
||||
uv_mode_cfl_allowed: DEFAULT_UV_MODE_CFL_ALLOWED_CDF,
|
||||
angle_delta: DEFAULT_ANGLE_DELTA_CDF,
|
||||
partition_w8: DEFAULT_PARTITION_W8_CDF,
|
||||
partition_w16: DEFAULT_PARTITION_W16_CDF,
|
||||
partition_w32: DEFAULT_PARTITION_W32_CDF,
|
||||
partition_w64: DEFAULT_PARTITION_W64_CDF,
|
||||
skip: DEFAULT_SKIP_CDF,
|
||||
is_inter: DEFAULT_IS_INTER_CDF,
|
||||
single_ref: DEFAULT_SINGLE_REF_CDF,
|
||||
new_mv: DEFAULT_NEW_MV_CDF,
|
||||
zero_mv: DEFAULT_ZERO_MV_CDF,
|
||||
ref_mv: DEFAULT_REF_MV_CDF,
|
||||
drl_mode: DEFAULT_DRL_MODE_CDF,
|
||||
mv_joint: DEFAULT_MV_JOINT_CDF,
|
||||
mv_class: DEFAULT_MV_CLASS_CDF,
|
||||
mv_class0_bit: [DEFAULT_MV_CLASS0_BIT_CDF; 2],
|
||||
mv_fr: DEFAULT_MV_FR_CDF,
|
||||
mv_class0_fr: DEFAULT_MV_CLASS0_FR_CDF,
|
||||
mv_sign: [DEFAULT_MV_SIGN_CDF; 2],
|
||||
mv_bit: [DEFAULT_MV_BIT_CDF; 2],
|
||||
intra_tx_type_set2: DEFAULT_INTRA_TX_TYPE_SET2_CDF,
|
||||
inter_tx_type_set3: DEFAULT_INTER_TX_TYPE_SET3_CDF,
|
||||
txb_skip: DEFAULT_TXB_SKIP_CDF[idx],
|
||||
eob_pt_16: DEFAULT_EOB_PT_16_CDF[idx],
|
||||
eob_pt_32: DEFAULT_EOB_PT_32_CDF[idx],
|
||||
eob_pt_64: DEFAULT_EOB_PT_64_CDF[idx],
|
||||
eob_pt_128: DEFAULT_EOB_PT_128_CDF[idx],
|
||||
eob_pt_256: DEFAULT_EOB_PT_256_CDF[idx],
|
||||
eob_pt_512: DEFAULT_EOB_PT_512_CDF[idx],
|
||||
eob_pt_1024: DEFAULT_EOB_PT_1024_CDF[idx],
|
||||
eob_extra: DEFAULT_EOB_EXTRA_CDF[idx],
|
||||
dc_sign: DEFAULT_DC_SIGN_CDF[idx],
|
||||
coeff_base_eob: DEFAULT_COEFF_BASE_EOB_CDF[idx],
|
||||
coeff_base: DEFAULT_COEFF_BASE_CDF[idx],
|
||||
coeff_br: DEFAULT_COEFF_BR_CDF[idx],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Cdfs {
|
||||
/// The counter reset of load_cdfs( ): the last entry of every CDF array is set to 0.
|
||||
pub fn clear_counts(&mut self) {
|
||||
macro_rules! clr {
|
||||
($($f:ident),*) => { $( self.$f.clear_count(); )* };
|
||||
}
|
||||
clr!(
|
||||
y_mode,
|
||||
uv_mode_cfl_not_allowed,
|
||||
uv_mode_cfl_allowed,
|
||||
angle_delta,
|
||||
partition_w8,
|
||||
partition_w16,
|
||||
partition_w32,
|
||||
partition_w64,
|
||||
skip,
|
||||
is_inter,
|
||||
single_ref,
|
||||
new_mv,
|
||||
zero_mv,
|
||||
ref_mv,
|
||||
drl_mode,
|
||||
mv_joint,
|
||||
mv_class,
|
||||
mv_class0_bit,
|
||||
mv_fr,
|
||||
mv_class0_fr,
|
||||
mv_sign,
|
||||
mv_bit,
|
||||
intra_tx_type_set2,
|
||||
inter_tx_type_set3,
|
||||
txb_skip,
|
||||
eob_pt_16,
|
||||
eob_pt_32,
|
||||
eob_pt_64,
|
||||
eob_pt_128,
|
||||
eob_pt_256,
|
||||
eob_pt_512,
|
||||
eob_pt_1024,
|
||||
eob_extra,
|
||||
dc_sign,
|
||||
coeff_base_eob,
|
||||
coeff_base,
|
||||
coeff_br
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Zeroes the adaptation counter (last element) of every innermost CDF array.
|
||||
trait ClearCount {
|
||||
fn clear_count(&mut self);
|
||||
}
|
||||
|
||||
impl<const N: usize> ClearCount for [u16; N] {
|
||||
fn clear_count(&mut self) {
|
||||
self[N - 1] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ClearCount, const M: usize> ClearCount for [T; M] {
|
||||
fn clear_count(&mut self) {
|
||||
for e in self.iter_mut() {
|
||||
e.clear_count();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,874 @@
|
|||
//! Mode decision and reconstruction (not normative): partitions, intra modes, motion vectors,
|
||||
//! transform coefficients. Every prediction and reconstruction step uses the normative
|
||||
//! processes, so the reconstruction built here is exactly what a decoder produces; the syntax
|
||||
//! is written afterwards by `writer.rs`.
|
||||
|
||||
use crate::pred::*;
|
||||
use crate::tables::*;
|
||||
use crate::transform::{forward_2d, inverse_2d};
|
||||
|
||||
/// Decisions for one coded block.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Leaf {
|
||||
pub bsize: usize,
|
||||
pub is_inter: bool,
|
||||
pub y_mode: usize,
|
||||
pub uv_mode: usize,
|
||||
pub tx_type_y: usize,
|
||||
/// Motion vector [row, col] in 1/8 luma samples (inter blocks).
|
||||
pub mv: [i32; 2],
|
||||
pub skip: bool,
|
||||
/// Quantised coefficients of the single transform block of each plane (raster order).
|
||||
pub coefs: [Vec<i32>; 3],
|
||||
}
|
||||
|
||||
pub(crate) enum Node {
|
||||
Out,
|
||||
Leaf(Box<Leaf>),
|
||||
Split(Box<[Node; 4]>),
|
||||
}
|
||||
|
||||
/// A reconstructed frame kept as a reference.
|
||||
pub(crate) struct RefFrame {
|
||||
pub planes: [Plane; 3],
|
||||
/// Motion vectors chosen for the frame (per 4x4), used as search candidates.
|
||||
pub mvs: Vec<[i32; 2]>,
|
||||
/// The frame's loop filter level.
|
||||
pub lf_level: u32,
|
||||
/// The CDFs saved at the end of the frame (loaded by the next frame).
|
||||
pub cdfs: Box<crate::cdf::Cdfs>,
|
||||
}
|
||||
|
||||
pub(crate) struct FrameGeom {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub mi_cols: usize,
|
||||
pub mi_rows: usize,
|
||||
pub sb_cols: usize,
|
||||
pub sb_rows: usize,
|
||||
}
|
||||
|
||||
impl FrameGeom {
|
||||
pub fn new(width: usize, height: usize) -> FrameGeom {
|
||||
let mi_cols = 2 * width.div_ceil(8);
|
||||
let mi_rows = 2 * height.div_ceil(8);
|
||||
FrameGeom { width, height, mi_cols, mi_rows, sb_cols: mi_cols.div_ceil(16), sb_rows: mi_rows.div_ceil(16) }
|
||||
}
|
||||
/// Padded plane size (whole superblocks).
|
||||
pub fn padded(&self, plane: usize) -> (usize, usize) {
|
||||
let s = (plane > 0) as usize;
|
||||
((self.sb_cols * 64) >> s, (self.sb_rows * 64) >> s)
|
||||
}
|
||||
/// Visible plane size.
|
||||
pub fn visible(&self, plane: usize) -> (usize, usize) {
|
||||
if plane == 0 { (self.width, self.height) } else { (self.width.div_ceil(2), self.height.div_ceil(2)) }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tx_size_for(log2: u32) -> usize {
|
||||
match log2 {
|
||||
2 => TX_4X4,
|
||||
3 => TX_8X8,
|
||||
4 => TX_16X16,
|
||||
5 => TX_32X32,
|
||||
_ => TX_64X64,
|
||||
}
|
||||
}
|
||||
|
||||
/// get_tx_set( ) for the square sizes used here with reduced_tx_set = 1.
|
||||
pub(crate) fn tx_set(tx_sz: usize, is_inter: bool) -> usize {
|
||||
let sqr_up = TX_SIZE_SQR_UP[tx_sz] as usize;
|
||||
if sqr_up > TX_32X32 {
|
||||
return TX_SET_DCTONLY;
|
||||
}
|
||||
if is_inter {
|
||||
TX_SET_INTER_3
|
||||
} else if sqr_up == TX_32X32 {
|
||||
TX_SET_DCTONLY
|
||||
} else {
|
||||
TX_SET_INTRA_2
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tx_in_set(set: usize, is_inter: bool, tx_type: usize) -> bool {
|
||||
if is_inter { TX_TYPE_IN_SET_INTER[set][tx_type] == 1 } else { TX_TYPE_IN_SET_INTRA[set][tx_type] == 1 }
|
||||
}
|
||||
|
||||
/// The chroma transform type of an intra block (compute_tx_type for plane > 0).
|
||||
pub(crate) fn intra_uv_tx_type(uv_mode: usize, uv_tx: usize) -> usize {
|
||||
let t = MODE_TO_TXFM[uv_mode] as usize;
|
||||
if tx_in_set(tx_set(uv_tx, false), false, t) { t } else { DCT_DCT }
|
||||
}
|
||||
|
||||
struct TxResult {
|
||||
levels: Vec<i32>,
|
||||
nonzero: bool,
|
||||
dist: f64,
|
||||
bits: f64,
|
||||
}
|
||||
|
||||
/// Saved encoder state of a block area (reconstruction, BlockDecoded flags).
|
||||
struct Snapshot {
|
||||
planes: [Vec<u16>; 3],
|
||||
decoded: Box<[[[bool; 35]; 35]; 3]>,
|
||||
}
|
||||
|
||||
pub(crate) struct Decider<'a> {
|
||||
pub g: &'a FrameGeom,
|
||||
pub bd: u32,
|
||||
pub intra_only: bool,
|
||||
pub src: &'a [Plane; 3],
|
||||
pub rec: [Plane; 3],
|
||||
pub refr: Option<&'a RefFrame>,
|
||||
decoded: Box<[[[bool; 35]; 35]; 3]>,
|
||||
/// Decision motion vectors per 4x4 (search candidates).
|
||||
pub mvs: Vec<[i32; 2]>,
|
||||
dc_q: f32,
|
||||
ac_q: f32,
|
||||
dc_qi: i64,
|
||||
ac_qi: i64,
|
||||
lambda: f64,
|
||||
lambda_sad: f64,
|
||||
/// Half-sample interpolations of the reference luma (visible area): (8, 0), (0, 8), (8, 8).
|
||||
half: [Vec<u16>; 3],
|
||||
}
|
||||
|
||||
const MODE_BITS_INTRA: f64 = 3.0;
|
||||
|
||||
impl<'a> Decider<'a> {
|
||||
pub fn new(g: &'a FrameGeom, bd: u32, qidx: u8, intra_only: bool, src: &'a [Plane; 3], refr: Option<&'a RefFrame>) -> Self {
|
||||
let bdi = ((bd - 8) >> 1) as usize;
|
||||
let dc_qi = DC_QLOOKUP[bdi][qidx as usize] as i64;
|
||||
let ac_qi = AC_QLOOKUP[bdi][qidx as usize] as i64;
|
||||
// Lagrangian multiplier in squared-sample units of the coded bit depth.
|
||||
let step = ac_qi as f64 / 8.0;
|
||||
let lambda = 0.25 * step * step;
|
||||
let rec = std::array::from_fn(|p| {
|
||||
let (w, h) = g.padded(p);
|
||||
Plane::new(w, h)
|
||||
});
|
||||
Decider {
|
||||
g,
|
||||
bd,
|
||||
intra_only,
|
||||
src,
|
||||
rec,
|
||||
refr,
|
||||
decoded: Box::new([[[false; 35]; 35]; 3]),
|
||||
mvs: vec![[0, 0]; g.mi_cols * g.mi_rows],
|
||||
dc_q: dc_qi as f32,
|
||||
ac_q: ac_qi as f32,
|
||||
dc_qi,
|
||||
ac_qi,
|
||||
lambda,
|
||||
lambda_sad: lambda.sqrt(),
|
||||
half: match refr {
|
||||
Some(rf) => [(8, 0), (0, 8), (8, 8)].map(|(fx, fy)| subpel_plane(&rf.planes[0], g.width, g.height, fx, fy, bd)),
|
||||
None => Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Decides one 64x64 superblock (reconstruction is written to `rec`).
|
||||
pub fn superblock(&mut self, r: usize, c: usize) -> Node {
|
||||
// clear_block_decoded_flags( r, c, sbSize4 )
|
||||
for plane in 0..3 {
|
||||
let s = (plane > 0) as usize;
|
||||
let sb_w4 = (self.g.mi_cols as isize - c as isize) >> s;
|
||||
let sb_h4 = (self.g.mi_rows as isize - r as isize) >> s;
|
||||
let bd = &mut self.decoded[plane];
|
||||
for y in -1..=((16 >> s) as isize) {
|
||||
for x in -1..=((16 >> s) as isize) {
|
||||
let v = if y < 0 && x < sb_w4 { true } else { x < 0 && y < sb_h4 };
|
||||
bd[(y + 1) as usize][(x + 1) as usize] = v;
|
||||
}
|
||||
}
|
||||
bd[(16 >> s) + 1][0] = false;
|
||||
}
|
||||
self.partition(r, c, BLOCK_64X64, None).0
|
||||
}
|
||||
|
||||
fn decoded_at(&self, plane: usize, y: isize, x: isize) -> bool {
|
||||
let (yy, xx) = (y + 1, x + 1);
|
||||
if yy < 0 || xx < 0 || yy > 34 || xx > 34 {
|
||||
return false;
|
||||
}
|
||||
self.decoded[plane][yy as usize][xx as usize]
|
||||
}
|
||||
|
||||
/// Marks the transform block of `plane` covering the block at (r, c) as decoded.
|
||||
fn mark_decoded(&mut self, plane: usize, r: usize, c: usize, n4: usize) {
|
||||
let s = (plane > 0) as usize;
|
||||
let by = (r & 15) >> s;
|
||||
let bx = (c & 15) >> s;
|
||||
let step = (n4 >> s).max(1);
|
||||
for i in 0..step {
|
||||
for j in 0..step {
|
||||
if by + i + 1 < 35 && bx + j + 1 < 35 {
|
||||
self.decoded[plane][by + i + 1][bx + j + 1] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot(&self, r: usize, c: usize, n4: usize) -> Snapshot {
|
||||
let planes = std::array::from_fn(|p| {
|
||||
let s = (p > 0) as usize;
|
||||
let (x0, y0, n) = ((c * 4) >> s, (r * 4) >> s, (n4 * 4) >> s);
|
||||
let pl = &self.rec[p];
|
||||
let mut v = Vec::with_capacity(n * n);
|
||||
for y in y0..y0 + n {
|
||||
v.extend_from_slice(&pl.data[y * pl.stride + x0..y * pl.stride + x0 + n]);
|
||||
}
|
||||
v
|
||||
});
|
||||
Snapshot { planes, decoded: self.decoded.clone() }
|
||||
}
|
||||
|
||||
fn restore(&mut self, snap: &Snapshot, r: usize, c: usize, n4: usize) {
|
||||
for p in 0..3 {
|
||||
let s = (p > 0) as usize;
|
||||
let (x0, y0, n) = ((c * 4) >> s, (r * 4) >> s, (n4 * 4) >> s);
|
||||
let pl = &mut self.rec[p];
|
||||
for (i, y) in (y0..y0 + n).enumerate() {
|
||||
pl.data[y * pl.stride + x0..y * pl.stride + x0 + n].copy_from_slice(&snap.planes[p][i * n..i * n + n]);
|
||||
}
|
||||
}
|
||||
self.decoded.clone_from(&snap.decoded);
|
||||
}
|
||||
|
||||
fn set_mvs(&mut self, r: usize, c: usize, n4: usize, mv: [i32; 2]) {
|
||||
for y in r..(r + n4).min(self.g.mi_rows) {
|
||||
for x in c..(c + n4).min(self.g.mi_cols) {
|
||||
self.mvs[y * self.g.mi_cols + x] = mv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn partition(&mut self, r: usize, c: usize, bsize: usize, parent_mv: Option<[i32; 2]>) -> (Node, f64) {
|
||||
let g = self.g;
|
||||
if r >= g.mi_rows || c >= g.mi_cols {
|
||||
return (Node::Out, 0.0);
|
||||
}
|
||||
let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize;
|
||||
let half = n4 >> 1;
|
||||
let has_rows = r + half < g.mi_rows;
|
||||
let has_cols = c + half < g.mi_cols;
|
||||
let can_none = has_rows && has_cols && (bsize != BLOCK_64X64 || !self.intra_only);
|
||||
let can_split = bsize > BLOCK_8X8;
|
||||
let sub = PARTITION_SUBSIZE[PARTITION_SPLIT][bsize] as usize;
|
||||
if !can_none {
|
||||
return self.split(r, c, sub, half, parent_mv);
|
||||
}
|
||||
let before = self.snapshot(r, c, n4);
|
||||
let (leaf, cost_none) = self.leaf(r, c, bsize, parent_mv);
|
||||
let cost_none = cost_none + self.lambda * 1.0;
|
||||
if !can_split {
|
||||
return (Node::Leaf(Box::new(leaf)), cost_none);
|
||||
}
|
||||
// a cheap skip block rarely gains from splitting
|
||||
if leaf.skip && cost_none < self.lambda * 8.0 {
|
||||
return (Node::Leaf(Box::new(leaf)), cost_none);
|
||||
}
|
||||
let after_none = self.snapshot(r, c, n4);
|
||||
self.restore(&before, r, c, n4);
|
||||
let pm = if leaf.is_inter { Some(leaf.mv) } else { parent_mv };
|
||||
let (node, cost_split) = self.split(r, c, sub, half, pm);
|
||||
let cost_split = cost_split + self.lambda * 2.0;
|
||||
if cost_split < cost_none {
|
||||
(node, cost_split)
|
||||
} else {
|
||||
self.restore(&after_none, r, c, n4);
|
||||
self.set_mvs(r, c, n4, if leaf.is_inter { leaf.mv } else { [0, 0] });
|
||||
(Node::Leaf(Box::new(leaf)), cost_none)
|
||||
}
|
||||
}
|
||||
|
||||
fn split(&mut self, r: usize, c: usize, sub: usize, half: usize, pm: Option<[i32; 2]>) -> (Node, f64) {
|
||||
let (n0, c0) = self.partition(r, c, sub, pm);
|
||||
let (n1, c1) = self.partition(r, c + half, sub, pm);
|
||||
let (n2, c2) = self.partition(r + half, c, sub, pm);
|
||||
let (n3, c3) = self.partition(r + half, c + half, sub, pm);
|
||||
(Node::Split(Box::new([n0, n1, n2, n3])), c0 + c1 + c2 + c3)
|
||||
}
|
||||
|
||||
/// Best coding of the block as a single (NONE partition) block.
|
||||
fn leaf(&mut self, r: usize, c: usize, bsize: usize, parent_mv: Option<[i32; 2]>) -> (Leaf, f64) {
|
||||
let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize;
|
||||
if self.intra_only {
|
||||
let res = self.intra_leaf(r, c, bsize);
|
||||
self.set_mvs(r, c, n4, [0, 0]);
|
||||
return res;
|
||||
}
|
||||
let before = self.snapshot(r, c, n4);
|
||||
let (inter, cost_inter) = self.inter_leaf(r, c, bsize, parent_mv);
|
||||
if bsize == BLOCK_64X64 || inter.skip {
|
||||
self.set_mvs(r, c, n4, inter.mv);
|
||||
return (inter, cost_inter);
|
||||
}
|
||||
let after_inter = self.snapshot(r, c, n4);
|
||||
self.restore(&before, r, c, n4);
|
||||
let (intra, cost_intra) = self.intra_leaf(r, c, bsize);
|
||||
if cost_intra < cost_inter {
|
||||
self.set_mvs(r, c, n4, [0, 0]);
|
||||
(intra, cost_intra)
|
||||
} else {
|
||||
self.restore(&after_inter, r, c, n4);
|
||||
self.set_mvs(r, c, n4, inter.mv);
|
||||
(inter, cost_inter)
|
||||
}
|
||||
}
|
||||
|
||||
fn intra_leaf(&mut self, r: usize, c: usize, bsize: usize) -> (Leaf, f64) {
|
||||
let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize;
|
||||
let log2 = (n4 * 4).trailing_zeros();
|
||||
let (sbr, sbc) = ((r & 15) as isize, (c & 15) as isize);
|
||||
let bd = self.bd;
|
||||
let mut pred = vec![0u16; 32 * 32];
|
||||
let mut best_pred = vec![0u16; 32 * 32];
|
||||
let n = 1usize << log2;
|
||||
// luma
|
||||
let e = IntraEdge {
|
||||
x: c * 4,
|
||||
y: r * 4,
|
||||
have_left: c > 0,
|
||||
have_above: r > 0,
|
||||
have_above_right: self.decoded_at(0, sbr - 1, sbc + n4 as isize),
|
||||
have_below_left: self.decoded_at(0, sbr + n4 as isize, sbc - 1),
|
||||
log2,
|
||||
max_x: (self.g.mi_cols * 4) as i32 - 1,
|
||||
max_y: (self.g.mi_rows * 4) as i32 - 1,
|
||||
bit_depth: bd,
|
||||
};
|
||||
let ed = edges(&self.rec[0], &e);
|
||||
// modes ranked by SATD, then a rate-distortion choice of mode and transform type among
|
||||
// the best three
|
||||
let mut ranked: Vec<(f64, usize)> = INTRA_MODES_USED
|
||||
.iter()
|
||||
.map(|&mode| {
|
||||
predict_intra(&ed, &e, mode, &mut pred);
|
||||
let sad = self.satd(0, c * 4, r * 4, n, &pred[..n * n]) as f64;
|
||||
(sad + self.lambda_sad * if mode == DC_PRED { 1.0 } else { MODE_BITS_INTRA }, mode)
|
||||
})
|
||||
.collect();
|
||||
ranked.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let tx_y = tx_size_for(log2);
|
||||
let mut best: Option<(f64, usize, usize, TxResult, Option<Vec<u16>>)> = None;
|
||||
for &(_, mode) in ranked.iter().take(3) {
|
||||
predict_intra(&ed, &e, mode, &mut pred);
|
||||
let mut types = vec![DCT_DCT];
|
||||
let alt = MODE_TO_TXFM[mode] as usize;
|
||||
if alt != DCT_DCT && tx_in_set(tx_set(tx_y, false), false, alt) {
|
||||
types.push(alt);
|
||||
}
|
||||
for &t in &types {
|
||||
let (res, recon) = self.try_tx(0, c * 4, r * 4, log2, t, &pred[..n * n], true);
|
||||
let type_bits = if !res.nonzero || tx_set(tx_y, false) == TX_SET_DCTONLY {
|
||||
0.0
|
||||
} else if t == DCT_DCT {
|
||||
1.0
|
||||
} else {
|
||||
2.0
|
||||
};
|
||||
let mode_bits = if mode == DC_PRED { 1.0 } else { MODE_BITS_INTRA };
|
||||
let cost = res.dist + self.lambda * (res.bits + type_bits + mode_bits);
|
||||
if best.as_ref().is_none_or(|b| cost < b.0) {
|
||||
best_pred[..n * n].copy_from_slice(&pred[..n * n]);
|
||||
best = Some((cost, mode, t, res, recon));
|
||||
}
|
||||
}
|
||||
}
|
||||
let (_, y_mode, tx_type_y, ty, recon) = best.expect("an intra mode");
|
||||
self.put(0, c * 4, r * 4, n, recon.as_deref().unwrap_or(&best_pred[..n * n]));
|
||||
let tx_type_y = if ty.nonzero { tx_type_y } else { DCT_DCT };
|
||||
self.mark_decoded(0, r, c, n4);
|
||||
// chroma (one mode for both planes)
|
||||
let cn = n >> 1;
|
||||
let clog2 = log2 - 1;
|
||||
let (csbr, csbc) = (sbr >> 1, sbc >> 1);
|
||||
let ce: [IntraEdge; 2] = std::array::from_fn(|i| IntraEdge {
|
||||
x: c * 2,
|
||||
y: r * 2,
|
||||
have_left: c > 0,
|
||||
have_above: r > 0,
|
||||
have_above_right: self.decoded_at(1 + i, csbr - 1, csbc + (n4 >> 1) as isize),
|
||||
have_below_left: self.decoded_at(1 + i, csbr + (n4 >> 1) as isize, csbc - 1),
|
||||
log2: clog2,
|
||||
max_x: (self.g.mi_cols * 2) as i32 - 1,
|
||||
max_y: (self.g.mi_rows * 2) as i32 - 1,
|
||||
bit_depth: bd,
|
||||
});
|
||||
let ced = [edges(&self.rec[1], &ce[0]), edges(&self.rec[2], &ce[1])];
|
||||
let mut best_uv = (f64::MAX, DC_PRED);
|
||||
for &mode in INTRA_MODES_USED.iter() {
|
||||
let mut sad = 0u64;
|
||||
for i in 0..2 {
|
||||
predict_intra(&ced[i], &ce[i], mode, &mut pred);
|
||||
sad += self.satd(1 + i, c * 2, r * 2, cn, &pred[..cn * cn]);
|
||||
}
|
||||
let cost = sad as f64 + self.lambda_sad * if mode == DC_PRED { 1.0 } else { 2.5 };
|
||||
if cost < best_uv.0 {
|
||||
best_uv = (cost, mode);
|
||||
}
|
||||
}
|
||||
let uv_mode = best_uv.1;
|
||||
let uv_tx = tx_size_for(clog2);
|
||||
let uv_type = intra_uv_tx_type(uv_mode, uv_tx);
|
||||
let mut tuv = Vec::with_capacity(2);
|
||||
for i in 0..2 {
|
||||
predict_intra(&ced[i], &ce[i], uv_mode, &mut pred);
|
||||
tuv.push(self.code_tx(1 + i, c * 2, r * 2, clog2, uv_type, &pred[..cn * cn], true));
|
||||
self.mark_decoded(1 + i, r, c, n4);
|
||||
}
|
||||
let skip = !ty.nonzero && !tuv[0].nonzero && !tuv[1].nonzero;
|
||||
let dist = ty.dist + tuv[0].dist + tuv[1].dist;
|
||||
let bits = if skip { 1.0 } else { ty.bits + tuv[0].bits + tuv[1].bits + 1.0 } + MODE_BITS_INTRA + 2.0;
|
||||
let [u, v]: [TxResult; 2] = tuv.try_into().ok().expect("two chroma planes");
|
||||
let leaf = Leaf { bsize, is_inter: false, y_mode, uv_mode, tx_type_y, mv: [0, 0], skip, coefs: [ty.levels, u.levels, v.levels] };
|
||||
(leaf, dist + self.lambda * bits)
|
||||
}
|
||||
|
||||
fn inter_leaf(&mut self, r: usize, c: usize, bsize: usize, parent_mv: Option<[i32; 2]>) -> (Leaf, f64) {
|
||||
let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize;
|
||||
let n = n4 * 4;
|
||||
let log2 = n.trailing_zeros();
|
||||
let (mv, mv_bits) = self.motion_search(r, c, n, parent_mv);
|
||||
let refr = self.refr.expect("reference frame");
|
||||
let mut pred = vec![0u16; n * n];
|
||||
let mut tx = Vec::with_capacity(3);
|
||||
let mut dist = 0.0;
|
||||
for p in 0..3 {
|
||||
let s = (p > 0) as usize;
|
||||
let (vw, vh) = self.g.visible(p);
|
||||
let pn = n >> s;
|
||||
let (x, y) = ((c * 4) >> s, (r * 4) >> s);
|
||||
predict_inter(&refr.planes[p], vw as i32 - 1, vh as i32 - 1, x, y, pn, pn, mv, s, self.bd, &mut pred[..pn * pn]);
|
||||
if bsize == BLOCK_64X64 {
|
||||
// coded as a skip block: the prediction is the reconstruction
|
||||
dist += self.sse(p, x, y, pn, &pred[..pn * pn]);
|
||||
self.put(p, x, y, pn, &pred[..pn * pn]);
|
||||
} else {
|
||||
let t = self.code_tx(p, x, y, log2 - s as u32, DCT_DCT, &pred[..pn * pn], false);
|
||||
dist += t.dist;
|
||||
tx.push(t);
|
||||
}
|
||||
self.mark_decoded(p, r, c, n4);
|
||||
}
|
||||
let mode_bits = if mv == [0, 0] { 1.5 } else { 2.0 + mv_bits };
|
||||
if bsize == BLOCK_64X64 {
|
||||
let leaf = Leaf { bsize, is_inter: true, y_mode: NEWMV, uv_mode: DC_PRED, tx_type_y: DCT_DCT, mv, skip: true, coefs: Default::default() };
|
||||
return (leaf, dist + self.lambda * (mode_bits + 1.0));
|
||||
}
|
||||
let skip = tx.iter().all(|t| !t.nonzero);
|
||||
let bits = mode_bits + if skip { 1.0 } else { 1.0 + tx.iter().map(|t| t.bits).sum::<f64>() };
|
||||
let [y, u, v]: [TxResult; 3] = tx.try_into().ok().expect("three planes");
|
||||
let leaf = Leaf { bsize, is_inter: true, y_mode: NEWMV, uv_mode: DC_PRED, tx_type_y: DCT_DCT, mv, skip, coefs: [y.levels, u.levels, v.levels] };
|
||||
(leaf, dist + self.lambda * bits)
|
||||
}
|
||||
|
||||
/// Full-sample motion search: candidate vectors then a shrinking diamond search.
|
||||
/// Returns the vector and an estimate of its coding cost in bits.
|
||||
fn motion_search(&self, r: usize, c: usize, n: usize, parent_mv: Option<[i32; 2]>) -> ([i32; 2], f64) {
|
||||
let g = self.g;
|
||||
let refr = self.refr.expect("reference frame");
|
||||
let mi = |rr: usize, cc: usize| self.mvs[rr * g.mi_cols + cc];
|
||||
let mut pred_mv = [0, 0];
|
||||
let mut cands: Vec<[i32; 2]> = vec![[0, 0]];
|
||||
if c > 0 {
|
||||
pred_mv = mi(r, c - 1);
|
||||
cands.push(pred_mv);
|
||||
}
|
||||
if r > 0 {
|
||||
cands.push(mi(r - 1, c));
|
||||
let cr = c + n / 4;
|
||||
if cr < g.mi_cols {
|
||||
cands.push(mi(r - 1, cr));
|
||||
}
|
||||
}
|
||||
if let Some(p) = parent_mv {
|
||||
cands.push(p);
|
||||
}
|
||||
cands.push(refr.mvs[r * g.mi_cols + c]);
|
||||
let (x0, y0) = ((c * 4) as i32, (r * 4) as i32);
|
||||
let (w, h) = (g.width as i32, g.height as i32);
|
||||
// keep the referenced block within 64 samples of the picture
|
||||
let lim_x = (-x0 - n as i32 - 64, w - x0 + 64);
|
||||
let lim_y = (-y0 - n as i32 - 64, h - y0 + 64);
|
||||
let valid = |m: [i32; 2]| {
|
||||
let (dy, dx) = (m[0] >> 3, m[1] >> 3);
|
||||
dx >= lim_x.0 && dx <= lim_x.1 && dy >= lim_y.0 && dy <= lim_y.1
|
||||
};
|
||||
let mv_bits = |m: [i32; 2]| -> f64 {
|
||||
let comp = |d: i32| if d == 0 { 0.5 } else { 2.0 + 2.0 * ((d.abs() / 8) as f64 + 1.0).log2() };
|
||||
comp(m[0] - pred_mv[0]) + comp(m[1] - pred_mv[1])
|
||||
};
|
||||
let cost =
|
||||
|m: [i32; 2]| -> f64 { self.sad_ref(&refr.planes[0], x0 + (m[1] >> 3), y0 + (m[0] >> 3), c * 4, r * 4, n) as f64 + self.lambda_sad * mv_bits(m) };
|
||||
let mut best = ([0, 0], f64::MAX);
|
||||
for &m in &cands {
|
||||
let m = [m[0] & !7, m[1] & !7];
|
||||
if !valid(m) {
|
||||
continue;
|
||||
}
|
||||
let cst = cost(m);
|
||||
if cst < best.1 {
|
||||
best = (m, cst);
|
||||
}
|
||||
}
|
||||
let mut step = 8 * if n >= 32 {
|
||||
16
|
||||
} else if n == 16 {
|
||||
8
|
||||
} else {
|
||||
4
|
||||
};
|
||||
while step >= 8 {
|
||||
let mut moved = true;
|
||||
let mut iters = 0;
|
||||
while moved && iters < 16 {
|
||||
moved = false;
|
||||
iters += 1;
|
||||
let center = best.0;
|
||||
for d in [[-step, 0], [step, 0], [0, -step], [0, step]] {
|
||||
let m = [center[0] + d[0], center[1] + d[1]];
|
||||
if !valid(m) {
|
||||
continue;
|
||||
}
|
||||
let cst = cost(m);
|
||||
if cst < best.1 {
|
||||
best = (m, cst);
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
step >>= 1;
|
||||
}
|
||||
// half- then quarter-sample refinement (plus the exact neighbour candidates)
|
||||
let mut buf = vec![0u16; n * n];
|
||||
let sub_cost = |m: [i32; 2], buf: &mut Vec<u16>| -> f64 {
|
||||
self.search_block(m, c * 4, r * 4, n, buf);
|
||||
self.sad(0, c * 4, r * 4, n, buf) as f64 + self.lambda_sad * mv_bits(m)
|
||||
};
|
||||
for m in cands {
|
||||
if m[0] & 7 != 0 || m[1] & 7 != 0 {
|
||||
let m = [m[0] & !1, m[1] & !1];
|
||||
if valid(m) {
|
||||
let cst = sub_cost(m, &mut buf);
|
||||
if cst < best.1 {
|
||||
best = (m, cst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for step in [4, 2] {
|
||||
let center = best.0;
|
||||
for d in [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]] {
|
||||
let m = [center[0] + d[0] * step, center[1] + d[1] * step];
|
||||
if !valid(m) {
|
||||
continue;
|
||||
}
|
||||
let cst = sub_cost(m, &mut buf);
|
||||
if cst < best.1 {
|
||||
best = (m, cst);
|
||||
}
|
||||
}
|
||||
}
|
||||
(best.0, mv_bits(best.0))
|
||||
}
|
||||
|
||||
/// Approximate luma prediction for motion search: exact at whole and half-sample
|
||||
/// positions (precomputed planes), averaged neighbours at quarter-sample positions.
|
||||
fn search_block(&self, mv: [i32; 2], x: usize, y: usize, n: usize, out: &mut [u16]) {
|
||||
let refr = self.refr.expect("reference frame");
|
||||
let (w, h) = (self.g.width as i32, self.g.height as i32);
|
||||
// per axis: up to two (integer offset, half) taps
|
||||
let axis = |v: i32| -> ([(i32, bool); 2], usize) {
|
||||
let p = 2 * v; // 1/16 units
|
||||
let (i, f) = (p >> 4, p & 15);
|
||||
match f {
|
||||
0 => ([(i, false), (i, false)], 1),
|
||||
8 => ([(i, true), (i, true)], 1),
|
||||
4 => ([(i, false), (i, true)], 2),
|
||||
_ => ([(i, true), (i + 1, false)], 2),
|
||||
}
|
||||
};
|
||||
let (ax, nx) = axis(mv[1]);
|
||||
let (ay, ny) = axis(mv[0]);
|
||||
let taps = nx.max(ny);
|
||||
let fetch = |k: usize, xx: i32, yy: i32| -> u16 {
|
||||
let (ox, hx) = ax[k.min(nx - 1)];
|
||||
let (oy, hy) = ay[k.min(ny - 1)];
|
||||
let sx = (xx + ox).clamp(0, w - 1) as usize;
|
||||
let sy = (yy + oy).clamp(0, h - 1) as usize;
|
||||
match (hx, hy) {
|
||||
(false, false) => refr.planes[0].at(sx, sy),
|
||||
(true, false) => self.half[0][sy * w as usize + sx],
|
||||
(false, true) => self.half[1][sy * w as usize + sx],
|
||||
(true, true) => self.half[2][sy * w as usize + sx],
|
||||
}
|
||||
};
|
||||
let vw = n.min(self.g.width.saturating_sub(x));
|
||||
let vh = n.min(self.g.height.saturating_sub(y));
|
||||
// fast path: every tap inside the picture
|
||||
let (x0, y0) = (x as i32, y as i32);
|
||||
let inside = (0..taps).all(|k| {
|
||||
let (ox, _) = ax[k.min(nx - 1)];
|
||||
let (oy, _) = ay[k.min(ny - 1)];
|
||||
x0 + ox >= 0 && y0 + oy >= 0 && x0 + ox + vw as i32 <= w && y0 + oy + vh as i32 <= h
|
||||
});
|
||||
if inside {
|
||||
let src = |k: usize| -> (&[u16], usize, usize) {
|
||||
let (ox, hx) = ax[k.min(nx - 1)];
|
||||
let (oy, hy) = ay[k.min(ny - 1)];
|
||||
let (sx, sy) = ((x0 + ox) as usize, (y0 + oy) as usize);
|
||||
match (hx, hy) {
|
||||
(false, false) => (&refr.planes[0].data, refr.planes[0].stride, sy * refr.planes[0].stride + sx),
|
||||
(true, false) => (&self.half[0], w as usize, sy * w as usize + sx),
|
||||
(false, true) => (&self.half[1], w as usize, sy * w as usize + sx),
|
||||
(true, true) => (&self.half[2], w as usize, sy * w as usize + sx),
|
||||
}
|
||||
};
|
||||
let (a, sa, oa) = src(0);
|
||||
if taps == 1 {
|
||||
for i in 0..vh {
|
||||
out[i * n..i * n + vw].copy_from_slice(&a[oa + i * sa..oa + i * sa + vw]);
|
||||
}
|
||||
} else {
|
||||
let (b, sb, ob) = src(1);
|
||||
for i in 0..vh {
|
||||
let (ra, rb) = (&a[oa + i * sa..oa + i * sa + vw], &b[ob + i * sb..ob + i * sb + vw]);
|
||||
for (o, (&p, &q)) in out[i * n..i * n + vw].iter_mut().zip(ra.iter().zip(rb)) {
|
||||
*o = ((p as u32 + q as u32 + 1) >> 1) as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
for i in 0..vh {
|
||||
for j in 0..vw {
|
||||
let (xx, yy) = (x0 + j as i32, y0 + i as i32);
|
||||
out[i * n + j] = if taps == 1 { fetch(0, xx, yy) } else { ((fetch(0, xx, yy) as u32 + fetch(1, xx, yy) as u32 + 1) >> 1) as u16 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SAD of the source block at (x, y) (luma) against the reference block at (rx, ry).
|
||||
fn sad_ref(&self, refp: &Plane, rx: i32, ry: i32, x: usize, y: usize, n: usize) -> u64 {
|
||||
let (vw, vh) = (self.g.width, self.g.height);
|
||||
let w = n.min(vw.saturating_sub(x));
|
||||
let h = n.min(vh.saturating_sub(y));
|
||||
let src = &self.src[0];
|
||||
let (lx, ly) = (vw as i32 - 1, vh as i32 - 1);
|
||||
let mut sad = 0u64;
|
||||
let inside = rx >= 0 && ry >= 0 && rx + w as i32 - 1 <= lx && ry + h as i32 - 1 <= ly;
|
||||
for i in 0..h {
|
||||
let s = &src.row(y + i)[x..x + w];
|
||||
let rr = refp.row((ry + i as i32).clamp(0, ly) as usize);
|
||||
if inside {
|
||||
let rrow = &rr[rx as usize..rx as usize + w];
|
||||
sad += s.iter().zip(rrow).map(|(&a, &b)| (a as i32 - b as i32).unsigned_abs()).sum::<u32>() as u64;
|
||||
} else {
|
||||
for j in 0..w {
|
||||
let b = rr[(rx + j as i32).clamp(0, lx) as usize];
|
||||
sad += (s[j] as i32 - b as i32).unsigned_abs() as u64;
|
||||
}
|
||||
}
|
||||
}
|
||||
sad
|
||||
}
|
||||
|
||||
/// Sum of absolute Hadamard-transformed differences (4x4 or 8x8 sub-blocks) of a prediction
|
||||
/// against the source, scaled to about the size of the SAD.
|
||||
fn satd(&self, p: usize, x: usize, y: usize, n: usize, pred: &[u16]) -> u64 {
|
||||
let src = &self.src[p];
|
||||
let k = n.min(8);
|
||||
let mut total = 0u64;
|
||||
let mut d = [0i32; 64];
|
||||
for by in (0..n).step_by(k) {
|
||||
for bx in (0..n).step_by(k) {
|
||||
for i in 0..k {
|
||||
let row = &src.row(y + by + i)[x + bx..x + bx + k];
|
||||
for j in 0..k {
|
||||
d[i * k + j] = row[j] as i32 - pred[(by + i) * n + bx + j] as i32;
|
||||
}
|
||||
}
|
||||
for i in 0..k {
|
||||
hadamard(&mut d[i * k..i * k + k]);
|
||||
}
|
||||
let mut col = [0i32; 8];
|
||||
let mut s = 0u64;
|
||||
for j in 0..k {
|
||||
for i in 0..k {
|
||||
col[i] = d[i * k + j];
|
||||
}
|
||||
hadamard(&mut col[..k]);
|
||||
s += col[..k].iter().map(|v| v.unsigned_abs() as u64).sum::<u64>();
|
||||
}
|
||||
total += s / (k as u64 / 2);
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
/// SAD of a prediction against the source over the visible part of the block.
|
||||
fn sad(&self, p: usize, x: usize, y: usize, n: usize, pred: &[u16]) -> u64 {
|
||||
let (vw, vh) = self.g.visible(p);
|
||||
let w = n.min(vw.saturating_sub(x));
|
||||
let h = n.min(vh.saturating_sub(y));
|
||||
let src = &self.src[p];
|
||||
let mut s = 0u64;
|
||||
for i in 0..h {
|
||||
let row = &src.row(y + i)[x..x + w];
|
||||
s += row.iter().zip(&pred[i * n..i * n + w]).map(|(&a, &b)| (a as i32 - b as i32).unsigned_abs()).sum::<u32>() as u64;
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn sse(&self, p: usize, x: usize, y: usize, n: usize, blk: &[u16]) -> f64 {
|
||||
let (vw, vh) = self.g.visible(p);
|
||||
let w = n.min(vw.saturating_sub(x));
|
||||
let h = n.min(vh.saturating_sub(y));
|
||||
let src = &self.src[p];
|
||||
let mut s = 0u64;
|
||||
for i in 0..h {
|
||||
let row = &src.row(y + i)[x..x + w];
|
||||
s += row
|
||||
.iter()
|
||||
.zip(&blk[i * n..i * n + w])
|
||||
.map(|(&a, &b)| {
|
||||
let d = a as i64 - b as i64;
|
||||
(d * d) as u64
|
||||
})
|
||||
.sum::<u64>();
|
||||
}
|
||||
s as f64
|
||||
}
|
||||
|
||||
fn put(&mut self, p: usize, x: usize, y: usize, n: usize, blk: &[u16]) {
|
||||
let pl = &mut self.rec[p];
|
||||
for i in 0..n {
|
||||
let o = (y + i) * pl.stride + x;
|
||||
pl.data[o..o + n].copy_from_slice(&blk[i * n..i * n + n]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform, quantise and reconstruct one square transform block of `plane` at (x, y)
|
||||
/// over the prediction `pred`; the reconstruction is written to `rec`.
|
||||
fn code_tx(&mut self, p: usize, x: usize, y: usize, log2: u32, tx_type: usize, pred: &[u16], intra: bool) -> TxResult {
|
||||
let (res, recon) = self.try_tx(p, x, y, log2, tx_type, pred, intra);
|
||||
let n = 1usize << log2;
|
||||
self.put(p, x, y, n, recon.as_deref().unwrap_or(pred));
|
||||
res
|
||||
}
|
||||
|
||||
/// Transform and quantise one block; returns the result and its reconstruction (None: the
|
||||
/// prediction) without writing it.
|
||||
fn try_tx(&self, p: usize, x: usize, y: usize, log2: u32, tx_type: usize, pred: &[u16], intra: bool) -> (TxResult, Option<Vec<u16>>) {
|
||||
let n = 1usize << log2;
|
||||
let tx_sz = tx_size_for(log2);
|
||||
let bd = self.bd;
|
||||
let src = &self.src[p];
|
||||
let mut res = vec![0i32; n * n];
|
||||
for i in 0..n {
|
||||
let row = &src.row(y + i)[x..x + n];
|
||||
for j in 0..n {
|
||||
res[i * n + j] = row[j] as i32 - pred[i * n + j] as i32;
|
||||
}
|
||||
}
|
||||
let mut coef = vec![0f32; n * n];
|
||||
forward_2d(&res, &mut coef, n, tx_type);
|
||||
// dead-zone quantiser: rounding offset below one half
|
||||
let rnd = if intra { 0.38f32 } else { 0.30f32 };
|
||||
let mut levels = vec![0i32; n * n];
|
||||
let mut nonzero = false;
|
||||
for (k, (&cf, l)) in coef.iter().zip(levels.iter_mut()).enumerate() {
|
||||
let q = if k == 0 { self.dc_q } else { self.ac_q };
|
||||
let a = (cf.abs() / q + rnd).floor() as i32;
|
||||
if a != 0 {
|
||||
*l = if cf < 0.0 { -a.min(1 << 16) } else { a.min(1 << 16) };
|
||||
nonzero = true;
|
||||
}
|
||||
}
|
||||
let zero_dist = self.sse(p, x, y, n, pred);
|
||||
if !nonzero {
|
||||
return (TxResult { levels, nonzero, dist: zero_dist, bits: 1.0 }, None);
|
||||
}
|
||||
let recon = self.reconstruct(&levels, tx_sz, tx_type, pred, n, bd);
|
||||
let dist = self.sse(p, x, y, n, &recon);
|
||||
let bits = estimate_bits(&levels, tx_sz);
|
||||
if dist + self.lambda * bits >= zero_dist + self.lambda {
|
||||
levels.iter_mut().for_each(|l| *l = 0);
|
||||
return (TxResult { levels, nonzero: false, dist: zero_dist, bits: 1.0 }, None);
|
||||
}
|
||||
(TxResult { levels, nonzero: true, dist, bits }, Some(recon))
|
||||
}
|
||||
|
||||
/// Dequantisation (7.12.3) and the inverse transform, added to the prediction.
|
||||
fn reconstruct(&self, levels: &[i32], tx_sz: usize, tx_type: usize, pred: &[u16], n: usize, bd: u32) -> Vec<u16> {
|
||||
let denom: i64 = if tx_sz == TX_32X32 { 2 } else { 1 };
|
||||
let lim = 1i64 << (7 + bd);
|
||||
let mut deq = vec![0i32; n * n];
|
||||
for (k, (&l, d)) in levels.iter().zip(deq.iter_mut()).enumerate() {
|
||||
if l == 0 {
|
||||
continue;
|
||||
}
|
||||
let q = if k == 0 { self.dc_qi } else { self.ac_qi };
|
||||
let dq = l as i64 * q;
|
||||
let mag = (dq.abs() & 0xFF_FFFF) / denom;
|
||||
*d = (if dq < 0 { -mag } else { mag }).clamp(-lim, lim - 1) as i32;
|
||||
}
|
||||
let mut resid = vec![0i32; n * n];
|
||||
inverse_2d(&deq, &mut resid, tx_sz, tx_type, bd);
|
||||
let max = (1i32 << bd) - 1;
|
||||
pred.iter().zip(&resid).map(|(&p, &r)| (p as i32 + r).clamp(0, max) as u16).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Unnormalised Walsh-Hadamard transform of 4 or 8 values, in place.
|
||||
fn hadamard(v: &mut [i32]) {
|
||||
let n = v.len();
|
||||
let mut h = 1;
|
||||
while h < n {
|
||||
for i in (0..n).step_by(2 * h) {
|
||||
for j in i..i + h {
|
||||
let (a, b) = (v[j], v[j + h]);
|
||||
v[j] = a + b;
|
||||
v[j + h] = a - b;
|
||||
}
|
||||
}
|
||||
h *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rough coefficient coding cost in bits.
|
||||
fn estimate_bits(levels: &[i32], tx_sz: usize) -> f64 {
|
||||
let scan = scan_for(tx_sz);
|
||||
let mut eob = 0;
|
||||
for (i, &pos) in scan.iter().enumerate() {
|
||||
if levels[pos as usize] != 0 {
|
||||
eob = i + 1;
|
||||
}
|
||||
}
|
||||
if eob == 0 {
|
||||
return 1.0;
|
||||
}
|
||||
let mut bits = 3.0 + (eob as f64).log2();
|
||||
for &pos in &scan[..eob] {
|
||||
let a = levels[pos as usize].unsigned_abs();
|
||||
bits += match a {
|
||||
0 => 0.7,
|
||||
1 => 2.5,
|
||||
_ => 3.5 + 1.6 * (a as f64).log2(),
|
||||
};
|
||||
}
|
||||
bits
|
||||
}
|
||||
|
||||
pub(crate) fn scan_for(tx_sz: usize) -> &'static [u16] {
|
||||
match tx_sz {
|
||||
TX_4X4 => &DEFAULT_SCAN_4X4,
|
||||
TX_8X8 => &DEFAULT_SCAN_8X8,
|
||||
TX_16X16 => &DEFAULT_SCAN_16X16,
|
||||
_ => &DEFAULT_SCAN_32X32,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,518 @@
|
|||
//! Clean-room pure-Rust AV1 encoder (Main profile, 4:2:0, 8 and 10 bit), implemented from the
|
||||
//! AV1 Bitstream & Decoding Process Specification v1.0.0 with Errata 1.
|
||||
//!
|
||||
//! Key frames and single-reference inter frames (LAST_FRAME = the previous frame), 64x64
|
||||
//! superblocks split down to 8x8 by rate-distortion decisions, intra modes DC / V / H / Paeth /
|
||||
//! smooth, full-sample motion search with the spec's motion vector prediction, DCT / ADST
|
||||
//! transforms up to 32x32, one tile per frame, in-loop filters off. See the crate README.
|
||||
//!
|
||||
//! The public API below is the contract `crates/export` codes against; keep it stable.
|
||||
|
||||
mod bitw;
|
||||
mod cdf;
|
||||
mod decide;
|
||||
mod loopfilter;
|
||||
mod pred;
|
||||
mod symbol;
|
||||
mod tables;
|
||||
mod transform;
|
||||
mod writer;
|
||||
|
||||
use bitw::{BitWriter, obu};
|
||||
use decide::{Decider, FrameGeom, RefFrame};
|
||||
use loopfilter::{LfInfo, loop_filter};
|
||||
use pred::Plane;
|
||||
use writer::TileWriter;
|
||||
|
||||
/// Rate control.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RateControl {
|
||||
/// Constant quantizer index (`base_q_idx`, 1–255; lower is better).
|
||||
ConstantQ(u8),
|
||||
/// Average bitrate target in kbit/s (frame-level quantizer adaptation).
|
||||
Bitrate { kbps: u32 },
|
||||
}
|
||||
|
||||
/// Encoder settings. Main profile (seq_profile 0), 4:2:0.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct EncoderConfig {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub fps_num: u32,
|
||||
pub fps_den: u32,
|
||||
/// 8 or 10.
|
||||
pub bit_depth: u8,
|
||||
/// `seq_level_idx` (e.g. 8 = level 4.0); `None` picks the lowest level that fits.
|
||||
pub level_idx: Option<u8>,
|
||||
pub rate: RateControl,
|
||||
/// Frames between key frames (1 = all key frames).
|
||||
pub keyint: u32,
|
||||
/// BT.709 limited range is signalled unless this is set.
|
||||
pub full_range: bool,
|
||||
}
|
||||
|
||||
impl EncoderConfig {
|
||||
pub fn new(width: u32, height: u32, fps_num: u32, fps_den: u32) -> Self {
|
||||
EncoderConfig { width, height, fps_num, fps_den, bit_depth: 8, level_idx: None, rate: RateControl::ConstantQ(100), keyint: 60, full_range: false }
|
||||
}
|
||||
}
|
||||
|
||||
/// One input picture, 4:2:0, samples at the configured bit depth.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Frame<'a> {
|
||||
pub y: &'a [u16],
|
||||
pub u: &'a [u16],
|
||||
pub v: &'a [u16],
|
||||
pub y_stride: usize,
|
||||
pub uv_stride: usize,
|
||||
}
|
||||
|
||||
/// One temporal unit as stored in MP4 / WebM samples: OBUs with `obu_has_size_field` = 1 and
|
||||
/// no temporal delimiter (key frames start with the sequence header OBU).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Packet {
|
||||
pub data: Vec<u8>,
|
||||
pub keyframe: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
InvalidConfig(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::InvalidConfig(s) => write!(f, "invalid AV1 encoder config: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
const OBU_SEQUENCE_HEADER: u8 = 1;
|
||||
const OBU_FRAME: u8 = 6;
|
||||
|
||||
/// Annex A level limits: (seq_level_idx, MaxPicSize, MaxHSize, MaxVSize, MaxDisplayRate,
|
||||
/// MaxBitrate (Main tier, Mbit/s)).
|
||||
const LEVELS: [(u8, u64, u32, u32, u64, u64); 14] = [
|
||||
(0, 147_456, 2048, 1152, 4_423_680, 1_500_000),
|
||||
(1, 278_784, 2816, 1584, 8_363_520, 3_000_000),
|
||||
(4, 665_856, 4352, 2448, 19_975_680, 6_000_000),
|
||||
(5, 1_065_024, 5504, 3096, 31_950_720, 10_000_000),
|
||||
(8, 2_359_296, 6144, 3456, 70_778_880, 12_000_000),
|
||||
(9, 2_359_296, 6144, 3456, 141_557_760, 20_000_000),
|
||||
(12, 8_912_896, 8192, 4352, 267_386_880, 30_000_000),
|
||||
(13, 8_912_896, 8192, 4352, 534_773_760, 40_000_000),
|
||||
(14, 8_912_896, 8192, 4352, 1_069_547_520, 60_000_000),
|
||||
(15, 8_912_896, 8192, 4352, 1_069_547_520, 60_000_000),
|
||||
(16, 35_651_584, 16384, 8704, 1_069_547_520, 60_000_000),
|
||||
(17, 35_651_584, 16384, 8704, 2_139_095_040, 100_000_000),
|
||||
(18, 35_651_584, 16384, 8704, 4_278_190_080, 160_000_000),
|
||||
(19, 35_651_584, 16384, 8704, 4_278_190_080, 160_000_000),
|
||||
];
|
||||
|
||||
/// The lowest level whose limits fit the stream (31 = no level limits when none does).
|
||||
fn pick_level(cfg: &EncoderConfig) -> u8 {
|
||||
let pic = cfg.width as u64 * cfg.height as u64;
|
||||
let rate = pic * cfg.fps_num as u64 / cfg.fps_den.max(1) as u64;
|
||||
let bitrate = match cfg.rate {
|
||||
RateControl::Bitrate { kbps } => kbps as u64 * 1000,
|
||||
RateControl::ConstantQ(_) => 0,
|
||||
};
|
||||
for &(idx, max_pic, max_h, max_v, max_rate, max_br) in LEVELS.iter() {
|
||||
if pic <= max_pic && cfg.width <= max_h && cfg.height <= max_v && rate <= max_rate && bitrate <= max_br {
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
31
|
||||
}
|
||||
|
||||
/// Frame-level rate control state for `RateControl::Bitrate`.
|
||||
struct RateState {
|
||||
/// Bits per frame at the target rate.
|
||||
frame_bits: f64,
|
||||
/// Quantizer index for inter frames (continuous).
|
||||
q: f64,
|
||||
/// Accumulated (spent - budget) bits.
|
||||
debt: f64,
|
||||
}
|
||||
|
||||
/// AV1 encoder. Frames come out in input order (no hidden/reordered frames).
|
||||
pub struct Encoder {
|
||||
cfg: EncoderConfig,
|
||||
geom: FrameGeom,
|
||||
level: u8,
|
||||
seq_obu: Vec<u8>,
|
||||
frame_num: u64,
|
||||
refr: Option<RefFrame>,
|
||||
rc: Option<RateState>,
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
pub fn new(cfg: EncoderConfig) -> Result<Encoder, Error> {
|
||||
if cfg.width == 0 || cfg.height == 0 {
|
||||
return Err(Error::InvalidConfig("empty frame".into()));
|
||||
}
|
||||
if cfg.width > 4096 || cfg.height > 4096 {
|
||||
return Err(Error::InvalidConfig(format!("{}x{} exceeds the single-tile limit (4096 samples per side)", cfg.width, cfg.height)));
|
||||
}
|
||||
if cfg.bit_depth != 8 && cfg.bit_depth != 10 {
|
||||
return Err(Error::InvalidConfig(format!("bit depth {} (8 or 10 supported)", cfg.bit_depth)));
|
||||
}
|
||||
if cfg.fps_num == 0 || cfg.fps_den == 0 {
|
||||
return Err(Error::InvalidConfig("frame rate".into()));
|
||||
}
|
||||
let geom = FrameGeom::new(cfg.width as usize, cfg.height as usize);
|
||||
if geom.sb_cols * geom.sb_rows > 2304 {
|
||||
return Err(Error::InvalidConfig(format!("{}x{} exceeds the single-tile area limit (MAX_TILE_AREA)", cfg.width, cfg.height)));
|
||||
}
|
||||
if let RateControl::Bitrate { kbps } = cfg.rate
|
||||
&& kbps == 0
|
||||
{
|
||||
return Err(Error::InvalidConfig("zero bitrate".into()));
|
||||
}
|
||||
let level = cfg.level_idx.unwrap_or_else(|| pick_level(&cfg));
|
||||
if level > 31 {
|
||||
return Err(Error::InvalidConfig(format!("seq_level_idx {level}")));
|
||||
}
|
||||
let rc = match cfg.rate {
|
||||
RateControl::ConstantQ(_) => None,
|
||||
RateControl::Bitrate { kbps } => {
|
||||
let frame_bits = kbps as f64 * 1000.0 * cfg.fps_den as f64 / cfg.fps_num as f64;
|
||||
// initial guess from the bits per sample
|
||||
let bpp = frame_bits / (cfg.width as f64 * cfg.height as f64);
|
||||
let q = (110.0 - 40.0 * (bpp / 0.1).log2()).clamp(20.0, 230.0);
|
||||
Some(RateState { frame_bits, q, debt: 0.0 })
|
||||
}
|
||||
};
|
||||
let mut enc = Encoder { cfg, geom, level, seq_obu: Vec::new(), frame_num: 0, refr: None, rc };
|
||||
let payload = enc.sequence_header_payload();
|
||||
obu(OBU_SEQUENCE_HEADER, &payload, &mut enc.seq_obu);
|
||||
Ok(enc)
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &EncoderConfig {
|
||||
&self.cfg
|
||||
}
|
||||
|
||||
/// The sequence header OBU (with size field).
|
||||
pub fn sequence_header_obu(&self) -> Vec<u8> {
|
||||
self.seq_obu.clone()
|
||||
}
|
||||
|
||||
/// AV1CodecConfigurationRecord (`av1C` payload, also the WebM CodecPrivate).
|
||||
pub fn av1c(&self) -> Vec<u8> {
|
||||
let high = (self.cfg.bit_depth > 8) as u8;
|
||||
let mut v = vec![
|
||||
0x81, // marker (1), version (1)
|
||||
self.level & 0x1f, // seq_profile 0, seq_level_idx_0
|
||||
(high << 6) | (1 << 3) | (1 << 2), // tier 0, high_bitdepth, twelve_bit 0, mono 0, ss_x 1, ss_y 1, csp 0
|
||||
0, // no initial_presentation_delay
|
||||
];
|
||||
v.extend_from_slice(&self.seq_obu);
|
||||
v
|
||||
}
|
||||
|
||||
/// sequence_header_obu( ) payload (5.5) including trailing bits.
|
||||
fn sequence_header_payload(&self) -> Vec<u8> {
|
||||
let c = &self.cfg;
|
||||
let mut w = BitWriter::new();
|
||||
w.f(3, 0); // seq_profile
|
||||
w.f(1, 0); // still_picture
|
||||
w.f(1, 0); // reduced_still_picture_header
|
||||
w.f(1, 0); // timing_info_present_flag
|
||||
w.f(1, 0); // initial_display_delay_present_flag
|
||||
w.f(5, 0); // operating_points_cnt_minus_1
|
||||
w.f(12, 0); // operating_point_idc[ 0 ]
|
||||
w.f(5, self.level as u32); // seq_level_idx[ 0 ]
|
||||
if self.level > 7 {
|
||||
w.f(1, 0); // seq_tier[ 0 ]
|
||||
}
|
||||
let wbits = 32 - (c.width - 1).max(1).leading_zeros();
|
||||
let hbits = 32 - (c.height - 1).max(1).leading_zeros();
|
||||
w.f(4, wbits - 1);
|
||||
w.f(4, hbits - 1);
|
||||
w.f(wbits, c.width - 1);
|
||||
w.f(hbits, c.height - 1);
|
||||
w.f(1, 0); // frame_id_numbers_present_flag
|
||||
w.f(1, 0); // use_128x128_superblock
|
||||
w.f(1, 0); // enable_filter_intra
|
||||
w.f(1, 0); // enable_intra_edge_filter
|
||||
w.f(1, 0); // enable_interintra_compound
|
||||
w.f(1, 0); // enable_masked_compound
|
||||
w.f(1, 0); // enable_warped_motion
|
||||
w.f(1, 0); // enable_dual_filter
|
||||
w.f(1, 0); // enable_order_hint
|
||||
w.f(1, 0); // seq_choose_screen_content_tools
|
||||
w.f(1, 0); // seq_force_screen_content_tools
|
||||
w.f(1, 0); // enable_superres
|
||||
w.f(1, 0); // enable_cdef
|
||||
w.f(1, 0); // enable_restoration
|
||||
// color_config( )
|
||||
w.f(1, (c.bit_depth > 8) as u32); // high_bitdepth
|
||||
w.f(1, 0); // mono_chrome
|
||||
w.f(1, 1); // color_description_present_flag
|
||||
w.f(8, 1); // color_primaries: BT.709
|
||||
w.f(8, 1); // transfer_characteristics: BT.709
|
||||
w.f(8, 1); // matrix_coefficients: BT.709
|
||||
w.f(1, c.full_range as u32); // color_range
|
||||
w.f(2, 0); // chroma_sample_position: unknown
|
||||
w.f(1, 0); // separate_uv_delta_q
|
||||
w.f(1, 0); // film_grain_params_present
|
||||
w.trailing_bits();
|
||||
w.into_bytes()
|
||||
}
|
||||
|
||||
/// uncompressed_header( ) (5.9) for a shown KEY_FRAME or an INTER_FRAME referencing slot 0.
|
||||
fn frame_header(&self, w: &mut BitWriter, key: bool, qidx: u8, lf_level: u32) {
|
||||
w.f(1, 0); // show_existing_frame
|
||||
w.f(2, if key { 0 } else { 1 }); // frame_type
|
||||
w.f(1, 1); // show_frame
|
||||
if !key {
|
||||
w.f(1, 0); // error_resilient_mode
|
||||
}
|
||||
w.f(1, 0); // disable_cdf_update
|
||||
w.f(1, 0); // frame_size_override_flag
|
||||
if !key {
|
||||
w.f(3, 0); // primary_ref_frame: LAST_FRAME's saved CDFs
|
||||
w.f(8, 1); // refresh_frame_flags: slot 0 becomes LAST for the next frame
|
||||
for _ in 0..7 {
|
||||
w.f(3, 0); // ref_frame_idx[ i ]
|
||||
}
|
||||
w.f(1, 0); // render_and_frame_size_different
|
||||
w.f(1, 0); // allow_high_precision_mv
|
||||
w.f(1, 0); // is_filter_switchable
|
||||
w.f(2, 0); // interpolation_filter = EIGHTTAP
|
||||
w.f(1, 0); // is_motion_mode_switchable
|
||||
} else {
|
||||
w.f(1, 0); // render_and_frame_size_different
|
||||
}
|
||||
w.f(1, 0); // disable_frame_end_update_cdf
|
||||
// tile_info( ): uniform spacing, one tile
|
||||
let g = &self.geom;
|
||||
let tile_log2 = |blk: usize, target: usize| {
|
||||
let mut k = 0;
|
||||
while (blk << k) < target {
|
||||
k += 1;
|
||||
}
|
||||
k
|
||||
};
|
||||
w.f(1, 1); // uniform_tile_spacing_flag
|
||||
if tile_log2(1, g.sb_cols.min(64)) > 0 {
|
||||
w.f(1, 0); // increment_tile_cols_log2
|
||||
}
|
||||
if tile_log2(1, g.sb_rows.min(64)) > 0 {
|
||||
w.f(1, 0); // increment_tile_rows_log2
|
||||
}
|
||||
// quantization_params( )
|
||||
w.f(8, qidx as u32);
|
||||
w.f(1, 0); // DeltaQYDc
|
||||
w.f(1, 0); // DeltaQUDc
|
||||
w.f(1, 0); // DeltaQUAc
|
||||
w.f(1, 0); // using_qmatrix
|
||||
w.f(1, 0); // segmentation_enabled
|
||||
w.f(1, 0); // delta_q_present (base_q_idx > 0)
|
||||
// loop_filter_params( ): the same level for every plane and direction
|
||||
w.f(6, lf_level);
|
||||
w.f(6, lf_level);
|
||||
if lf_level != 0 {
|
||||
w.f(6, lf_level);
|
||||
w.f(6, lf_level);
|
||||
}
|
||||
w.f(3, 0); // loop_filter_sharpness
|
||||
w.f(1, 0); // loop_filter_delta_enabled
|
||||
w.f(1, 0); // tx_mode_select (TX_MODE_LARGEST)
|
||||
if !key {
|
||||
w.f(1, 0); // reference_select
|
||||
}
|
||||
w.f(1, 1); // reduced_tx_set
|
||||
if !key {
|
||||
for _ in 0..7 {
|
||||
w.f(1, 0); // is_global
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies a picture into superblock-padded planes (edge replication).
|
||||
fn load_source(&self, frame: &Frame) -> [Plane; 3] {
|
||||
let maxv = (1u16 << self.cfg.bit_depth) - 1;
|
||||
std::array::from_fn(|p| {
|
||||
let (vw, vh) = self.geom.visible(p);
|
||||
let (pw, ph) = self.geom.padded(p);
|
||||
let (data, stride) = match p {
|
||||
0 => (frame.y, frame.y_stride),
|
||||
1 => (frame.u, frame.uv_stride),
|
||||
_ => (frame.v, frame.uv_stride),
|
||||
};
|
||||
assert!(stride >= vw && data.len() >= stride * (vh - 1) + vw, "plane {p} is too small for {vw}x{vh}");
|
||||
let mut pl = Plane::new(pw, ph);
|
||||
for y in 0..ph {
|
||||
let sy = y.min(vh - 1);
|
||||
let src = &data[sy * stride..sy * stride + vw];
|
||||
let row = &mut pl.data[y * pw..y * pw + pw];
|
||||
for (x, d) in row.iter_mut().enumerate() {
|
||||
*d = src[x.min(vw - 1)].min(maxv);
|
||||
}
|
||||
}
|
||||
pl
|
||||
})
|
||||
}
|
||||
|
||||
/// Codes one frame at `qidx`; returns the frame OBU and the reconstruction.
|
||||
fn encode_frame(&self, src: &[Plane; 3], key: bool, qidx: u8) -> (Vec<u8>, RefFrame) {
|
||||
let g = &self.geom;
|
||||
let refr = if key { None } else { self.refr.as_ref() };
|
||||
let mut dec = Decider::new(g, self.cfg.bit_depth as u32, qidx, key, src, refr);
|
||||
let mut tw = TileWriter::new(g, key, qidx, refr.map(|r| &*r.cdfs));
|
||||
for sbr in 0..g.sb_rows {
|
||||
tw.start_sb_row();
|
||||
for sbc in 0..g.sb_cols {
|
||||
let node = dec.superblock(sbr * 16, sbc * 16);
|
||||
tw.superblock(sbr * 16, sbc * 16, &node);
|
||||
}
|
||||
}
|
||||
let lf_level = self.pick_filter_level(&dec.rec, &tw.lf_info(), src, qidx);
|
||||
loop_filter(&mut dec.rec, &tw.lf_info(), [lf_level; 4], self.cfg.bit_depth as u32, false, 1);
|
||||
let (tile, cdfs) = tw.finish();
|
||||
let mut w = BitWriter::new();
|
||||
self.frame_header(&mut w, key, qidx, lf_level);
|
||||
w.byte_align();
|
||||
// tile_group_obu( ): a single tile, no tile_start_and_end_present_flag
|
||||
let mut payload = w.into_bytes();
|
||||
payload.extend_from_slice(&tile);
|
||||
let mut out = Vec::with_capacity(payload.len() + 8);
|
||||
obu(OBU_FRAME, &payload, &mut out);
|
||||
let rf = RefFrame { planes: dec.rec, mvs: dec.mvs, lf_level, cdfs };
|
||||
(out, rf)
|
||||
}
|
||||
|
||||
/// The loop filter level minimising the luma error (searched around a guess from the
|
||||
/// quantizer, on the luma of a subset of the superblock rows).
|
||||
fn pick_filter_level(&self, rec: &[Plane; 3], info: &LfInfo, src: &[Plane; 3], qidx: u8) -> u32 {
|
||||
let (w, h) = (self.geom.width, self.geom.height);
|
||||
let sb_row_step = if self.geom.sb_rows >= 9 { 3 } else { 1 };
|
||||
let sse = |pl: &Plane| -> u64 {
|
||||
let mut s = 0u64;
|
||||
for y in 0..h {
|
||||
if !(y >> 6).is_multiple_of(sb_row_step) {
|
||||
continue;
|
||||
}
|
||||
for (a, b) in pl.row(y)[..w].iter().zip(&src[0].row(y)[..w]) {
|
||||
let d = *a as i64 - *b as i64;
|
||||
s += (d * d) as u64;
|
||||
}
|
||||
}
|
||||
s
|
||||
};
|
||||
let bd = self.cfg.bit_depth as u32;
|
||||
let mut cache: Vec<(u32, u64)> = vec![(0, sse(&rec[0]))];
|
||||
let mut eval = |lvl: u32| -> u64 {
|
||||
if let Some(&(_, e)) = cache.iter().find(|(l, _)| *l == lvl) {
|
||||
return e;
|
||||
}
|
||||
let mut planes = [rec[0].clone(), Plane::new(0, 0), Plane::new(0, 0)];
|
||||
loop_filter(&mut planes, info, [lvl; 4], bd, true, sb_row_step);
|
||||
let e = sse(&planes[0]);
|
||||
cache.push((lvl, e));
|
||||
e
|
||||
};
|
||||
let guess = ((qidx as u32 * 3) / 16).clamp(2, 50);
|
||||
let mut best = (0u32, eval(0));
|
||||
let mut step = (guess / 2).max(1);
|
||||
let mut center = guess;
|
||||
let e = eval(center);
|
||||
if e < best.1 {
|
||||
best = (center, e);
|
||||
}
|
||||
while step >= 2 {
|
||||
for lvl in [center.saturating_sub(step), (center + step).min(63)] {
|
||||
let e = eval(lvl);
|
||||
if e < best.1 {
|
||||
best = (lvl, e);
|
||||
}
|
||||
}
|
||||
center = best.0.max(1);
|
||||
step /= 2;
|
||||
}
|
||||
best.0
|
||||
}
|
||||
|
||||
/// Encodes one frame.
|
||||
pub fn encode(&mut self, frame: &Frame) -> Packet {
|
||||
let key = self.frame_num.is_multiple_of(self.cfg.keyint.max(1) as u64);
|
||||
let src = self.load_source(frame);
|
||||
let (frame_obu, rf) = match self.cfg.rate {
|
||||
RateControl::ConstantQ(q) => self.encode_frame(&src, key, q.max(1)),
|
||||
RateControl::Bitrate { .. } => self.encode_rate_controlled(&src, key),
|
||||
};
|
||||
self.refr = Some(rf);
|
||||
self.frame_num += 1;
|
||||
let mut data = Vec::with_capacity(frame_obu.len() + self.seq_obu.len());
|
||||
if key {
|
||||
data.extend_from_slice(&self.seq_obu);
|
||||
}
|
||||
data.extend_from_slice(&frame_obu);
|
||||
Packet { data, keyframe: key }
|
||||
}
|
||||
|
||||
fn encode_rate_controlled(&mut self, src: &[Plane; 3], key: bool) -> (Vec<u8>, RefFrame) {
|
||||
let first = self.frame_num == 0;
|
||||
let rc = self.rc.as_ref().expect("rate control state");
|
||||
let (frame_bits, debt, q_inter) = (rc.frame_bits, rc.debt, rc.q);
|
||||
// key frames get a lower quantizer and a larger share of the budget
|
||||
let key_boost = 1.0 + (self.cfg.keyint.clamp(1, 60) as f64 - 1.0).sqrt() * 0.75;
|
||||
let budget = if key { frame_bits * key_boost } else { frame_bits };
|
||||
// pay back a fraction of the accumulated deviation
|
||||
let target = (budget - debt * 0.25).max(budget * 0.25);
|
||||
let mut q = if key { q_inter - 24.0 } else { q_inter };
|
||||
let mut best: Option<(Vec<u8>, RefFrame, f64)> = None;
|
||||
let passes = if first { 4 } else { 1 };
|
||||
for pass in 0..passes {
|
||||
let qi = q.round().clamp(1.0, 255.0) as u8;
|
||||
let (obu, rf) = self.encode_frame(src, key, qi);
|
||||
let bits = obu.len() as f64 * 8.0;
|
||||
let err = (bits / target).log2();
|
||||
let better = best.as_ref().is_none_or(|b| err.abs() < (b.2 / target).log2().abs());
|
||||
if better {
|
||||
best = Some((obu, rf, bits));
|
||||
}
|
||||
if pass + 1 < passes {
|
||||
q = (q + 30.0 * err).clamp(1.0, 255.0);
|
||||
}
|
||||
}
|
||||
let (obu, rf, bits) = best.expect("encoded");
|
||||
let rc = self.rc.as_mut().expect("rate control state");
|
||||
rc.debt += bits - budget;
|
||||
// adapt the inter quantizer: about 30 index steps per doubling of the rate
|
||||
let err = (bits / target).log2().clamp(-1.0, 1.0);
|
||||
if first && key {
|
||||
rc.q = (q + 24.0).clamp(1.0, 255.0);
|
||||
} else {
|
||||
rc.q = (rc.q + 12.0 * err).clamp(1.0, 255.0);
|
||||
}
|
||||
(obu, rf)
|
||||
}
|
||||
|
||||
/// The loop filter level of the last encoded frame. For tests.
|
||||
#[doc(hidden)]
|
||||
pub fn last_loop_filter_level(&self) -> Option<u32> {
|
||||
self.refr.as_ref().map(|r| r.lf_level)
|
||||
}
|
||||
|
||||
/// The reconstruction of the last encoded frame (visible area, planes Y, U, V), equal to
|
||||
/// what a decoder outputs. For tests.
|
||||
#[doc(hidden)]
|
||||
pub fn last_reconstruction(&self) -> Option<[Vec<u16>; 3]> {
|
||||
let rf = self.refr.as_ref()?;
|
||||
Some(std::array::from_fn(|p| {
|
||||
let (vw, vh) = self.geom.visible(p);
|
||||
let pl = &rf.planes[p];
|
||||
let mut v = Vec::with_capacity(vw * vh);
|
||||
for y in 0..vh {
|
||||
v.extend_from_slice(&pl.row(y)[..vw]);
|
||||
}
|
||||
v
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
//! Deblocking loop filter (spec 7.14), mirrored from the decoder process so that the encoder's
|
||||
//! references equal a decoder's. Segmentation and loop filter deltas are off and the sharpness
|
||||
//! is 0 in the frames this encoder writes.
|
||||
|
||||
use crate::pred::Plane;
|
||||
use crate::tables::*;
|
||||
|
||||
/// Mode info the loop filter reads (per 4x4 luma unit, row stride `mi_cols`), and the
|
||||
/// transform size per 4x4 unit of each plane (`LoopfilterTxSizes`, row stride `mi_cols + 32`).
|
||||
pub(crate) struct LfInfo<'a> {
|
||||
pub mi_cols: usize,
|
||||
pub mi_rows: usize,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub mi_size: &'a [u8],
|
||||
pub skip: &'a [bool],
|
||||
pub is_inter: &'a [bool],
|
||||
pub tx: [&'a [u8]; 3],
|
||||
}
|
||||
|
||||
/// Applies the loop filter with `loop_filter_level[ 0..4 ]` to `planes`. For level search
|
||||
/// (`luma_only`), only plane 0 and only every `sb_row_step`-th superblock row are filtered.
|
||||
pub(crate) fn loop_filter(planes: &mut [Plane; 3], info: &LfInfo, level: [u32; 4], bit_depth: u32, luma_only: bool, sb_row_step: usize) {
|
||||
if level[0] == 0 && level[1] == 0 {
|
||||
return;
|
||||
}
|
||||
let nplanes = if luma_only { 1 } else { 3 };
|
||||
for (plane, pl) in planes.iter_mut().enumerate().take(nplanes) {
|
||||
if plane > 0 && level[1 + plane] == 0 {
|
||||
continue;
|
||||
}
|
||||
for pass in 0..2 {
|
||||
let step = if plane == 0 { 1 } else { 2 };
|
||||
let mut row = 0;
|
||||
while row < info.mi_rows {
|
||||
if !(row >> 4).is_multiple_of(sb_row_step) {
|
||||
row += step;
|
||||
continue;
|
||||
}
|
||||
let mut col = 0;
|
||||
while col < info.mi_cols {
|
||||
edge(pl, info, level, bit_depth, plane, pass, row, col);
|
||||
col += step;
|
||||
}
|
||||
row += step;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn edge(pl: &mut Plane, info: &LfInfo, level: [u32; 4], bd: u32, plane: usize, pass: usize, row: usize, col: usize) {
|
||||
let s = (plane > 0) as usize;
|
||||
let (dx, dy) = if pass == 0 { (1usize, 0usize) } else { (0, 1) };
|
||||
let x = col * 4;
|
||||
let y = row * 4;
|
||||
let row = row | s;
|
||||
let col = col | s;
|
||||
let on_screen = !(x >= info.width || y >= info.height || (pass == 0 && x == 0) || (pass == 1 && y == 0));
|
||||
if !on_screen {
|
||||
return;
|
||||
}
|
||||
let xp = x >> s;
|
||||
let yp = y >> s;
|
||||
let prev_row = row - (dy << s);
|
||||
let prev_col = col - (dx << s);
|
||||
let i = row * info.mi_cols + col;
|
||||
let mi_size = info.mi_size[i] as usize;
|
||||
let tx_stride = info.mi_cols + 32;
|
||||
let tx_sz = info.tx[plane][(row >> s) * tx_stride + (col >> s)] as usize;
|
||||
let plane_size = SUBSAMPLED_SIZE[mi_size][s][s] as usize;
|
||||
let skip = info.skip[i];
|
||||
let is_intra = !info.is_inter[i];
|
||||
let prev_tx_sz = info.tx[plane][(prev_row >> s) * tx_stride + (prev_col >> s)] as usize;
|
||||
let is_block_edge = if pass == 0 {
|
||||
xp.is_multiple_of(4 * NUM_4X4_BLOCKS_WIDE[plane_size] as usize)
|
||||
} else {
|
||||
yp.is_multiple_of(4 * NUM_4X4_BLOCKS_HIGH[plane_size] as usize)
|
||||
};
|
||||
let is_tx_edge = if pass == 0 { xp.is_multiple_of(TX_WIDTH[tx_sz] as usize) } else { yp.is_multiple_of(TX_HEIGHT[tx_sz] as usize) };
|
||||
let apply_filter = is_tx_edge && (is_block_edge || !skip || is_intra);
|
||||
let base_size = if pass == 0 { TX_WIDTH[prev_tx_sz].min(TX_WIDTH[tx_sz]) } else { TX_HEIGHT[prev_tx_sz].min(TX_HEIGHT[tx_sz]) } as usize;
|
||||
let filter_size = if plane == 0 { 16.min(base_size) } else { 8.min(base_size) };
|
||||
// 7.14.4 with no deltas, segmentation or sharpness: the same strength for every block
|
||||
let lvl = level[if plane == 0 { pass } else { plane + 1 }].min(MAX_LOOP_FILTER as u32) as i32;
|
||||
if !apply_filter || lvl == 0 {
|
||||
return;
|
||||
}
|
||||
let limit = lvl.max(1);
|
||||
let blimit = 2 * (lvl + 2) + limit;
|
||||
let thresh = lvl >> 4;
|
||||
for k in 0..4 {
|
||||
sample_filter(pl, xp + dy * k, yp + dx * k, plane, limit, blimit, thresh, dx as isize, dy as isize, filter_size, bd);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_filter(pl: &mut Plane, x: usize, y: usize, plane: usize, limit: i32, blimit: i32, thresh: i32, dx: isize, dy: isize, filter_size: usize, bd: u32) {
|
||||
let stride = pl.stride as isize;
|
||||
let step = dy * stride + dx;
|
||||
let base = y as isize * stride + x as isize;
|
||||
let reach: isize = if filter_size >= 16 { 7 } else { 4 };
|
||||
let mut v = [0i32; 16];
|
||||
for k in -reach..reach {
|
||||
v[(k + 8) as usize] = pl.data[(base + k * step) as usize] as i32;
|
||||
}
|
||||
let at = |k: isize| -> i32 { v[(k + 8) as usize] };
|
||||
let (q0, q1, q2, q3) = (at(0), at(1), at(2), at(3));
|
||||
let (p0, p1, p2, p3) = (at(-1), at(-2), at(-3), at(-4));
|
||||
let d = &mut pl.data;
|
||||
let sh = bd - 8;
|
||||
let thresh_bd = thresh << sh;
|
||||
let hev = (p1 - p0).abs() > thresh_bd || (q1 - q0).abs() > thresh_bd;
|
||||
let filter_len = if filter_size == 4 {
|
||||
4
|
||||
} else if plane != 0 {
|
||||
6
|
||||
} else if filter_size == 8 {
|
||||
8
|
||||
} else {
|
||||
16
|
||||
};
|
||||
let limit_bd = limit << sh;
|
||||
let blimit_bd = blimit << sh;
|
||||
let mut mask = (p1 - p0).abs() > limit_bd || (q1 - q0).abs() > limit_bd || (p0 - q0).abs() * 2 + (p1 - q1).abs() / 2 > blimit_bd;
|
||||
if filter_len >= 6 {
|
||||
mask |= (p2 - p1).abs() > limit_bd || (q2 - q1).abs() > limit_bd;
|
||||
}
|
||||
if filter_len >= 8 {
|
||||
mask |= (p3 - p2).abs() > limit_bd || (q3 - q2).abs() > limit_bd;
|
||||
}
|
||||
if mask {
|
||||
return;
|
||||
}
|
||||
let t_bd = 1 << sh;
|
||||
let flat = if filter_size >= 8 {
|
||||
let mut m = (p1 - p0).abs() > t_bd || (q1 - q0).abs() > t_bd || (p2 - p0).abs() > t_bd || (q2 - q0).abs() > t_bd;
|
||||
if filter_len >= 8 {
|
||||
m |= (p3 - p0).abs() > t_bd || (q3 - q0).abs() > t_bd;
|
||||
}
|
||||
!m
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let flat2 = if filter_size >= 16 {
|
||||
let (q4, q5, q6, p4, p5, p6) = (at(4), at(5), at(6), at(-5), at(-6), at(-7));
|
||||
!((p6 - p0).abs() > t_bd
|
||||
|| (q6 - q0).abs() > t_bd
|
||||
|| (p5 - p0).abs() > t_bd
|
||||
|| (q5 - q0).abs() > t_bd
|
||||
|| (p4 - p0).abs() > t_bd
|
||||
|| (q4 - q0).abs() > t_bd)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if filter_size == 4 || !flat {
|
||||
// narrow filter (7.14.6.3)
|
||||
let half = 1i32 << (bd - 1);
|
||||
let c = |v: i32| v.clamp(-half, half - 1);
|
||||
let off = 0x80 << sh;
|
||||
let (ps1, ps0, qs0, qs1) = (p1 - off, p0 - off, q0 - off, q1 - off);
|
||||
let mut filter = if hev { c(ps1 - qs1) } else { 0 };
|
||||
filter = c(filter + 3 * (qs0 - ps0));
|
||||
let filter1 = c(filter + 4) >> 3;
|
||||
let filter2 = c(filter + 3) >> 3;
|
||||
d[base as usize] = (c(qs0 - filter1) + off) as u16;
|
||||
d[(base - step) as usize] = (c(ps0 + filter2) + off) as u16;
|
||||
if !hev {
|
||||
let f = (filter1 + 1) >> 1;
|
||||
d[(base + step) as usize] = (c(qs1 - f) + off) as u16;
|
||||
d[(base - 2 * step) as usize] = (c(ps1 + f) + off) as u16;
|
||||
}
|
||||
} else {
|
||||
// wide filter (7.14.6.4)
|
||||
let log2 = if filter_size == 8 || !flat2 { 3 } else { 4 };
|
||||
let n: isize = if log2 == 4 {
|
||||
6
|
||||
} else if plane == 0 {
|
||||
3
|
||||
} else {
|
||||
2
|
||||
};
|
||||
let n2: isize = if log2 == 3 && plane == 0 { 0 } else { 1 };
|
||||
let mut f = [0i32; 12];
|
||||
for i in -n..n {
|
||||
let mut t = 0;
|
||||
for j in -n..=n {
|
||||
let p = (i + j).clamp(-(n + 1), n);
|
||||
let tap = if j.abs() <= n2 { 2 } else { 1 };
|
||||
t += at(p) * tap;
|
||||
}
|
||||
f[(i + n) as usize] = (t + (1 << (log2 - 1))) >> log2;
|
||||
}
|
||||
for i in -n..n {
|
||||
d[(base + i * step) as usize] = f[(i + n) as usize] as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,362 @@
|
|||
//! Prediction processes, mirrored from the spec (7.11.2 intra prediction for the non-directional
|
||||
//! modes plus V_PRED / H_PRED without edge filtering, 7.11.3 block inter prediction without
|
||||
//! reference scaling) so that the encoder predicts exactly like a decoder.
|
||||
|
||||
use crate::tables::*;
|
||||
use crate::transform::round2;
|
||||
|
||||
/// A plane buffer (padded to whole superblocks).
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Plane {
|
||||
pub data: Vec<u16>,
|
||||
pub stride: usize,
|
||||
}
|
||||
|
||||
impl Plane {
|
||||
pub fn new(w: usize, h: usize) -> Plane {
|
||||
Plane { data: vec![0; w * h], stride: w }
|
||||
}
|
||||
#[inline(always)]
|
||||
pub fn at(&self, x: usize, y: usize) -> u16 {
|
||||
self.data[y * self.stride + x]
|
||||
}
|
||||
#[inline(always)]
|
||||
pub fn row(&self, y: usize) -> &[u16] {
|
||||
&self.data[y * self.stride..(y + 1) * self.stride]
|
||||
}
|
||||
}
|
||||
|
||||
/// The intra modes this encoder uses (values are the spec's y / uv mode numbers).
|
||||
pub(crate) const INTRA_MODES_USED: [usize; 7] = [DC_PRED, V_PRED, H_PRED, PAETH_PRED, SMOOTH_PRED, SMOOTH_V_PRED, SMOOTH_H_PRED];
|
||||
|
||||
/// Inputs of the intra prediction process.
|
||||
pub(crate) struct IntraEdge {
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
pub have_left: bool,
|
||||
pub have_above: bool,
|
||||
pub have_above_right: bool,
|
||||
pub have_below_left: bool,
|
||||
pub log2: u32,
|
||||
/// ((MiCols * MI_SIZE) >> subX) - 1 and the same for rows.
|
||||
pub max_x: i32,
|
||||
pub max_y: i32,
|
||||
pub bit_depth: u32,
|
||||
}
|
||||
|
||||
/// Edge samples: `above[0..2n]`, `left[0..2n]` and the top-left corner.
|
||||
pub(crate) struct Edges {
|
||||
above: [i32; 64],
|
||||
left: [i32; 64],
|
||||
corner: i32,
|
||||
}
|
||||
|
||||
pub(crate) fn edges(pl: &Plane, e: &IntraEdge) -> Edges {
|
||||
let w = 1usize << e.log2;
|
||||
let h = w;
|
||||
let bd = e.bit_depth;
|
||||
let (x, y) = (e.x, e.y);
|
||||
let n = w + h;
|
||||
let mut above = [0i32; 64];
|
||||
let mut left = [0i32; 64];
|
||||
if !e.have_above && e.have_left {
|
||||
let v = pl.at(x - 1, y) as i32;
|
||||
above[..n].iter_mut().for_each(|a| *a = v);
|
||||
} else if !e.have_above && !e.have_left {
|
||||
let v = (1 << (bd - 1)) - 1;
|
||||
above[..n].iter_mut().for_each(|a| *a = v);
|
||||
} else {
|
||||
let above_limit = e.max_x.min(x as i32 + if e.have_above_right { 2 * w as i32 } else { w as i32 } - 1);
|
||||
let row = pl.row(y - 1);
|
||||
for (i, a) in above[..n].iter_mut().enumerate() {
|
||||
*a = row[above_limit.min(x as i32 + i as i32) as usize] as i32;
|
||||
}
|
||||
}
|
||||
if !e.have_left && e.have_above {
|
||||
let v = pl.at(x, y - 1) as i32;
|
||||
left[..n].iter_mut().for_each(|a| *a = v);
|
||||
} else if !e.have_left && !e.have_above {
|
||||
let v = (1 << (bd - 1)) + 1;
|
||||
left[..n].iter_mut().for_each(|a| *a = v);
|
||||
} else {
|
||||
let left_limit = e.max_y.min(y as i32 + if e.have_below_left { 2 * h as i32 } else { h as i32 } - 1);
|
||||
for (i, l) in left[..n].iter_mut().enumerate() {
|
||||
*l = pl.at(x - 1, left_limit.min(y as i32 + i as i32) as usize) as i32;
|
||||
}
|
||||
}
|
||||
let corner = if e.have_above && e.have_left {
|
||||
pl.at(x - 1, y - 1) as i32
|
||||
} else if e.have_above {
|
||||
pl.at(x, y - 1) as i32
|
||||
} else if e.have_left {
|
||||
pl.at(x - 1, y) as i32
|
||||
} else {
|
||||
1 << (bd - 1)
|
||||
};
|
||||
Edges { above, left, corner }
|
||||
}
|
||||
|
||||
fn sm_weights(log2: u32) -> &'static [u8] {
|
||||
match log2 {
|
||||
2 => &SM_WEIGHTS_TX_4X4,
|
||||
3 => &SM_WEIGHTS_TX_8X8,
|
||||
4 => &SM_WEIGHTS_TX_16X16,
|
||||
_ => &SM_WEIGHTS_TX_32X32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Predicts a square `2^log2` block with `mode` into `pred` (row stride = block width).
|
||||
pub(crate) fn predict_intra(ed: &Edges, e: &IntraEdge, mode: usize, pred: &mut [u16]) {
|
||||
let log2 = e.log2;
|
||||
let w = 1usize << log2;
|
||||
let h = w;
|
||||
let bd = e.bit_depth;
|
||||
let (above, left) = (&ed.above, &ed.left);
|
||||
match mode {
|
||||
DC_PRED => {
|
||||
let v = if e.have_left && e.have_above {
|
||||
let sum: i32 = left[..h].iter().sum::<i32>() + above[..w].iter().sum::<i32>() + ((w + h) >> 1) as i32;
|
||||
sum / (w + h) as i32
|
||||
} else if e.have_left {
|
||||
(left[..h].iter().sum::<i32>() + (h >> 1) as i32) >> log2
|
||||
} else if e.have_above {
|
||||
(above[..w].iter().sum::<i32>() + (w >> 1) as i32) >> log2
|
||||
} else {
|
||||
1 << (bd - 1)
|
||||
};
|
||||
let v = v.clamp(0, (1 << bd) - 1) as u16;
|
||||
pred[..w * h].iter_mut().for_each(|p| *p = v);
|
||||
}
|
||||
V_PRED => {
|
||||
for i in 0..h {
|
||||
for j in 0..w {
|
||||
pred[i * w + j] = above[j] as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
H_PRED => {
|
||||
for i in 0..h {
|
||||
pred[i * w..i * w + w].iter_mut().for_each(|p| *p = left[i] as u16);
|
||||
}
|
||||
}
|
||||
PAETH_PRED => {
|
||||
let tl = ed.corner;
|
||||
for i in 0..h {
|
||||
for j in 0..w {
|
||||
let a = above[j];
|
||||
let l = left[i];
|
||||
let base = a + l - tl;
|
||||
let p_left = (base - l).abs();
|
||||
let p_top = (base - a).abs();
|
||||
let p_tl = (base - tl).abs();
|
||||
pred[i * w + j] = if p_left <= p_top && p_left <= p_tl {
|
||||
l as u16
|
||||
} else if p_top <= p_tl {
|
||||
a as u16
|
||||
} else {
|
||||
tl as u16
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let wx = sm_weights(log2);
|
||||
let wy = wx;
|
||||
for i in 0..h {
|
||||
for j in 0..w {
|
||||
let v = if mode == SMOOTH_PRED {
|
||||
let s = wy[i] as i32 * above[j] + (256 - wy[i] as i32) * left[h - 1] + wx[j] as i32 * left[i] + (256 - wx[j] as i32) * above[w - 1];
|
||||
round2(s, 9)
|
||||
} else if mode == SMOOTH_V_PRED {
|
||||
round2(wy[i] as i32 * above[j] + (256 - wy[i] as i32) * left[h - 1], 8)
|
||||
} else {
|
||||
round2(wx[j] as i32 * left[i] + (256 - wx[j] as i32) * above[w - 1], 8)
|
||||
};
|
||||
pred[i * w + j] = v as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Block inter prediction (7.11.3.3 motion vector scaling without scaling, 7.11.3.4 block
|
||||
/// inter prediction with the EIGHTTAP filter, single reference so InterRound1 = 11 and the
|
||||
/// final rounding is the identity) of the `w x h` block at plane position (x, y) displaced by
|
||||
/// `mv` (1/8 luma sample units, [row, col]). `last_x` / `last_y` are the largest sample
|
||||
/// coordinates of the reference plane; the result goes to `pred` (row stride `w`).
|
||||
pub(crate) fn predict_inter(
|
||||
refp: &Plane,
|
||||
last_x: i32,
|
||||
last_y: i32,
|
||||
x: usize,
|
||||
y: usize,
|
||||
w: usize,
|
||||
h: usize,
|
||||
mv: [i32; 2],
|
||||
ss: usize,
|
||||
bit_depth: u32,
|
||||
pred: &mut [u16],
|
||||
) {
|
||||
// positions in 1/16 sample units ((startX >> 6) & 15 is the filter phase)
|
||||
let px = ((x as i32) << 4) + ((2 * mv[1]) >> ss);
|
||||
let py = ((y as i32) << 4) + ((2 * mv[0]) >> ss);
|
||||
let (fx, fy) = ((px & 15) as usize, (py & 15) as usize);
|
||||
let (ix, iy) = (px >> 4, py >> 4);
|
||||
let max = (1i32 << bit_depth) - 1;
|
||||
if fx == 0 && fy == 0 {
|
||||
// the filters' phase-0 kernel is a unit impulse: a clamped copy
|
||||
for r in 0..h {
|
||||
let ry = (iy + r as i32).clamp(0, last_y) as usize;
|
||||
let row = refp.row(ry);
|
||||
let out = &mut pred[r * w..r * w + w];
|
||||
let x0 = ix;
|
||||
if x0 >= 0 && x0 + w as i32 - 1 <= last_x {
|
||||
out.copy_from_slice(&row[x0 as usize..x0 as usize + w]);
|
||||
} else {
|
||||
for (c, o) in out.iter_mut().enumerate() {
|
||||
*o = row[(x0 + c as i32).clamp(0, last_x) as usize];
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
let filt_h = if w <= 4 { 4 } else { EIGHTTAP };
|
||||
let filt_v = if h <= 4 { 4 } else { EIGHTTAP };
|
||||
let kh = &SUBPEL_FILTERS[filt_h][fx];
|
||||
let kv = &SUBPEL_FILTERS[filt_v][fy];
|
||||
let bx = ix - 3;
|
||||
let x_inside = bx >= 0 && bx + w as i32 + 7 <= last_x + 1;
|
||||
// horizontal pass of one reference row into `out` (Round2( sum, InterRound0 = 3 ))
|
||||
let hfilter = |ry: usize, out: &mut [i32]| {
|
||||
let row = refp.row(ry);
|
||||
if x_inside {
|
||||
let seg = &row[bx as usize..bx as usize + w + 7];
|
||||
for (c, o) in out.iter_mut().enumerate() {
|
||||
let mut s = 0i32;
|
||||
for t in 0..8 {
|
||||
s += kh[t] as i32 * seg[c + t] as i32;
|
||||
}
|
||||
*o = round2(s, 3);
|
||||
}
|
||||
} else {
|
||||
for (c, o) in out.iter_mut().enumerate() {
|
||||
let mut s = 0i32;
|
||||
for t in 0..8 {
|
||||
s += kh[t] as i32 * row[(bx + c as i32 + t as i32).clamp(0, last_x) as usize] as i32;
|
||||
}
|
||||
*o = round2(s, 3);
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut inter = [0i32; (64 + 7) * 64];
|
||||
if fy == 0 {
|
||||
// the vertical kernel is 128 at the centre: Round2( 128 * v, 11 ) = Round2( v, 4 )
|
||||
for r in 0..h {
|
||||
let ry = (iy + r as i32).clamp(0, last_y) as usize;
|
||||
hfilter(ry, &mut inter[..w]);
|
||||
for (p, &v) in pred[r * w..r * w + w].iter_mut().zip(&inter[..w]) {
|
||||
*p = round2(v, 4).clamp(0, max) as u16;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if fx == 0 {
|
||||
// the horizontal pass is exactly 16 * sample: Round2( 16 * sum, 11 ) = Round2( sum, 7 )
|
||||
for r in 0..h {
|
||||
let rows: [&[u16]; 8] = std::array::from_fn(|t| refp.row((iy + r as i32 + t as i32 - 3).clamp(0, last_y) as usize));
|
||||
for c in 0..w {
|
||||
let xx = if x_inside { (ix + c as i32) as usize } else { (ix + c as i32).clamp(0, last_x) as usize };
|
||||
let mut s = 0i32;
|
||||
for t in 0..8 {
|
||||
s += kv[t] as i32 * rows[t][xx] as i32;
|
||||
}
|
||||
pred[r * w + c] = round2(s, 7).clamp(0, max) as u16;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
let inter_h = h + 7;
|
||||
for r in 0..inter_h {
|
||||
let ry = (iy + r as i32 - 3).clamp(0, last_y) as usize;
|
||||
hfilter(ry, &mut inter[r * w..r * w + w]);
|
||||
}
|
||||
for r in 0..h {
|
||||
for c in 0..w {
|
||||
let mut s = 0i32;
|
||||
for t in 0..8 {
|
||||
s += kv[t] as i32 * inter[(r + t) * w + c];
|
||||
}
|
||||
pred[r * w + c] = round2(s, 11).clamp(0, max) as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The visible `w x h` area of a reference plane interpolated at the sub-sample phase
|
||||
/// (`fx`, `fy`) (1/16 units) with the EIGHTTAP filter, as `predict_inter` computes it for
|
||||
/// blocks wider and taller than 4. Used for motion search.
|
||||
pub(crate) fn subpel_plane(refp: &Plane, w: usize, h: usize, fx: usize, fy: usize, bit_depth: u32) -> Vec<u16> {
|
||||
let kh = &SUBPEL_FILTERS[EIGHTTAP][fx];
|
||||
let kv = &SUBPEL_FILTERS[EIGHTTAP][fy];
|
||||
let max = (1i32 << bit_depth) - 1;
|
||||
let (lx, ly) = (w as i32 - 1, h as i32 - 1);
|
||||
let mut inter = vec![0i32; w * h];
|
||||
for y in 0..h {
|
||||
let row = refp.row(y);
|
||||
for x in 0..w {
|
||||
let mut s = 0i32;
|
||||
for t in 0..8 {
|
||||
s += kh[t] as i32 * row[(x as i32 + t as i32 - 3).clamp(0, lx) as usize] as i32;
|
||||
}
|
||||
inter[y * w + x] = round2(s, 3);
|
||||
}
|
||||
}
|
||||
let mut out = vec![0u16; w * h];
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let mut s = 0i32;
|
||||
for t in 0..8 {
|
||||
s += kv[t] as i32 * inter[(y as i32 + t as i32 - 3).clamp(0, ly) as usize * w + x];
|
||||
}
|
||||
out[y * w + x] = round2(s, 11).clamp(0, max) as u16;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Whole-plane interpolation equals block inter prediction at the same phase.
|
||||
#[test]
|
||||
fn subpel_plane_matches_block_prediction() {
|
||||
let (w, h) = (37, 29);
|
||||
let mut p = Plane::new(64, 64);
|
||||
for (i, v) in p.data.iter_mut().enumerate() {
|
||||
*v = ((i * 7919) % 256) as u16;
|
||||
}
|
||||
for (fx, fy) in [(8, 0), (0, 8), (8, 8), (4, 12)] {
|
||||
let plane = subpel_plane(&p, w, h, fx, fy, 8);
|
||||
for (bx, by) in [(0i32, 0i32), (8, 8), (24, 16), (-5, 20), (30, -3)] {
|
||||
let mut pred = vec![0u16; 64];
|
||||
let mv = [by * 8 + fy as i32 / 2, bx * 8 + fx as i32 / 2];
|
||||
predict_inter(&p, w as i32 - 1, h as i32 - 1, 0, 0, 8, 8, mv, 0, 8, &mut pred);
|
||||
for r in 0..8 {
|
||||
for c in 0..8 {
|
||||
let (x, y) = ((bx + c).clamp(0, w as i32 - 1), (by + r).clamp(0, h as i32 - 1));
|
||||
if bx + c == x && by + r == y {
|
||||
assert_eq!(pred[(r * 8 + c) as usize], plane[y as usize * w + x as usize], "phase ({fx}, {fy}) at ({}, {})", bx + c, by + r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase_zero_filters_are_impulses() {
|
||||
for f in SUBPEL_FILTERS.iter() {
|
||||
assert_eq!(f[0], [0, 0, 0, 128, 0, 0, 0, 0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
//! Multi-symbol arithmetic encoder: the exact inverse of the symbol decoder of spec 8.2.
|
||||
//!
|
||||
//! The decoder keeps `SymbolValue`, the bitwise complement of a 15-bit window into the tile data
|
||||
//! minus the lower bounds of the symbols decoded so far, and selects symbol `k` when
|
||||
//! `cur(k) <= SymbolValue < cur(k - 1)` with
|
||||
//! `cur(k) = ((SymbolRange >> 8) * ((32768 - cdf[k]) >> 6) >> 1) + 4 * (N - k - 1)` and
|
||||
//! `cur(-1) = SymbolRange`. The encoder therefore runs an ordinary carry-propagating range
|
||||
//! coder over the complemented bit stream: coding symbol `k` adds `cur(k)` to `low` and sets the
|
||||
//! range to `cur(k - 1) - cur(k)`; both are renormalised exactly like the decoder's range.
|
||||
//!
|
||||
//! At the end (spec 8.2.4 exit process) the decoder has consumed `S` renormalisation bits plus a
|
||||
//! 15-bit window, and the bits from position `S` on must be a one followed by zeros. In the
|
||||
//! complemented domain that window is `0b011_1111_1111_1111`, so the encoder picks the `S`-bit
|
||||
//! prefix `D` with `D * 2^15 + 2^14 - 1` inside `[low, low + range)`, i.e.
|
||||
//! `D = (low + 2^14) >> 15`, and writes `!D`, a one bit and zero padding.
|
||||
|
||||
pub(crate) struct SymbolWriter {
|
||||
/// Low end of the interval, holding `15 + cnt` bits not yet moved to `out` (plus carries).
|
||||
low: u64,
|
||||
rng: u32,
|
||||
cnt: u32,
|
||||
out: Vec<u8>,
|
||||
pub disable_update: bool,
|
||||
}
|
||||
|
||||
impl SymbolWriter {
|
||||
pub fn new(disable_update: bool) -> Self {
|
||||
SymbolWriter { low: 0, rng: 1 << 15, cnt: 0, out: Vec::with_capacity(1 << 16), disable_update }
|
||||
}
|
||||
|
||||
/// Adds `k` to the bytes already output (carry propagation).
|
||||
fn carry(&mut self, mut k: u64) {
|
||||
let mut i = self.out.len();
|
||||
while k != 0 {
|
||||
i -= 1;
|
||||
let t = self.out[i] as u64 + k;
|
||||
self.out[i] = t as u8;
|
||||
k = t >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn encode(&mut self, cdf: &[u16], n: usize, s: usize) {
|
||||
let r8 = self.rng >> 8;
|
||||
let cur = |k: usize| -> u32 { ((r8 * ((32768 - cdf[k] as u32) >> 6)) >> 1) + 4 * (n - k - 1) as u32 };
|
||||
let hi = if s == 0 { self.rng } else { cur(s - 1) };
|
||||
let lo = cur(s);
|
||||
debug_assert!(hi > lo, "symbol {s} has an empty interval");
|
||||
self.low += lo as u64;
|
||||
self.rng = hi - lo;
|
||||
let d = 15 - (31 - self.rng.leading_zeros());
|
||||
if d == 0 {
|
||||
return;
|
||||
}
|
||||
self.rng <<= d;
|
||||
self.low <<= d;
|
||||
self.cnt += d;
|
||||
while self.cnt >= 8 {
|
||||
let sh = 15 + self.cnt - 8;
|
||||
let c = self.low >> sh;
|
||||
self.low &= (1u64 << sh) - 1;
|
||||
self.cnt -= 8;
|
||||
self.carry(c >> 8);
|
||||
self.out.push((c & 0xff) as u8);
|
||||
}
|
||||
}
|
||||
|
||||
/// Codes `s` with the adaptive CDF `cdf` (N values plus the adaptation counter).
|
||||
#[inline]
|
||||
pub fn symbol(&mut self, s: usize, cdf: &mut [u16]) {
|
||||
let n = cdf.len() - 1;
|
||||
debug_assert!(s < n);
|
||||
self.encode(cdf, n, s);
|
||||
if !self.disable_update {
|
||||
update_cdf(cdf, n, s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Codes `s` with a CDF that is not adapted.
|
||||
pub fn symbol_fixed(&mut self, s: usize, cdf: &[u16]) {
|
||||
let n = cdf.len() - 1;
|
||||
self.encode(cdf, n, s);
|
||||
}
|
||||
|
||||
/// `read_bool()` counterpart.
|
||||
pub fn bool(&mut self, b: bool) {
|
||||
self.encode(&[1 << 14, 1 << 15, 0], 2, b as usize);
|
||||
}
|
||||
|
||||
/// `L(n)` counterpart (most significant bit first).
|
||||
#[cfg(test)]
|
||||
pub fn literal(&mut self, n: u32, v: u32) {
|
||||
for i in (0..n).rev() {
|
||||
self.bool((v >> i) & 1 != 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Finishes the tile: returns its bytes (with the trailing one bit and zero padding).
|
||||
pub fn finish(mut self) -> Vec<u8> {
|
||||
let low = self.low + (1 << 14);
|
||||
let mut tail = low >> 15;
|
||||
self.carry(tail >> self.cnt);
|
||||
tail &= (1u64 << self.cnt) - 1;
|
||||
let mut bytes: Vec<u8> = self.out.iter().map(|b| !b).collect();
|
||||
let n = self.cnt + 1; // 1..=8 bits: !tail then the trailing one bit
|
||||
let v = (((!tail) & ((1u64 << self.cnt) - 1)) << 1) | 1;
|
||||
bytes.push((v << (8 - n)) as u8);
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
/// CDF adaptation (spec 8.2.6, as in the decoder).
|
||||
#[inline(always)]
|
||||
pub(crate) fn update_cdf(cdf: &mut [u16], n: usize, symbol: usize) {
|
||||
let count = cdf[n];
|
||||
let rate = 3 + (count > 15) as u32 + (count > 31) as u32 + (31 - (n as u32).leading_zeros()).min(2);
|
||||
let mut tmp = 0u32;
|
||||
for i in 0..n - 1 {
|
||||
if i == symbol {
|
||||
tmp = 1 << 15;
|
||||
}
|
||||
let c = cdf[i] as u32;
|
||||
if tmp < c {
|
||||
cdf[i] = (c - ((c - tmp) >> rate)) as u16;
|
||||
} else {
|
||||
cdf[i] = (c + ((tmp - c) >> rate)) as u16;
|
||||
}
|
||||
}
|
||||
cdf[n] += (count < 32) as u16;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The spec's symbol decoder (8.2.2 - 8.2.4), for checking the encoder.
|
||||
struct Dec<'a> {
|
||||
data: &'a [u8],
|
||||
pos: usize,
|
||||
value: u32,
|
||||
range: u32,
|
||||
max_bits: i64,
|
||||
}
|
||||
|
||||
impl<'a> Dec<'a> {
|
||||
fn new(data: &'a [u8]) -> Self {
|
||||
let mut d = Dec { data, pos: 0, value: 0, range: 1 << 15, max_bits: 8 * data.len() as i64 - 15 };
|
||||
let nb = (data.len() * 8).min(15) as u32;
|
||||
let buf = d.bits(nb);
|
||||
d.value = ((1 << 15) - 1) ^ (buf << (15 - nb));
|
||||
d
|
||||
}
|
||||
fn bits(&mut self, n: u32) -> u32 {
|
||||
let mut x = 0;
|
||||
for _ in 0..n {
|
||||
let b = if self.pos < self.data.len() * 8 { (self.data[self.pos >> 3] >> (7 - (self.pos & 7))) & 1 } else { 0 };
|
||||
self.pos += 1;
|
||||
x = (x << 1) | b as u32;
|
||||
}
|
||||
x
|
||||
}
|
||||
fn symbol(&mut self, cdf: &mut [u16], adapt: bool) -> usize {
|
||||
let n = cdf.len() - 1;
|
||||
let mut cur = self.range;
|
||||
let mut s = 0;
|
||||
let mut prev;
|
||||
loop {
|
||||
prev = cur;
|
||||
let f = (1u32 << 15) - cdf[s] as u32;
|
||||
cur = (((self.range >> 8) * (f >> 6)) >> 1) + 4 * (n - s - 1) as u32;
|
||||
if self.value >= cur {
|
||||
break;
|
||||
}
|
||||
s += 1;
|
||||
}
|
||||
self.range = prev - cur;
|
||||
self.value -= cur;
|
||||
let b = 15 - (31 - self.range.leading_zeros());
|
||||
if b > 0 {
|
||||
self.range <<= b;
|
||||
let nb = (b as i64).min(self.max_bits.max(0)) as u32;
|
||||
let nd = self.bits(nb) << (b - nb);
|
||||
self.value = nd ^ (((self.value + 1) << b) - 1);
|
||||
self.max_bits -= b as i64;
|
||||
}
|
||||
if adapt {
|
||||
update_cdf(cdf, n, s);
|
||||
}
|
||||
s
|
||||
}
|
||||
/// Position of the trailing one bit (8.2.4).
|
||||
fn trailing_position(&self) -> i64 {
|
||||
let consumed = (self.pos as i64).min(self.data.len() as i64 * 8);
|
||||
consumed - (15i64).min(self.max_bits + 15)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bools_only() {
|
||||
for len in 1..200 {
|
||||
let bits: Vec<bool> = (0..len).map(|i| (i * 7 + len) % 3 == 0).collect();
|
||||
let mut w = SymbolWriter::new(false);
|
||||
for &b in &bits {
|
||||
w.bool(b);
|
||||
}
|
||||
let data = w.finish();
|
||||
let mut d = Dec::new(&data);
|
||||
for (i, &b) in bits.iter().enumerate() {
|
||||
assert_eq!(d.symbol(&mut [1 << 14, 1 << 15, 0], false) == 1, b, "len {len} bit {i} data {data:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_random_symbols() {
|
||||
let mut seed = 0x1234_5678u32;
|
||||
let mut rnd = || {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 17;
|
||||
seed ^= seed << 5;
|
||||
seed
|
||||
};
|
||||
for trial in 0..300 {
|
||||
let count = 1 + (rnd() % 3000) as usize;
|
||||
let mut cdfs_e = vec![[4096u16, 11264, 19328, 32768, 0]; 4];
|
||||
let mut cdfs_d = cdfs_e.clone();
|
||||
let mut syms = Vec::new();
|
||||
let mut w = SymbolWriter::new(false);
|
||||
for _ in 0..count {
|
||||
let kind = rnd() % 3;
|
||||
let ctx = (rnd() % 4) as usize;
|
||||
// skewed symbols so that the range also gets small
|
||||
let s = if rnd() % 4 == 0 { (rnd() % 4) as usize } else { 0 };
|
||||
match kind {
|
||||
0 => w.symbol(s, &mut cdfs_e[ctx]),
|
||||
1 => w.bool(s & 1 == 1),
|
||||
_ => w.literal(5, s as u32 * 7),
|
||||
}
|
||||
syms.push((kind, ctx, s));
|
||||
}
|
||||
let data = w.finish();
|
||||
let mut d = Dec::new(&data);
|
||||
for (idx, &(kind, ctx, s)) in syms.iter().enumerate() {
|
||||
let got = match kind {
|
||||
0 => d.symbol(&mut cdfs_d[ctx], true),
|
||||
1 => d.symbol(&mut [1 << 14, 1 << 15, 0], false),
|
||||
_ => {
|
||||
let mut v = 0;
|
||||
for _ in 0..5 {
|
||||
v = (v << 1) | d.symbol(&mut [1 << 14, 1 << 15, 0], false);
|
||||
}
|
||||
v / 7
|
||||
}
|
||||
};
|
||||
let want = if kind == 1 { s & 1 } else { s };
|
||||
assert_eq!(got, want, "trial {trial} idx {idx} kind {kind} of {}", syms.len());
|
||||
}
|
||||
let tp = d.trailing_position();
|
||||
assert!(tp >= 0 && (tp as usize) < data.len() * 8, "trial {trial}: trailing bit position {tp}");
|
||||
let tp = tp as usize;
|
||||
assert_eq!((data[tp >> 3] >> (7 - (tp & 7))) & 1, 1, "trial {trial}: trailing one bit");
|
||||
for p in tp + 1..data.len() * 8 {
|
||||
assert_eq!((data[p >> 3] >> (7 - (p & 7))) & 1, 0, "trial {trial}: padding");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,481 @@
|
|||
//! Transforms. The inverse transforms are the normative ones of spec 7.13 (mirrored from
|
||||
//! filmcraft-av1, so that the encoder reconstructs exactly like a decoder); the forward
|
||||
//! transforms are floating-point matrices scaled to match them (not normative).
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::tables::*;
|
||||
|
||||
#[inline(always)]
|
||||
fn round2_64(x: i64, n: u32) -> i32 {
|
||||
if n == 0 { x as i32 } else { ((x + (1i64 << (n - 1))) >> n) as i32 }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn round2(x: i32, n: u32) -> i32 {
|
||||
if n == 0 { x } else { (x + (1 << (n - 1))) >> n }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn cos128(angle: i32) -> i64 {
|
||||
let a = (angle & 255) as usize;
|
||||
(match a {
|
||||
0..=64 => COS128_LOOKUP[a] as i32,
|
||||
65..=128 => -(COS128_LOOKUP[128 - a] as i32),
|
||||
129..=192 => -(COS128_LOOKUP[a - 128] as i32),
|
||||
_ => COS128_LOOKUP[256 - a] as i32,
|
||||
}) as i64
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn sin128(angle: i32) -> i64 {
|
||||
cos128(angle - 64)
|
||||
}
|
||||
|
||||
fn brev(num_bits: u32, x: usize) -> usize {
|
||||
let mut t = 0;
|
||||
for i in 0..num_bits {
|
||||
let bit = (x >> i) & 1;
|
||||
t += bit << (num_bits - 1 - i);
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn bf(t: &mut [i32], a: usize, b: usize, angle: i32, flip: bool) {
|
||||
let x = t[a] as i64 * cos128(angle) - t[b] as i64 * sin128(angle);
|
||||
let y = t[a] as i64 * sin128(angle) + t[b] as i64 * cos128(angle);
|
||||
t[a] = round2_64(x, 12);
|
||||
t[b] = round2_64(y, 12);
|
||||
if flip {
|
||||
t.swap(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn hd(t: &mut [i32], a: usize, b: usize, flip: bool, r: u32) {
|
||||
let (a, b) = if flip { (b, a) } else { (a, b) };
|
||||
let x = t[a];
|
||||
let y = t[b];
|
||||
let lo = -(1i32 << (r - 1));
|
||||
let hi = (1i32 << (r - 1)) - 1;
|
||||
t[a] = (x + y).clamp(lo, hi);
|
||||
t[b] = (x - y).clamp(lo, hi);
|
||||
}
|
||||
|
||||
/// Inverse DCT of length 2^n, n = 2..=5 (7.13.2.3), in place.
|
||||
fn inverse_dct(t: &mut [i32], n: u32, r: u32) {
|
||||
let len = 1usize << n;
|
||||
let mut copy = [0i32; 32];
|
||||
copy[..len].copy_from_slice(&t[..len]);
|
||||
for i in 0..len {
|
||||
t[i] = copy[brev(n, i)];
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..8 {
|
||||
bf(t, 16 + i, 31 - i, 6 + ((brev(3, 7 - i) as i32) << 3), false);
|
||||
}
|
||||
}
|
||||
if n >= 4 {
|
||||
for i in 0..4 {
|
||||
bf(t, 8 + i, 15 - i, 12 + ((brev(2, 3 - i) as i32) << 4), false);
|
||||
}
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..8 {
|
||||
hd(t, 16 + 2 * i, 17 + 2 * i, i & 1 == 1, r);
|
||||
}
|
||||
}
|
||||
if n >= 3 {
|
||||
for i in 0..2 {
|
||||
bf(t, 4 + i, 7 - i, 56 - 32 * i as i32, false);
|
||||
}
|
||||
}
|
||||
if n >= 4 {
|
||||
for i in 0..4 {
|
||||
hd(t, 8 + 2 * i, 9 + 2 * i, i & 1 == 1, r);
|
||||
}
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..2 {
|
||||
for j in 0..2 {
|
||||
bf(t, 30 - 4 * i - j, 17 + 4 * i + j, 24 + ((j as i32) << 6) + (((1 - i) as i32) << 5), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in 0..2 {
|
||||
bf(t, 2 * i, 2 * i + 1, 32 + 16 * i as i32, i == 0);
|
||||
}
|
||||
if n >= 3 {
|
||||
for i in 0..2 {
|
||||
hd(t, 4 + 2 * i, 5 + 2 * i, i == 1, r);
|
||||
}
|
||||
}
|
||||
if n >= 4 {
|
||||
for i in 0..2 {
|
||||
bf(t, 14 - i, 9 + i, 48 + 64 * i as i32, true);
|
||||
}
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..4 {
|
||||
for j in 0..2 {
|
||||
hd(t, 16 + 4 * i + j, 19 + 4 * i - j, i & 1 == 1, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in 0..2 {
|
||||
hd(t, i, 3 - i, false, r);
|
||||
}
|
||||
if n >= 3 {
|
||||
bf(t, 6, 5, 32, true);
|
||||
}
|
||||
if n >= 4 {
|
||||
for i in 0..2 {
|
||||
for j in 0..2 {
|
||||
hd(t, 8 + 4 * i + j, 11 + 4 * i - j, i == 1, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..4 {
|
||||
bf(t, 29 - i, 18 + i, 48 + (i as i32 >> 1) * 64, true);
|
||||
}
|
||||
}
|
||||
if n >= 3 {
|
||||
for i in 0..4 {
|
||||
hd(t, i, 7 - i, false, r);
|
||||
}
|
||||
}
|
||||
if n >= 4 {
|
||||
for i in 0..2 {
|
||||
bf(t, 13 - i, 10 + i, 32, true);
|
||||
}
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..2 {
|
||||
for j in 0..4 {
|
||||
hd(t, 16 + i * 8 + j, 23 + i * 8 - j, i == 1, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
if n >= 4 {
|
||||
for i in 0..8 {
|
||||
hd(t, i, 15 - i, false, r);
|
||||
}
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..4 {
|
||||
bf(t, 27 - i, 20 + i, 32, true);
|
||||
}
|
||||
}
|
||||
if n >= 5 {
|
||||
for i in 0..16 {
|
||||
hd(t, i, 31 - i, false, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SINPI_1_9: i64 = 1321;
|
||||
const SINPI_2_9: i64 = 2482;
|
||||
const SINPI_3_9: i64 = 3344;
|
||||
const SINPI_4_9: i64 = 3803;
|
||||
|
||||
fn inverse_adst4(t: &mut [i32]) {
|
||||
let (t0, t1, t2, t3) = (t[0] as i64, t[1] as i64, t[2] as i64, t[3] as i64);
|
||||
let mut s = [SINPI_1_9 * t0, SINPI_2_9 * t0, SINPI_3_9 * t1, SINPI_4_9 * t2, SINPI_1_9 * t2, SINPI_2_9 * t3, SINPI_4_9 * t3];
|
||||
let a7 = t0 - t2;
|
||||
let b7 = a7 + t3;
|
||||
s[0] += s[3];
|
||||
s[1] -= s[4];
|
||||
s[3] = s[2];
|
||||
s[2] = SINPI_3_9 * b7;
|
||||
s[0] += s[5];
|
||||
s[1] -= s[6];
|
||||
let x = [s[0] + s[3], s[1] + s[3], s[2], s[0] + s[1] - s[3]];
|
||||
for i in 0..4 {
|
||||
t[i] = round2_64(x[i], 12);
|
||||
}
|
||||
}
|
||||
|
||||
fn adst_in_permute(t: &mut [i32], n: u32) {
|
||||
let n0 = 1usize << n;
|
||||
let mut copy = [0i32; 16];
|
||||
copy[..n0].copy_from_slice(&t[..n0]);
|
||||
for i in 0..n0 {
|
||||
let idx = if i & 1 == 1 { i - 1 } else { n0 - i - 1 };
|
||||
t[i] = copy[idx];
|
||||
}
|
||||
}
|
||||
|
||||
fn adst_out_permute(t: &mut [i32], n: u32) {
|
||||
let n0 = 1usize << n;
|
||||
let mut copy = [0i32; 16];
|
||||
copy[..n0].copy_from_slice(&t[..n0]);
|
||||
for i in 0..n0 {
|
||||
let a = (i >> 3) & 1;
|
||||
let b = ((i >> 2) & 1) ^ ((i >> 3) & 1);
|
||||
let c = ((i >> 1) & 1) ^ ((i >> 2) & 1);
|
||||
let d = (i & 1) ^ ((i >> 1) & 1);
|
||||
let idx = ((d << 3) | (c << 2) | (b << 1) | a) >> (4 - n);
|
||||
t[i] = if i & 1 == 1 { -copy[idx] } else { copy[idx] };
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_adst8(t: &mut [i32], r: u32) {
|
||||
adst_in_permute(t, 3);
|
||||
for i in 0..4 {
|
||||
bf(t, 2 * i, 2 * i + 1, 60 - 16 * i as i32, true);
|
||||
}
|
||||
for i in 0..4 {
|
||||
hd(t, i, 4 + i, false, r);
|
||||
}
|
||||
for i in 0..2 {
|
||||
bf(t, 4 + 3 * i, 5 + i, 48 - 32 * i as i32, true);
|
||||
}
|
||||
for i in 0..2 {
|
||||
for j in 0..2 {
|
||||
hd(t, 4 * j + i, 2 + 4 * j + i, false, r);
|
||||
}
|
||||
}
|
||||
for i in 0..2 {
|
||||
bf(t, 2 + 4 * i, 3 + 4 * i, 32, true);
|
||||
}
|
||||
adst_out_permute(t, 3);
|
||||
}
|
||||
|
||||
fn inverse_adst16(t: &mut [i32], r: u32) {
|
||||
adst_in_permute(t, 4);
|
||||
for i in 0..8 {
|
||||
bf(t, 2 * i, 2 * i + 1, 62 - 8 * i as i32, true);
|
||||
}
|
||||
for i in 0..8 {
|
||||
hd(t, i, 8 + i, false, r);
|
||||
}
|
||||
for i in 0..2 {
|
||||
bf(t, 8 + 2 * i, 9 + 2 * i, 56 - 32 * i as i32, true);
|
||||
bf(t, 13 + 2 * i, 12 + 2 * i, 8 + 32 * i as i32, true);
|
||||
}
|
||||
for i in 0..4 {
|
||||
for j in 0..2 {
|
||||
hd(t, 8 * j + i, 4 + 8 * j + i, false, r);
|
||||
}
|
||||
}
|
||||
for i in 0..2 {
|
||||
for j in 0..2 {
|
||||
bf(t, 4 + 8 * j + 3 * i, 5 + 8 * j + i, 48 - 32 * i as i32, true);
|
||||
}
|
||||
}
|
||||
for i in 0..2 {
|
||||
for j in 0..4 {
|
||||
hd(t, 4 * j + i, 2 + 4 * j + i, false, r);
|
||||
}
|
||||
}
|
||||
for i in 0..4 {
|
||||
bf(t, 2 + 4 * i, 3 + 4 * i, 32, true);
|
||||
}
|
||||
adst_out_permute(t, 4);
|
||||
}
|
||||
|
||||
/// 1-D transform kind.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub(crate) enum Kind {
|
||||
Dct,
|
||||
Adst,
|
||||
}
|
||||
|
||||
/// (vertical / column kind, horizontal / row kind) of a transform type.
|
||||
pub(crate) fn kinds(tx_type: usize) -> (Kind, Kind) {
|
||||
match tx_type {
|
||||
DCT_DCT => (Kind::Dct, Kind::Dct),
|
||||
ADST_DCT => (Kind::Adst, Kind::Dct),
|
||||
DCT_ADST => (Kind::Dct, Kind::Adst),
|
||||
ADST_ADST => (Kind::Adst, Kind::Adst),
|
||||
_ => unreachable!("unsupported transform type {tx_type}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_1d(t: &mut [i32], kind: Kind, n: u32, r: u32) {
|
||||
match kind {
|
||||
Kind::Dct => inverse_dct(t, n, r),
|
||||
Kind::Adst => match n {
|
||||
2 => inverse_adst4(t),
|
||||
3 => inverse_adst8(t, r),
|
||||
_ => inverse_adst16(t, r),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 2-D inverse transform (7.13.3) of a square `n x n` block (n = 4..=32) of dequantised
|
||||
/// coefficients (row stride `n`) into `residual` (row stride `n`).
|
||||
pub(crate) fn inverse_2d(dequant: &[i32], residual: &mut [i32], tx_sz: usize, tx_type: usize, bit_depth: u32) {
|
||||
let log2 = TX_WIDTH_LOG2[tx_sz] as u32;
|
||||
let n = 1usize << log2;
|
||||
let row_shift = TRANSFORM_ROW_SHIFT[tx_sz] as u32;
|
||||
let row_clamp = bit_depth + 8;
|
||||
let col_clamp = (bit_depth + 6).max(16);
|
||||
let (ck, rk) = kinds(tx_type);
|
||||
let lo = -(1i32 << (col_clamp - 1));
|
||||
let hi = (1i32 << (col_clamp - 1)) - 1;
|
||||
let mut t = [0i32; 32];
|
||||
for i in 0..n {
|
||||
let row = &dequant[i * n..i * n + n];
|
||||
if row.iter().all(|&v| v == 0) {
|
||||
residual[i * n..i * n + n].iter_mut().for_each(|v| *v = 0);
|
||||
continue;
|
||||
}
|
||||
t[..n].copy_from_slice(row);
|
||||
inverse_1d(&mut t, rk, log2, row_clamp);
|
||||
for j in 0..n {
|
||||
residual[i * n + j] = round2(t[j], row_shift).clamp(lo, hi);
|
||||
}
|
||||
}
|
||||
for j in 0..n {
|
||||
for i in 0..n {
|
||||
t[i] = residual[i * n + j];
|
||||
}
|
||||
inverse_1d(&mut t, ck, log2, col_clamp);
|
||||
for i in 0..n {
|
||||
residual[i * n + j] = round2(t[i], 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Orthonormal forward basis matrices `[k][i]` (row k = basis function k), sizes 4..=32.
|
||||
struct Bases {
|
||||
dct: [Vec<f32>; 4],
|
||||
adst: [Vec<f32>; 3],
|
||||
/// The same matrices transposed (`[i][k]`).
|
||||
dct_t: [Vec<f32>; 4],
|
||||
adst_t: [Vec<f32>; 3],
|
||||
}
|
||||
|
||||
fn transpose(m: &[f32]) -> Vec<f32> {
|
||||
let n = (m.len() as f64).sqrt() as usize;
|
||||
let mut t = vec![0f32; n * n];
|
||||
for k in 0..n {
|
||||
for i in 0..n {
|
||||
t[i * n + k] = m[k * n + i];
|
||||
}
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn bases() -> &'static Bases {
|
||||
static B: OnceLock<Bases> = OnceLock::new();
|
||||
B.get_or_init(|| {
|
||||
let pi = std::f64::consts::PI;
|
||||
let dct = std::array::from_fn(|l| {
|
||||
let n = 4usize << l;
|
||||
let mut m = vec![0f32; n * n];
|
||||
for k in 0..n {
|
||||
let c = if k == 0 { std::f64::consts::FRAC_1_SQRT_2 } else { 1.0 };
|
||||
for i in 0..n {
|
||||
m[k * n + i] = (c * (2.0 / n as f64).sqrt() * (pi * (2 * i + 1) as f64 * k as f64 / (2 * n) as f64).cos()) as f32;
|
||||
}
|
||||
}
|
||||
m
|
||||
});
|
||||
let adst = std::array::from_fn(|l| {
|
||||
let n = 4usize << l;
|
||||
let mut m = vec![0f32; n * n];
|
||||
for k in 0..n {
|
||||
for i in 0..n {
|
||||
m[k * n + i] = if n == 4 {
|
||||
// the AV1 4-point ADST (sin(pi (i + 1)(2k + 1) / 9) basis)
|
||||
(2.0 / 3.0 * (pi * ((i + 1) * (2 * k + 1)) as f64 / 9.0).sin()) as f32
|
||||
} else {
|
||||
((2.0 / n as f64).sqrt() * (pi * ((2 * i + 1) * (2 * k + 1)) as f64 / (4 * n) as f64).sin()) as f32
|
||||
};
|
||||
}
|
||||
}
|
||||
m
|
||||
});
|
||||
let dct_t = std::array::from_fn(|l| transpose(&dct[l]));
|
||||
let adst_t = std::array::from_fn(|l| transpose(&adst[l]));
|
||||
Bases { dct, adst, dct_t, adst_t }
|
||||
})
|
||||
}
|
||||
|
||||
fn basis_t(kind: Kind, n: usize) -> &'static [f32] {
|
||||
let l = n.trailing_zeros() as usize - 2;
|
||||
match kind {
|
||||
Kind::Dct => &bases().dct_t[l],
|
||||
Kind::Adst => &bases().adst_t[l],
|
||||
}
|
||||
}
|
||||
|
||||
fn basis(kind: Kind, n: usize) -> &'static [f32] {
|
||||
let l = n.trailing_zeros() as usize - 2;
|
||||
match kind {
|
||||
Kind::Dct => &bases().dct[l],
|
||||
Kind::Adst => &bases().adst[l],
|
||||
}
|
||||
}
|
||||
|
||||
/// Forward 2-D transform of the `n x n` residual (row stride `n`) into orthonormal-domain
|
||||
/// coefficients scaled by 8 (so that `level = out / q` with the spec's dequantiser).
|
||||
pub(crate) fn forward_2d(residual: &[i32], out: &mut [f32], n: usize, tx_type: usize) {
|
||||
let (ck, rk) = kinds(tx_type);
|
||||
let bc = basis(ck, n);
|
||||
let brt = basis_t(rk, n);
|
||||
// tmp[i][k] = sum_j residual[i][j] * br[k][j] (horizontal; written as row updates so that
|
||||
// the inner loops vectorise)
|
||||
let mut tmp = [0f32; 32 * 32];
|
||||
for i in 0..n {
|
||||
let t = &mut tmp[i * n..i * n + n];
|
||||
for j in 0..n {
|
||||
let r = residual[i * n + j];
|
||||
if r == 0 {
|
||||
continue;
|
||||
}
|
||||
let r = r as f32;
|
||||
for (o, &b) in t.iter_mut().zip(&brt[j * n..j * n + n]) {
|
||||
*o += r * b;
|
||||
}
|
||||
}
|
||||
}
|
||||
// out[k][j] = 8 * sum_i bc[k][i] * tmp[i][j] (vertical)
|
||||
out[..n * n].iter_mut().for_each(|v| *v = 0.0);
|
||||
for k in 0..n {
|
||||
let o = &mut out[k * n..k * n + n];
|
||||
for i in 0..n {
|
||||
let b = 8.0 * bc[k * n + i];
|
||||
for (v, &t) in o.iter_mut().zip(&tmp[i * n..i * n + n]) {
|
||||
*v += b * t;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// forward (float) then the normative inverse reproduces the residual: the forward basis
|
||||
/// and scale match the decoder's inverse transforms for every supported size and type.
|
||||
#[test]
|
||||
fn forward_matches_inverse() {
|
||||
let mut seed = 99u32;
|
||||
let mut rnd = || {
|
||||
seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12345);
|
||||
((seed >> 16) % 201) as i32 - 100
|
||||
};
|
||||
for (tx, n) in [(TX_4X4, 4), (TX_8X8, 8), (TX_16X16, 16), (TX_32X32, 32)] {
|
||||
let denom = if tx == TX_32X32 { 2.0 } else { 1.0 };
|
||||
for tx_type in [DCT_DCT, ADST_DCT, DCT_ADST, ADST_ADST] {
|
||||
if n == 32 && tx_type != DCT_DCT {
|
||||
continue;
|
||||
}
|
||||
let res: Vec<i32> = (0..n * n).map(|_| rnd()).collect();
|
||||
let mut coef = vec![0f32; n * n];
|
||||
forward_2d(&res, &mut coef, n, tx_type);
|
||||
// q = 8 / denom keeps full precision: dequant = level * q / denom = coef
|
||||
let deq: Vec<i32> = coef.iter().map(|&c| (c / denom).round() as i32).collect();
|
||||
let mut out = vec![0i32; n * n];
|
||||
inverse_2d(&deq, &mut out, tx, tx_type, 8);
|
||||
let err: i32 = res.iter().zip(&out).map(|(a, b)| (a - b).abs()).max().unwrap();
|
||||
assert!(err <= 1, "tx {tx} type {tx_type}: max error {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,136 @@
|
|||
//! Speed / efficiency measurements on ffmpeg-generated test sources (ignored by default):
|
||||
//! `cargo test -p effectcraft-av1enc --test bench -- --ignored --nocapture`.
|
||||
|
||||
mod common;
|
||||
|
||||
use std::process::Command;
|
||||
use std::time::Instant;
|
||||
|
||||
use common::*;
|
||||
use effectcraft_av1enc::{Encoder, EncoderConfig, RateControl};
|
||||
|
||||
/// Raw 8-bit 4:2:0 frames of an ffmpeg lavfi source, or None without ffmpeg.
|
||||
fn lavfi(src: &str, w: usize, h: usize, frames: usize) -> Option<Vec<Pic>> {
|
||||
let out = std::env::temp_dir().join(format!("effectcraft-av1enc-bench-{}-{src}-{w}x{h}.yuv", std::process::id()));
|
||||
let st = Command::new("ffmpeg")
|
||||
.args(["-v", "error", "-y", "-f", "lavfi", "-i"])
|
||||
.arg(format!("{src}=size={w}x{h}:rate=30"))
|
||||
.args(["-frames:v", &frames.to_string(), "-pix_fmt", "yuv420p", "-f", "rawvideo"])
|
||||
.arg(&out)
|
||||
.status()
|
||||
.ok()?;
|
||||
if !st.success() {
|
||||
return None;
|
||||
}
|
||||
let raw = std::fs::read(&out).ok()?;
|
||||
let _ = std::fs::remove_file(&out);
|
||||
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
|
||||
let fs = w * h + 2 * cw * ch;
|
||||
Some(
|
||||
raw.chunks_exact(fs)
|
||||
.map(|f| Pic {
|
||||
w,
|
||||
h,
|
||||
y: f[..w * h].iter().map(|&b| b as u16).collect(),
|
||||
u: f[w * h..w * h + cw * ch].iter().map(|&b| b as u16).collect(),
|
||||
v: f[w * h + cw * ch..].iter().map(|&b| b as u16).collect(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn run(name: &str, pics: &[Pic], rate: RateControl, keyint: u32) {
|
||||
let (w, h) = (pics[0].w, pics[0].h);
|
||||
let mut cfg = EncoderConfig::new(w as u32, h as u32, 30, 1);
|
||||
cfg.rate = rate;
|
||||
cfg.keyint = keyint;
|
||||
let mut enc = Encoder::new(cfg).unwrap();
|
||||
let mut bytes = 0;
|
||||
let mut psnr_sum = 0.0;
|
||||
let mut t_key = 0.0;
|
||||
let mut t_inter = 0.0;
|
||||
let mut sizes = Vec::new();
|
||||
for p in pics {
|
||||
let t = Instant::now();
|
||||
let pkt = enc.encode(&p.frame());
|
||||
let dt = t.elapsed().as_secs_f64();
|
||||
if pkt.keyframe {
|
||||
t_key += dt;
|
||||
} else {
|
||||
t_inter += dt;
|
||||
}
|
||||
bytes += pkt.data.len();
|
||||
sizes.push(pkt.data.len());
|
||||
let rec = enc.last_reconstruction().unwrap();
|
||||
psnr_sum += psnr(&p.y, &rec[0], 8);
|
||||
}
|
||||
let n = pics.len() as f64;
|
||||
let n_key = pics.len().div_ceil(keyint as usize) as f64;
|
||||
eprintln!(
|
||||
"{name} {w}x{h} {rate:?}: {:.0} kbit/s, PSNR-Y {:.2} dB, key {:.2} s/frame, inter {:.2} s/frame, sizes {:?}",
|
||||
bytes as f64 * 8.0 * 30.0 / n / 1000.0,
|
||||
psnr_sum / n,
|
||||
t_key / n_key,
|
||||
if n > n_key { t_inter / (n - n_key) } else { 0.0 },
|
||||
&sizes[..sizes.len().min(6)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_lavfi() {
|
||||
if std::env::var("AV1ENC_BENCH_QUICK").is_err() {
|
||||
let Some(pics) = lavfi("testsrc2", 1920, 1080, 6) else {
|
||||
eprintln!("ffmpeg not available");
|
||||
return;
|
||||
};
|
||||
run("testsrc2", &pics, RateControl::ConstantQ(100), 30);
|
||||
}
|
||||
if let Some(pics) = lavfi("mandelbrot", 640, 360, 20) {
|
||||
for q in [40, 80, 120, 160] {
|
||||
run("mandelbrot", &pics, RateControl::ConstantQ(q), 30);
|
||||
}
|
||||
run("mandelbrot", &pics, RateControl::Bitrate { kbps: 1000 }, 30);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_intra() {
|
||||
for src in ["mandelbrot", "sierpinski", "gradients"] {
|
||||
let Some(pics) = lavfi(src, 640, 360, 4) else {
|
||||
return;
|
||||
};
|
||||
for q in [40, 80, 120, 160] {
|
||||
run(&format!("{src}-intra"), &pics, RateControl::ConstantQ(q), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_1080p_loop() {
|
||||
let Some(pics) = lavfi("mandelbrot", 1920, 1080, 4) else {
|
||||
return;
|
||||
};
|
||||
run("mandelbrot", &pics, RateControl::ConstantQ(120), 30);
|
||||
run("mandelbrot", &pics, RateControl::ConstantQ(120), 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_rate_control() {
|
||||
let Some(pics) = lavfi("mandelbrot", 480, 272, 60) else {
|
||||
eprintln!("ffmpeg not available");
|
||||
return;
|
||||
};
|
||||
for kbps in [200, 600, 2000] {
|
||||
run("mandelbrot", &pics, RateControl::Bitrate { kbps }, 30);
|
||||
}
|
||||
let Some(pics) = lavfi("testsrc2", 480, 272, 60) else {
|
||||
return;
|
||||
};
|
||||
for kbps in [300, 3000] {
|
||||
run("testsrc2", &pics, RateControl::Bitrate { kbps }, 60);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use effectcraft_av1enc::{Encoder, EncoderConfig, Frame, Packet};
|
||||
|
||||
/// A synthetic 4:2:0 picture.
|
||||
pub struct Pic {
|
||||
pub w: usize,
|
||||
pub h: usize,
|
||||
pub y: Vec<u16>,
|
||||
pub u: Vec<u16>,
|
||||
pub v: Vec<u16>,
|
||||
}
|
||||
|
||||
impl Pic {
|
||||
pub fn frame(&self) -> Frame<'_> {
|
||||
Frame { y: &self.y, u: &self.u, v: &self.v, y_stride: self.w, uv_stride: self.w.div_ceil(2) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Smooth gradients with a soft blob, moved by (dx, dy) samples per frame index `t`, plus a
|
||||
/// textured rectangle that moves differently.
|
||||
pub fn synth(w: usize, h: usize, bd: u32, t: usize) -> Pic {
|
||||
let max = ((1u32 << bd) - 1) as f64;
|
||||
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
|
||||
let sample = |x: f64, y: f64| -> f64 {
|
||||
let fx = x - 2.0 * t as f64;
|
||||
let fy = y - 1.0 * t as f64;
|
||||
let base = 0.25 + 0.5 * (fx / 97.0).sin() * (fy / 61.0).cos() * 0.5 + 0.2 * ((fx + fy) / (w + h) as f64);
|
||||
let d = ((fx - w as f64 / 2.0).powi(2) + (fy - h as f64 / 2.0).powi(2)).sqrt();
|
||||
let blob = 0.3 * (-d * d / (2.0 * (w.min(h) as f64 / 5.0).powi(2))).exp();
|
||||
// a checkered patch moving right by 4 per frame
|
||||
let px = x - 4.0 * t as f64 - w as f64 / 4.0;
|
||||
let py = y - h as f64 / 3.0;
|
||||
let patch =
|
||||
if (0.0..24.0).contains(&px) && (0.0..16.0).contains(&py) { if ((px as i32 / 4) + (py as i32 / 4)) % 2 == 0 { 0.25 } else { -0.25 } } else { 0.0 };
|
||||
(base + blob + patch).clamp(0.0, 1.0)
|
||||
};
|
||||
let mut y = vec![0u16; w * h];
|
||||
for j in 0..h {
|
||||
for i in 0..w {
|
||||
y[j * w + i] = (sample(i as f64, j as f64) * max).round() as u16;
|
||||
}
|
||||
}
|
||||
let mut u = vec![0u16; cw * ch];
|
||||
let mut v = vec![0u16; cw * ch];
|
||||
for j in 0..ch {
|
||||
for i in 0..cw {
|
||||
let s = sample(2.0 * i as f64, 2.0 * j as f64);
|
||||
u[j * cw + i] = ((0.5 + 0.3 * (s - 0.5) + 0.1 * (i as f64 / 13.0).sin()) * max).round() as u16;
|
||||
v[j * cw + i] = ((0.5 - 0.25 * (s - 0.5) + 0.1 * (j as f64 / 7.0).cos()) * max).round() as u16;
|
||||
}
|
||||
}
|
||||
Pic { w, h, y, u, v }
|
||||
}
|
||||
|
||||
/// Deterministic noise picture (hard case for prediction).
|
||||
pub fn noise(w: usize, h: usize, bd: u32, seed: u32) -> Pic {
|
||||
let mut s = seed.wrapping_mul(2_654_435_761).max(1);
|
||||
let mut rnd = || {
|
||||
s ^= s << 13;
|
||||
s ^= s >> 17;
|
||||
s ^= s << 5;
|
||||
s
|
||||
};
|
||||
let mask = (1u32 << bd) - 1;
|
||||
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
|
||||
Pic {
|
||||
w,
|
||||
h,
|
||||
y: (0..w * h).map(|_| (rnd() & mask) as u16).collect(),
|
||||
u: (0..cw * ch).map(|_| (rnd() & mask) as u16).collect(),
|
||||
v: (0..cw * ch).map(|_| (rnd() & mask) as u16).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn psnr(a: &[u16], b: &[u16], bd: u32) -> f64 {
|
||||
let max = ((1u32 << bd) - 1) as f64;
|
||||
let mse = a.iter().zip(b).map(|(&x, &y)| (x as f64 - y as f64).powi(2)).sum::<f64>() / a.len() as f64;
|
||||
if mse == 0.0 { 99.0 } else { 10.0 * (max * max / mse).log10() }
|
||||
}
|
||||
|
||||
pub struct Run {
|
||||
pub packets: Vec<Packet>,
|
||||
pub recons: Vec<[Vec<u16>; 3]>,
|
||||
pub psnr_y: Vec<f64>,
|
||||
pub lf_levels: Vec<u32>,
|
||||
}
|
||||
|
||||
/// Encodes `pics`, checks every packet decodes (filmcraft-av1) to exactly the encoder's
|
||||
/// reconstruction and returns packets, reconstructions and luma PSNRs.
|
||||
pub fn encode_and_check(cfg: EncoderConfig, pics: &[Pic]) -> Run {
|
||||
let bd = cfg.bit_depth as u32;
|
||||
let mut enc = Encoder::new(cfg).expect("config");
|
||||
let mut dec = filmcraft_av1::Decoder::new();
|
||||
let mut run = Run { packets: Vec::new(), recons: Vec::new(), psnr_y: Vec::new(), lf_levels: Vec::new() };
|
||||
for (i, p) in pics.iter().enumerate() {
|
||||
let pkt = enc.encode(&p.frame());
|
||||
let rec = enc.last_reconstruction().expect("reconstruction");
|
||||
let out = dec.decode(&pkt.data).unwrap_or_else(|e| panic!("frame {i}: decode error {e:?}"));
|
||||
assert_eq!(out.len(), 1, "frame {i}: one picture per packet");
|
||||
let d = &out[0];
|
||||
assert_eq!((d.width as usize, d.height as usize), (p.w, p.h));
|
||||
for pl in 0..3 {
|
||||
if d.planes[pl] != rec[pl] {
|
||||
let first = d.planes[pl].iter().zip(&rec[pl]).position(|(a, b)| a != b).unwrap_or(usize::MAX);
|
||||
let pw = if pl == 0 { p.w } else { p.w.div_ceil(2) };
|
||||
panic!("frame {i} plane {pl}: decoder output differs from the reconstruction (first at x {} y {})", first % pw, first / pw);
|
||||
}
|
||||
}
|
||||
run.psnr_y.push(psnr(&p.y, &rec[0], bd));
|
||||
run.lf_levels.push(enc.last_loop_filter_level().expect("level"));
|
||||
run.recons.push(rec);
|
||||
run.packets.push(pkt);
|
||||
}
|
||||
run
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
//! External oracle: streams decoded by ffmpeg's libdav1d must equal the encoder's reconstruction
|
||||
//! bit for bit. Skipped when `ffmpeg` (with libdav1d) is not installed. ffmpeg's native `av1`
|
||||
//! decoder only drives hardware decoders, so it is not used.
|
||||
|
||||
mod common;
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use common::*;
|
||||
use effectcraft_av1enc::{Encoder, EncoderConfig, Packet, RateControl};
|
||||
|
||||
fn have_libdav1d() -> bool {
|
||||
let Ok(out) = Command::new("ffmpeg").args(["-hide_banner", "-decoders"]).output() else {
|
||||
eprintln!("ffmpeg not found: skipping the external oracle");
|
||||
return false;
|
||||
};
|
||||
let ok = out.status.success() && String::from_utf8_lossy(&out.stdout).contains("libdav1d");
|
||||
if !ok {
|
||||
eprintln!("ffmpeg without libdav1d: skipping the external oracle");
|
||||
}
|
||||
ok
|
||||
}
|
||||
|
||||
/// IVF container with one temporal unit (temporal delimiter + packet) per frame.
|
||||
fn ivf(w: usize, h: usize, packets: &[Packet]) -> Vec<u8> {
|
||||
let mut v = Vec::new();
|
||||
v.extend_from_slice(b"DKIF");
|
||||
v.extend_from_slice(&0u16.to_le_bytes());
|
||||
v.extend_from_slice(&32u16.to_le_bytes());
|
||||
v.extend_from_slice(b"AV01");
|
||||
v.extend_from_slice(&(w as u16).to_le_bytes());
|
||||
v.extend_from_slice(&(h as u16).to_le_bytes());
|
||||
v.extend_from_slice(&30u32.to_le_bytes());
|
||||
v.extend_from_slice(&1u32.to_le_bytes());
|
||||
v.extend_from_slice(&(packets.len() as u32).to_le_bytes());
|
||||
v.extend_from_slice(&0u32.to_le_bytes());
|
||||
for (i, p) in packets.iter().enumerate() {
|
||||
v.extend_from_slice(&((p.data.len() + 2) as u32).to_le_bytes());
|
||||
v.extend_from_slice(&(i as u64).to_le_bytes());
|
||||
v.extend_from_slice(&[0x12, 0x00]); // OBU_TEMPORAL_DELIMITER
|
||||
v.extend_from_slice(&p.data);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// Encodes `pics`, decodes the IVF stream with `ffmpeg -c:v libdav1d` and compares every
|
||||
/// decoded frame with the encoder's reconstruction.
|
||||
/// Returns the loop filter levels used.
|
||||
fn check(name: &str, cfg: EncoderConfig, pics: &[Pic]) -> Vec<u32> {
|
||||
let (w, h, bd) = (cfg.width as usize, cfg.height as usize, cfg.bit_depth);
|
||||
let mut enc = Encoder::new(cfg).expect("config");
|
||||
let mut packets = Vec::new();
|
||||
let mut recons = Vec::new();
|
||||
let mut levels = Vec::new();
|
||||
for p in pics {
|
||||
packets.push(enc.encode(&p.frame()));
|
||||
recons.push(enc.last_reconstruction().expect("reconstruction"));
|
||||
levels.push(enc.last_loop_filter_level().expect("level"));
|
||||
}
|
||||
let dir = std::env::temp_dir().join(format!("effectcraft-av1enc-{}-{name}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).expect("temp dir");
|
||||
let input = dir.join("in.ivf");
|
||||
let output = dir.join("out.yuv");
|
||||
std::fs::write(&input, ivf(w, h, &packets)).expect("write ivf");
|
||||
let pix_fmt = if bd == 8 { "yuv420p" } else { "yuv420p10le" };
|
||||
let res = Command::new("ffmpeg")
|
||||
.args(["-v", "error", "-y", "-c:v", "libdav1d", "-i"])
|
||||
.arg(&input)
|
||||
.args(["-f", "rawvideo", "-pix_fmt", pix_fmt])
|
||||
.arg(&output)
|
||||
.output()
|
||||
.expect("run ffmpeg");
|
||||
let stderr = String::from_utf8_lossy(&res.stderr);
|
||||
assert!(res.status.success() && stderr.trim().is_empty(), "{name}: ffmpeg -c:v libdav1d failed: {stderr}");
|
||||
let raw = std::fs::read(&output).expect("decoded yuv");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let samples: Vec<u16> =
|
||||
if bd == 8 { raw.iter().map(|&b| b as u16).collect() } else { raw.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect() };
|
||||
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
|
||||
let fsize = w * h + 2 * cw * ch;
|
||||
assert_eq!(samples.len(), fsize * pics.len(), "{name}: decoded frame count");
|
||||
for (i, rec) in recons.iter().enumerate() {
|
||||
let f = &samples[i * fsize..(i + 1) * fsize];
|
||||
let planes = [&f[..w * h], &f[w * h..w * h + cw * ch], &f[w * h + cw * ch..]];
|
||||
for p in 0..3 {
|
||||
if planes[p] != &rec[p][..] {
|
||||
let pw = if p == 0 { w } else { cw };
|
||||
let k = planes[p].iter().zip(&rec[p]).position(|(a, b)| a != b).unwrap_or(0);
|
||||
panic!("{name}: frame {i} plane {p} differs from the reconstruction at ({}, {})", k % pw, k / pw);
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!(
|
||||
"{name}: libdav1d bit-exact over {} frames ({} bytes, loop filter levels {levels:?})",
|
||||
pics.len(),
|
||||
packets.iter().map(|p| p.data.len()).sum::<usize>()
|
||||
);
|
||||
levels
|
||||
}
|
||||
|
||||
fn cfg(w: usize, h: usize, bd: u8, rate: RateControl, keyint: u32) -> EncoderConfig {
|
||||
let mut c = EncoderConfig::new(w as u32, h as u32, 30, 1);
|
||||
c.bit_depth = bd;
|
||||
c.rate = rate;
|
||||
c.keyint = keyint;
|
||||
c
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn libdav1d_decodes_bit_exact() {
|
||||
if !have_libdav1d() {
|
||||
return;
|
||||
}
|
||||
// a moving pattern with inter frames
|
||||
let pics: Vec<Pic> = (0..8).map(|t| synth(176, 144, 8, t)).collect();
|
||||
check("inter8", cfg(176, 144, 8, RateControl::ConstantQ(90), 5), &pics);
|
||||
// all-intra at a fine quantizer (large coefficients, Golomb codes)
|
||||
let pics: Vec<Pic> = (0..2).map(|t| noise(64, 48, 8, t as u32 + 3)).collect();
|
||||
check("noise-intra", cfg(64, 48, 8, RateControl::ConstantQ(4), 1), &pics);
|
||||
// the finest quantizer: the largest coefficients, 8 and 10 bit, intra and inter
|
||||
let pics: Vec<Pic> = (0..3).map(|t| noise(72, 40, 8, t as u32 + 11)).collect();
|
||||
check("noise-q1", cfg(72, 40, 8, RateControl::ConstantQ(1), 2), &pics);
|
||||
let pics: Vec<Pic> = (0..3).map(|t| noise(40, 72, 10, t as u32 + 21)).collect();
|
||||
check("noise-q1-10", cfg(40, 72, 10, RateControl::ConstantQ(1), 2), &pics);
|
||||
// odd sizes, 8 and 10 bit
|
||||
let pics: Vec<Pic> = (0..4).map(|t| synth(33, 17, 8, t)).collect();
|
||||
check("odd8", cfg(33, 17, 8, RateControl::ConstantQ(60), 3), &pics);
|
||||
let pics: Vec<Pic> = (0..5).map(|t| synth(101, 75, 10, t)).collect();
|
||||
check("odd10", cfg(101, 75, 10, RateControl::ConstantQ(110), 4), &pics);
|
||||
// a noisy moving picture, bitrate mode
|
||||
let pics: Vec<Pic> = (0..6).map(|t| noisy(128, 80, t)).collect();
|
||||
let levels = check("bitrate", cfg(128, 80, 8, RateControl::Bitrate { kbps: 300 }, 10), &pics);
|
||||
assert!(levels.iter().any(|&l| l > 0), "the deblocking filter is exercised");
|
||||
// a coarse quantizer: strong deblocking, 10 bit
|
||||
let pics: Vec<Pic> = (0..4).map(|t| synth(120, 88, 10, t)).collect();
|
||||
let levels = check("deblock10", cfg(120, 88, 10, RateControl::ConstantQ(220), 2), &pics);
|
||||
assert!(levels.iter().any(|&l| l > 0), "the deblocking filter is exercised");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn libdav1d_decodes_1080p() {
|
||||
if !have_libdav1d() {
|
||||
return;
|
||||
}
|
||||
let pics: Vec<Pic> = (0..3).map(|t| noisy(1920, 1080, t)).collect();
|
||||
check("1080p", cfg(1920, 1080, 8, RateControl::ConstantQ(120), 30), &pics);
|
||||
}
|
||||
|
||||
fn noisy(w: usize, h: usize, t: usize) -> Pic {
|
||||
let mut p = synth(w, h, 8, t);
|
||||
let n = noise(w, h, 8, 7 + t as u32);
|
||||
for (a, b) in p.y.iter_mut().zip(&n.y) {
|
||||
*a = (*a as i32 + (*b as i32 - 128) / 16).clamp(0, 255) as u16;
|
||||
}
|
||||
p
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
//! Round trips through FilmCraft's spec-derived AV1 decoder: decoded frames must equal the
|
||||
//! encoder's own reconstruction bit for bit.
|
||||
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
use effectcraft_av1enc::{EncoderConfig, RateControl};
|
||||
|
||||
fn cfg(w: u32, h: u32, bd: u8, q: u8, keyint: u32) -> EncoderConfig {
|
||||
let mut c = EncoderConfig::new(w, h, 30, 1);
|
||||
c.bit_depth = bd;
|
||||
c.rate = RateControl::ConstantQ(q);
|
||||
c.keyint = keyint;
|
||||
c
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intra_8bit_smooth_high_quality() {
|
||||
let pics = vec![synth(128, 96, 8, 0)];
|
||||
let run = encode_and_check(cfg(128, 96, 8, 40, 1), &pics);
|
||||
eprintln!("intra 8-bit q40: {} bytes, PSNR-Y {:.2} dB", run.packets[0].data.len(), run.psnr_y[0]);
|
||||
assert!(run.packets[0].keyframe);
|
||||
assert!(run.psnr_y[0] > 35.0, "PSNR {:.2}", run.psnr_y[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_sizes_all_intra() {
|
||||
for (w, h) in [(33, 17), (1, 1), (7, 9), (65, 63), (100, 30)] {
|
||||
let pics: Vec<Pic> = (0..2).map(|t| synth(w, h, 8, t)).collect();
|
||||
let run = encode_and_check(cfg(w as u32, h as u32, 8, 90, 1), &pics);
|
||||
assert!(run.packets.iter().all(|p| p.keyframe));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_low_q_exact() {
|
||||
let pics = vec![noise(72, 40, 8, 1)];
|
||||
let run = encode_and_check(cfg(72, 40, 8, 1, 1), &pics);
|
||||
assert!(run.psnr_y[0] > 40.0, "PSNR {:.2}", run.psnr_y[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inter_moving_pattern() {
|
||||
let pics: Vec<Pic> = (0..6).map(|t| synth(160, 112, 8, t)).collect();
|
||||
let run = encode_and_check(cfg(160, 112, 8, 80, 30), &pics);
|
||||
let sizes: Vec<usize> = run.packets.iter().map(|p| p.data.len()).collect();
|
||||
eprintln!("inter sizes {sizes:?} psnr {:?}", run.psnr_y);
|
||||
assert!(run.packets[0].keyframe && run.packets[1..].iter().all(|p| !p.keyframe));
|
||||
let inter_avg = sizes[1..].iter().sum::<usize>() as f64 / (sizes.len() - 1) as f64;
|
||||
assert!(inter_avg < sizes[0] as f64 * 0.5, "inter frames {inter_avg} vs key {}", sizes[0]);
|
||||
assert!(run.psnr_y.iter().all(|&p| p > 33.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_bit_inter() {
|
||||
let pics: Vec<Pic> = (0..4).map(|t| synth(96, 72, 10, t)).collect();
|
||||
let run = encode_and_check(cfg(96, 72, 10, 60, 3), &pics);
|
||||
eprintln!("10-bit psnr {:?}", run.psnr_y);
|
||||
assert!(run.psnr_y.iter().all(|&p| p > 35.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_size_inter() {
|
||||
let pics: Vec<Pic> = (0..5).map(|t| synth(57, 43, 8, t)).collect();
|
||||
encode_and_check(cfg(57, 43, 8, 120, 10), &pics);
|
||||
let pics: Vec<Pic> = (0..3).map(|t| synth(33, 17, 10, t)).collect();
|
||||
encode_and_check(cfg(33, 17, 10, 120, 10), &pics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bitrate_mode_tracks_target() {
|
||||
let n = 12;
|
||||
let pics: Vec<Pic> = (0..n).map(|t| synth(192, 128, 8, t)).collect();
|
||||
let mut c = cfg(192, 128, 8, 0, 12);
|
||||
c.rate = RateControl::Bitrate { kbps: 400 };
|
||||
let run = encode_and_check(c, &pics);
|
||||
let bits: usize = run.packets.iter().map(|p| p.data.len() * 8).sum();
|
||||
let kbps = bits as f64 / (n as f64 / 30.0) / 1000.0;
|
||||
eprintln!("bitrate mode: {kbps:.1} kbit/s for a 400 kbit/s target, psnr {:?}", run.psnr_y);
|
||||
assert!(kbps > 400.0 / 2.0 && kbps < 400.0 * 2.0, "{kbps} kbit/s");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headers() {
|
||||
let enc = effectcraft_av1enc::Encoder::new(cfg(1920, 1080, 10, 100, 60)).unwrap();
|
||||
let c = enc.av1c();
|
||||
assert_eq!(c[0], 0x81);
|
||||
assert_eq!(c[1] & 0x1f, 8, "1080p30 is level 4.0");
|
||||
assert_eq!(c[2], 0b0100_1100);
|
||||
assert_eq!(&c[4..], &enc.sequence_header_obu()[..]);
|
||||
assert_eq!(enc.sequence_header_obu()[0], 0x0A);
|
||||
}
|
||||
|
|
@ -213,7 +213,7 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option<
|
|||
}
|
||||
let before = om.clone();
|
||||
if let Some(f) = str_p(p, "format") {
|
||||
let f = OutputFormat::from_name(f).ok_or_else(|| bad(cmd, "format: h264|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff"))?;
|
||||
let f = OutputFormat::from_name(f).ok_or_else(|| bad(cmd, "format: h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff"))?;
|
||||
om.set_format(f);
|
||||
}
|
||||
if let Some(c) = str_p(p, "channels") {
|
||||
|
|
@ -223,7 +223,7 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option<
|
|||
"alpha" | "alphaonly" => Channels::Alpha,
|
||||
_ => return Err(bad(cmd, "channels: rgb|rgba|alpha")),
|
||||
};
|
||||
if om.channels == Channels::Rgba && !om.format.supports_alpha() {
|
||||
if om.channels == Channels::Rgba && !om.supports_alpha() {
|
||||
return Err(bad(cmd, format!("{} has no alpha channel", om.format.label())));
|
||||
}
|
||||
}
|
||||
|
|
@ -234,6 +234,7 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option<
|
|||
_ => return Err(bad(cmd, "color: straight|premultiplied")),
|
||||
};
|
||||
}
|
||||
any |= apply_codec_options(om, p, cmd)?;
|
||||
if let Some(q) = f_p(p, "quality") {
|
||||
om.quality = q.clamp(1.0, 100.0) as u8;
|
||||
}
|
||||
|
|
@ -377,6 +378,62 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option<
|
|||
Ok(any)
|
||||
}
|
||||
|
||||
/// HEVC / AV1 codec options, the WebM video codec and the Opus audio options.
|
||||
fn apply_codec_options(om: &mut OutputModule, p: &Value, cmd: &str) -> Result<bool> {
|
||||
use effectcraft_project::render_queue::{CodecProfile, OpusApplication, RateControlMode, VideoCodecOptions, WebmVideoCodec};
|
||||
let mut any = false;
|
||||
if let Some(c) = str_p(p, "webmCodec") {
|
||||
om.webm_codec = match c.to_ascii_lowercase().as_str() {
|
||||
"vp9" => WebmVideoCodec::Vp9,
|
||||
"av1" => WebmVideoCodec::Av1,
|
||||
_ => return Err(bad(cmd, "webmCodec: vp9|av1")),
|
||||
};
|
||||
if !om.supports_alpha() {
|
||||
om.channels = Channels::Rgb;
|
||||
}
|
||||
any = true;
|
||||
}
|
||||
if let Some(v) = str_p(p, "profile") {
|
||||
om.codec.profile = CodecProfile::from_name(v).ok_or_else(|| bad(cmd, "profile: main|main10"))?;
|
||||
any = true;
|
||||
}
|
||||
match p.get("level") {
|
||||
Some(Value::Null) => {
|
||||
om.codec.level = None;
|
||||
any = true;
|
||||
}
|
||||
Some(v) => {
|
||||
let s = match v {
|
||||
Value::String(s) => s.clone(),
|
||||
other => other.to_string(),
|
||||
};
|
||||
om.codec.level = VideoCodecOptions::parse_level(&s).ok_or_else(|| bad(cmd, "level: auto|null or a level like 4.1"))?;
|
||||
any = true;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
if let Some(v) = str_p(p, "rateControl") {
|
||||
om.codec.rate_control = RateControlMode::from_name(v).ok_or_else(|| bad(cmd, "rateControl: bitrate|quality"))?;
|
||||
any = true;
|
||||
}
|
||||
if let Some(q) = f_p(p, "quality") {
|
||||
om.codec.quality = q.clamp(1.0, 100.0) as u8;
|
||||
}
|
||||
if let Some(b) = f_p(p, "audioBitrate") {
|
||||
om.opus_bitrate_kbps = b.clamp(6.0, 510.0) as u32;
|
||||
any = true;
|
||||
}
|
||||
if let Some(a) = str_p(p, "opusApplication") {
|
||||
om.opus_application = match a.to_ascii_lowercase().as_str() {
|
||||
"audio" | "music" => OpusApplication::Audio,
|
||||
"voip" | "voice" | "speech" => OpusApplication::Voip,
|
||||
_ => return Err(bad(cmd, "opusApplication: audio|voice")),
|
||||
};
|
||||
any = true;
|
||||
}
|
||||
Ok(any)
|
||||
}
|
||||
|
||||
/// `module` (1-based; 1 = the first output module).
|
||||
fn module_p(s: &Session, i: usize, p: &Value, cmd: &str) -> Result<usize> {
|
||||
let n = p.get("module").and_then(Value::as_u64).unwrap_or(1) as usize;
|
||||
|
|
@ -618,7 +675,17 @@ fn formats(s: &mut Session, _: &Value) -> Result<Value> {
|
|||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(json!({"formats": v, "proresProfiles": ProResProfile::ALL.iter().map(|p| p.label()).collect::<Vec<_>>()}))
|
||||
use effectcraft_project::render_queue::{CodecProfile, VideoCodecOptions};
|
||||
let level = |l: &u8| format!("{}.{}", l / 10, l % 10);
|
||||
Ok(json!({
|
||||
"formats": v,
|
||||
"proresProfiles": ProResProfile::ALL.iter().map(|p| p.label()).collect::<Vec<_>>(),
|
||||
"codecProfiles": CodecProfile::ALL.iter().map(|p| p.label()).collect::<Vec<_>>(),
|
||||
"hevcLevels": VideoCodecOptions::HEVC_LEVELS.iter().map(level).collect::<Vec<_>>(),
|
||||
"av1Levels": VideoCodecOptions::AV1_LEVELS.iter().map(level).collect::<Vec<_>>(),
|
||||
"webmCodecs": ["vp9", "av1"],
|
||||
"opusApplications": ["audio", "voice"],
|
||||
}))
|
||||
}
|
||||
|
||||
/// `item?|index?`, else the last item (the queue's newest entry).
|
||||
|
|
@ -902,7 +969,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Add to Render Queue",
|
||||
["Composition"],
|
||||
Some("Cmd+M"),
|
||||
"{comp?: id|name, template?: Render Settings template, format?: h264|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff, output?: path|template, log?: errorsOnly|plusSettings|plusPerFrameInfo, quality?: best|draft|1-100 (jpeg, webm), resolution?: full|half|third|quarter|scale, proxyUse?, effects?, solo?, guideLayers?, colorDepth?, frameBlending?, fieldRender?, pulldown?, motionBlur?, timeSpan?: workArea|comp|custom, start?: s, end?: s, duration?: s, frameRate?: fps|null, skipExisting?: bool, storageOverflow?: bool, channels?: rgb|rgba|alpha, color?, bitrate?: kbps, webmBitrate?, keyframeInterval?, proresProfile?, crop?, resize?, includeProjectLink?, audio?: auto|on|off, sampleRate?, audioChannels?, audioFormat?, loop?: bool (values as in setRenderSettings / setOutputModule)}",
|
||||
"{comp?: id|name, template?: Render Settings template, format?: h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff, output?: path|template, log?: errorsOnly|plusSettings|plusPerFrameInfo, quality?: best|draft|1-100 (jpeg, webm), resolution?: full|half|third|quarter|scale, proxyUse?, effects?, solo?, guideLayers?, colorDepth?, frameBlending?, fieldRender?, pulldown?, motionBlur?, timeSpan?: workArea|comp|custom, start?: s, end?: s, duration?: s, frameRate?: fps|null, skipExisting?: bool, storageOverflow?: bool, channels?: rgb|rgba|alpha, color?, bitrate?: kbps, webmBitrate?, keyframeInterval?, proresProfile?, profile?, level?, rateControl?, webmCodec?, audioBitrate?, opusApplication?, crop?, resize?, includeProjectLink?, audio?: auto|on|off, sampleRate?, audioChannels?, audioFormat?, loop?: bool (values as in setRenderSettings / setOutputModule)}",
|
||||
can_add,
|
||||
add
|
||||
),
|
||||
|
|
@ -922,7 +989,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Output Module Settings...",
|
||||
[],
|
||||
None,
|
||||
"{item?|index?, module?: n (1 = first), template?: name, format?, channels?: rgb|rgba|alpha, color?: straight|premultiplied, quality?: 1-100, bitrate?: kbps, webmBitrate?: bool, keyframeInterval?: frames, proresProfile?: proxy|lt|standard|hq|4444|4444xq, crop?: bool|{useRoi?, roi?, top?, left?, bottom?, right?}, cropTop?, cropLeft?, cropBottom?, cropRight?, resize?: bool|{preset?, width?, height?, lockAspect?, quality?: low|high}, resizeWidth?, resizeHeight?, postRenderAction?: none|import|importAndReplace|setProxy, includeProjectLink?: bool, audio?: auto|on|off, sampleRate?, audioChannels?: mono|stereo, audioFormat?: 16|24|32, loop?: bool, output?}",
|
||||
"{item?|index?, module?: n (1 = first), template?: name, format?: h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff, channels?: rgb|rgba|alpha, color?: straight|premultiplied, quality?: 1-100, bitrate?: kbps, webmBitrate?: bool, keyframeInterval?: frames (0 = auto), proresProfile?: proxy|lt|standard|hq|4444|4444xq, profile?: main|main10 (HEVC/AV1), level?: auto|4.1, rateControl?: bitrate|quality, webmCodec?: vp9|av1, audioBitrate?: Opus kbps, opusApplication?: audio|voice, crop?: bool|{useRoi?, roi?, top?, left?, bottom?, right?}, cropTop?, cropLeft?, cropBottom?, cropRight?, resize?: bool|{preset?, width?, height?, lockAspect?, quality?: low|high}, resizeWidth?, resizeHeight?, postRenderAction?: none|import|importAndReplace|setProxy, includeProjectLink?: bool, audio?: auto|on|off, sampleRate?, audioChannels?: mono|stereo, audioFormat?: 16|24|32, loop?: bool, output?}",
|
||||
has_items,
|
||||
set_output_module
|
||||
),
|
||||
|
|
|
|||
|
|
@ -790,6 +790,8 @@ mod tests_anim_tools;
|
|||
#[cfg(test)]
|
||||
mod tests_camera_track;
|
||||
#[cfg(test)]
|
||||
mod tests_codec_options;
|
||||
#[cfg(test)]
|
||||
mod tests_color_view;
|
||||
#[cfg(test)]
|
||||
mod tests_disk_cache;
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ pub fn bytes_per_frame(om: &OutputModule, w: u32, h: u32, fps: f64) -> f64 {
|
|||
let px = (w as f64 * h as f64) / (1920.0 * 1080.0);
|
||||
let mbps = |m: f64| m * 1_000_000.0 / 8.0;
|
||||
match om.format {
|
||||
OutputFormat::H264 => om.bitrate_kbps as f64 * 1000.0 / 8.0 / fps,
|
||||
OutputFormat::H264 | OutputFormat::Hevc | OutputFormat::Av1 => om.bitrate_kbps as f64 * 1000.0 / 8.0 / fps,
|
||||
OutputFormat::ProRes => {
|
||||
let rate = match om.prores_profile {
|
||||
Proxy => 45.0,
|
||||
|
|
|
|||
|
|
@ -159,6 +159,6 @@ fn without_exporter() {
|
|||
s.execute("renderQueue.add", json!({})).unwrap();
|
||||
assert!(!s.is_enabled("renderQueue.render"));
|
||||
let f = s.execute("renderQueue.formats", json!({})).unwrap();
|
||||
assert_eq!(f["formats"].as_array().unwrap().len(), 10);
|
||||
assert_eq!(f["formats"].as_array().unwrap().len(), 12);
|
||||
assert_eq!(f["formats"][0]["available"], false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
//! M13.4: HEVC / AV1 output modules and the WebM codec / Opus options over the command API.
|
||||
|
||||
use effectcraft_project::render_queue::{Channels, CodecProfile, OpusApplication, OutputFormat, RateControlMode, VideoCodecOptions, WebmVideoCodec};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session::default();
|
||||
s.execute("file.openDemoProject", json!({})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hevc_and_av1_output_module_options() {
|
||||
let mut s = session();
|
||||
let a = s
|
||||
.execute(
|
||||
"renderQueue.add",
|
||||
json!({"format": "hevc", "profile": "main10", "level": "4.1", "rateControl": "quality", "quality": 85, "keyframeInterval": 30}),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(a["outputPath"].as_str().unwrap().ends_with(".mp4"), "{a}");
|
||||
let om = &s.project.render_queue[0].output;
|
||||
assert_eq!(om.format, OutputFormat::Hevc);
|
||||
assert_eq!(om.codec, VideoCodecOptions { profile: CodecProfile::Main10, level: Some(41), rate_control: RateControlMode::Quality, quality: 85 });
|
||||
assert_eq!(om.keyframe_interval, 30);
|
||||
assert_eq!(a["output"]["codec"]["profile"], "Main10", "{a}");
|
||||
let id = a["item"].clone();
|
||||
s.execute("renderQueue.setOutputModule", json!({"item": id, "format": "av1", "level": null, "rateControl": "bitrate", "bitrate": 3000})).unwrap();
|
||||
let om = &s.project.render_queue[0].output;
|
||||
assert_eq!((om.format, om.codec.level, om.codec.rate_control, om.bitrate_kbps), (OutputFormat::Av1, None, RateControlMode::Bitrate, 3000));
|
||||
assert!(s.execute("renderQueue.setOutputModule", json!({"item": id, "channels": "rgba"})).is_err(), "AV1 MP4 has no alpha");
|
||||
assert!(s.execute("renderQueue.setOutputModule", json!({"item": id, "profile": "high"})).is_err());
|
||||
assert!(s.execute("renderQueue.setOutputModule", json!({"item": id, "level": "0.5"})).is_err());
|
||||
assert!(s.execute("renderQueue.setOutputModule", json!({"item": id, "rateControl": "cbr2"})).is_err());
|
||||
|
||||
// WebM: VP9 with alpha, then AV1 (alpha dropped) with voice-tuned Opus.
|
||||
s.execute("renderQueue.setOutputModule", json!({"item": id, "format": "webm", "channels": "rgba"})).unwrap();
|
||||
assert_eq!(s.project.render_queue[0].output.channels, Channels::Rgba);
|
||||
s.execute("renderQueue.setOutputModule", json!({"item": id, "webmCodec": "av1", "audioBitrate": 24, "opusApplication": "voice"})).unwrap();
|
||||
let om = &s.project.render_queue[0].output;
|
||||
assert_eq!((om.webm_codec, om.channels, om.opus_bitrate_kbps, om.opus_application), (WebmVideoCodec::Av1, Channels::Rgb, 24, OpusApplication::Voip));
|
||||
assert!(s.execute("renderQueue.setOutputModule", json!({"item": id, "webmCodec": "theora"})).is_err());
|
||||
|
||||
// The formats query lists the new formats and option values.
|
||||
let f = s.execute("renderQueue.formats", json!({})).unwrap();
|
||||
let ids: Vec<&str> = f["formats"].as_array().unwrap().iter().filter_map(|v| v["id"].as_str()).collect();
|
||||
assert!(ids.contains(&"Hevc") && ids.contains(&"Av1"), "{ids:?}");
|
||||
assert_eq!(f["codecProfiles"], json!(["Main", "Main 10"]));
|
||||
assert!(f["hevcLevels"].as_array().unwrap().contains(&json!("4.1")));
|
||||
assert_eq!(f["webmCodecs"], json!(["vp9", "av1"]));
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "effectcraft-export"
|
||||
description = "Render Queue jobs and output modules: H.264 MP4 (+AAC) and ProRes MOV (+PCM) via FilmCraft's pure-Rust encoders, WebM (VP9 + Opus), PNG/JPEG/TIFF/EXR sequences, animated GIF, WAV/AIFF"
|
||||
description = "Render Queue jobs and output modules: H.264 MP4 (+AAC) and ProRes MOV (+PCM) via FilmCraft, HEVC / AV1 MP4 (+AAC) and WebM (VP9 or AV1 + Opus) via EffectCraft encoders, PNG/JPEG/TIFF/EXR sequences, animated GIF, WAV/AIFF"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
|
@ -20,6 +20,8 @@ filmcraft-aac = { workspace = true }
|
|||
filmcraft-isobmff = { workspace = true }
|
||||
effectcraft-opusenc = { workspace = true }
|
||||
effectcraft-vp9enc = { workspace = true }
|
||||
effectcraft-hevcenc = { workspace = true }
|
||||
effectcraft-av1enc = { workspace = true }
|
||||
image = { workspace = true, features = ["png", "jpeg", "tiff", "exr"] }
|
||||
gif = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
//! Movie export: FilmCraft H.264 → MP4 (+ AAC) and ProRes → MOV (+ PCM), muxed by
|
||||
//! Movie export: FilmCraft H.264, HEVC (`effectcraft-hevcenc`) and AV1 (`effectcraft-av1enc`)
|
||||
//! → MP4 (+ AAC) and ProRes → MOV (+ PCM), muxed by
|
||||
//! FilmCraft's ISO BMFF / QuickTime writer.
|
||||
|
||||
use std::io::Write;
|
||||
|
|
@ -18,14 +19,14 @@ fn mux_err(e: impl std::fmt::Display) -> ExportError {
|
|||
ExportError::Io(e.to_string())
|
||||
}
|
||||
|
||||
struct Packet {
|
||||
data: Vec<u8>,
|
||||
key: bool,
|
||||
pub(crate) struct Packet {
|
||||
pub(crate) data: Vec<u8>,
|
||||
pub(crate) key: bool,
|
||||
/// pts − dts in the track timescale.
|
||||
cto: i32,
|
||||
pub(crate) cto: i32,
|
||||
}
|
||||
|
||||
trait VideoEncoder {
|
||||
pub(crate) trait VideoEncoder {
|
||||
fn sample_entry(&self) -> SampleEntry;
|
||||
fn encode(&mut self, rgba: &[u8], index: u64) -> Result<Vec<Packet>>;
|
||||
fn flush(&mut self) -> Result<Vec<Packet>>;
|
||||
|
|
@ -257,9 +258,11 @@ pub(crate) fn movie(job: &Cx, comp: &Comp, w: u32, h: u32, st: &mut State) -> Re
|
|||
};
|
||||
let mut venc: Box<dyn VideoEncoder> = match fmt {
|
||||
OutputFormat::H264 => Box::new(H264::new(w, h, rate, job.output.bitrate_kbps)?),
|
||||
OutputFormat::Hevc => Box::new(crate::hevc_av1::Hevc::new(w, h, rate, job.output)?),
|
||||
OutputFormat::Av1 => Box::new(crate::hevc_av1::Av1::new(w, h, rate, job.output)?),
|
||||
_ => Box::new(ProRes::new(w, h, job.output.prores_profile, if channels == Channels::Alpha { Channels::Rgb } else { channels })),
|
||||
};
|
||||
let brand = if fmt == OutputFormat::H264 { Brand::Mp4 } else { Brand::Mov };
|
||||
let brand = if fmt == OutputFormat::ProRes { Brand::Mov } else { Brand::Mp4 };
|
||||
let batch = batch_size();
|
||||
let render = |k: u64| -> Vec<u8> { job.pixels(&job.frame(comp, k), comp, channels, w, h) };
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,203 @@
|
|||
//! HEVC (`effectcraft-hevcenc`) and AV1 (`effectcraft-av1enc`) video encoders for the MP4
|
||||
//! movie writer (`hvc1` / `av01` sample entries) and AV1 in WebM, configured from the Output
|
||||
//! Module's [`VideoCodecOptions`].
|
||||
|
||||
use effectcraft_project::render_queue::{CodecProfile, OutputModule, RateControlMode, VideoCodecOptions};
|
||||
use effectcraft_time::FrameRate;
|
||||
use filmcraft_isobmff::{Av1Config, CodecConfig, FourCc, HevcConfig, SampleEntry};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::encode::{Packet, VideoEncoder};
|
||||
use crate::{ExportError, Result};
|
||||
|
||||
fn enc_err(e: impl std::fmt::Display) -> ExportError {
|
||||
ExportError::Encode(e.to_string())
|
||||
}
|
||||
|
||||
/// BT.709 limited-range 4:2:0 planes at `bits` (8 or 10) from RGBA8 (2×2 chroma average;
|
||||
/// alpha ignored). Chroma planes are `ceil(w/2)` × `ceil(h/2)`.
|
||||
pub(crate) fn rgba_to_yuv420_bits(rgba: &[u8], w: usize, h: usize, bits: u8, y: &mut Vec<u16>, u: &mut Vec<u16>, v: &mut Vec<u16>) {
|
||||
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
|
||||
let scale = (1u32 << (bits - 8)) as f32;
|
||||
let (lo, hi) = (scale, 254.0 * scale + (scale - 1.0));
|
||||
y.resize(w * h, 0);
|
||||
u.resize(cw * ch, 0);
|
||||
v.resize(cw * ch, 0);
|
||||
y.par_chunks_mut(w * 2).zip(u.par_chunks_mut(cw).zip(v.par_chunks_mut(cw))).enumerate().for_each(|(cy, (yr, (ur, vr)))| {
|
||||
let rows = yr.len() / w;
|
||||
let mut acc = vec![(0f32, 0f32, 0f32); cw];
|
||||
for dy in 0..rows {
|
||||
let src = &rgba[(cy * 2 + dy) * w * 4..][..w * 4];
|
||||
for x in 0..w {
|
||||
let (r, g, b) = (src[x * 4] as f32 / 255.0, src[x * 4 + 1] as f32 / 255.0, src[x * 4 + 2] as f32 / 255.0);
|
||||
let l = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
yr[dy * w + x] = ((16.0 + 219.0 * l) * scale).round().clamp(lo, hi) as u16;
|
||||
let a = &mut acc[x / 2];
|
||||
a.0 += (b - l) / 1.8556;
|
||||
a.1 += (r - l) / 1.5748;
|
||||
a.2 += 1.0;
|
||||
}
|
||||
}
|
||||
for (cx, a) in acc.iter().enumerate() {
|
||||
ur[cx] = ((128.0 + 224.0 * a.0 / a.2) * scale).round().clamp(lo, hi) as u16;
|
||||
vr[cx] = ((128.0 + 224.0 * a.1 / a.2) * scale).round().clamp(lo, hi) as u16;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Planes {
|
||||
y: Vec<u16>,
|
||||
u: Vec<u16>,
|
||||
v: Vec<u16>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- HEVC
|
||||
|
||||
pub(crate) struct Hevc {
|
||||
enc: effectcraft_hevcenc::Encoder,
|
||||
w: u32,
|
||||
h: u32,
|
||||
bits: u8,
|
||||
planes: Planes,
|
||||
}
|
||||
|
||||
/// The HEVC encoder settings for an output module.
|
||||
pub(crate) fn hevc_config(w: u32, h: u32, rate: FrameRate, om: &OutputModule) -> effectcraft_hevcenc::EncoderConfig {
|
||||
use effectcraft_hevcenc as he;
|
||||
let o: &VideoCodecOptions = &om.codec;
|
||||
let mut cfg = he::EncoderConfig::new(w, h, rate.num as u32, rate.den as u32);
|
||||
cfg.profile = match o.profile {
|
||||
CodecProfile::Main => he::Profile::Main,
|
||||
CodecProfile::Main10 => he::Profile::Main10,
|
||||
};
|
||||
cfg.level_idc = o.hevc_level_idc();
|
||||
cfg.rate = match o.rate_control {
|
||||
RateControlMode::Bitrate => he::RateControl::Bitrate { kbps: om.bitrate_kbps.max(50) },
|
||||
RateControlMode::Quality => he::RateControl::ConstantQp(o.hevc_qp()),
|
||||
};
|
||||
cfg.keyint = om.keyint(rate.as_f64());
|
||||
cfg
|
||||
}
|
||||
|
||||
impl Hevc {
|
||||
pub(crate) fn new(w: u32, h: u32, rate: FrameRate, om: &OutputModule) -> Result<Hevc> {
|
||||
let cfg = hevc_config(w, h, rate, om);
|
||||
let bits = cfg.profile.bit_depth();
|
||||
let enc = effectcraft_hevcenc::Encoder::new(cfg).map_err(enc_err)?;
|
||||
Ok(Hevc { enc, w, h, bits, planes: Planes::default() })
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoEncoder for Hevc {
|
||||
fn sample_entry(&self) -> SampleEntry {
|
||||
let cfg = HevcConfig::parse(&self.enc.hvcc()).unwrap_or_default();
|
||||
SampleEntry::hevc(cfg, self.w as u16, self.h as u16)
|
||||
}
|
||||
fn encode(&mut self, rgba: &[u8], _index: u64) -> Result<Vec<Packet>> {
|
||||
let (w, h) = (self.w as usize, self.h as usize);
|
||||
let p = &mut self.planes;
|
||||
rgba_to_yuv420_bits(rgba, w, h, self.bits, &mut p.y, &mut p.u, &mut p.v);
|
||||
let frame = effectcraft_hevcenc::Frame { y: &p.y, u: &p.u, v: &p.v, y_stride: w, uv_stride: w.div_ceil(2) };
|
||||
let pk = self.enc.encode(&frame);
|
||||
Ok(vec![Packet { data: pk.data, key: pk.keyframe, cto: 0 }])
|
||||
}
|
||||
fn flush(&mut self) -> Result<Vec<Packet>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- AV1
|
||||
|
||||
pub(crate) struct Av1 {
|
||||
enc: effectcraft_av1enc::Encoder,
|
||||
w: u32,
|
||||
h: u32,
|
||||
bits: u8,
|
||||
planes: Planes,
|
||||
}
|
||||
|
||||
/// The AV1 encoder settings for an output module.
|
||||
pub(crate) fn av1_config(w: u32, h: u32, rate: FrameRate, om: &OutputModule) -> effectcraft_av1enc::EncoderConfig {
|
||||
use effectcraft_av1enc as ae;
|
||||
let o: &VideoCodecOptions = &om.codec;
|
||||
let mut cfg = ae::EncoderConfig::new(w, h, rate.num as u32, rate.den as u32);
|
||||
cfg.bit_depth = o.profile.bit_depth();
|
||||
cfg.level_idx = o.av1_level_idx();
|
||||
cfg.rate = match o.rate_control {
|
||||
RateControlMode::Bitrate => ae::RateControl::Bitrate { kbps: om.bitrate_kbps.max(50) },
|
||||
RateControlMode::Quality => ae::RateControl::ConstantQ(o.av1_qindex()),
|
||||
};
|
||||
cfg.keyint = om.keyint(rate.as_f64());
|
||||
cfg
|
||||
}
|
||||
|
||||
impl Av1 {
|
||||
pub(crate) fn new(w: u32, h: u32, rate: FrameRate, om: &OutputModule) -> Result<Av1> {
|
||||
let cfg = av1_config(w, h, rate, om);
|
||||
let bits = cfg.bit_depth;
|
||||
let enc = effectcraft_av1enc::Encoder::new(cfg).map_err(enc_err)?;
|
||||
Ok(Av1 { enc, w, h, bits, planes: Planes::default() })
|
||||
}
|
||||
/// The `av1C` record (also the WebM CodecPrivate).
|
||||
pub(crate) fn av1c(&self) -> Vec<u8> {
|
||||
self.enc.av1c()
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoEncoder for Av1 {
|
||||
fn sample_entry(&self) -> SampleEntry {
|
||||
let cfg = Av1Config::parse(&self.enc.av1c()).unwrap_or_default();
|
||||
SampleEntry::video(FourCc(*b"av01"), CodecConfig::Av1(cfg), self.w as u16, self.h as u16)
|
||||
}
|
||||
fn encode(&mut self, rgba: &[u8], _index: u64) -> Result<Vec<Packet>> {
|
||||
let (w, h) = (self.w as usize, self.h as usize);
|
||||
let p = &mut self.planes;
|
||||
rgba_to_yuv420_bits(rgba, w, h, self.bits, &mut p.y, &mut p.u, &mut p.v);
|
||||
let frame = effectcraft_av1enc::Frame { y: &p.y, u: &p.u, v: &p.v, y_stride: w, uv_stride: w.div_ceil(2) };
|
||||
let pk = self.enc.encode(&frame);
|
||||
Ok(vec![Packet { data: pk.data, key: pk.keyframe, cto: 0 }])
|
||||
}
|
||||
fn flush(&mut self) -> Result<Vec<Packet>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ten_bit_planes_scale_eight_bit_ones() {
|
||||
let rgba = [255u8, 255, 255, 255, 0, 0, 0, 255, 255, 0, 0, 255, 0, 255, 0, 255];
|
||||
let (mut y, mut u, mut v) = (vec![], vec![], vec![]);
|
||||
rgba_to_yuv420_bits(&rgba, 2, 2, 8, &mut y, &mut u, &mut v);
|
||||
assert_eq!((y[0], y[1]), (235, 16));
|
||||
rgba_to_yuv420_bits(&rgba, 2, 2, 10, &mut y, &mut u, &mut v);
|
||||
assert_eq!((y[0], y[1]), (940, 64));
|
||||
assert_eq!(u.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_options_map_to_encoder_settings() {
|
||||
let mut om = OutputModule::for_format(effectcraft_project::render_queue::OutputFormat::Hevc);
|
||||
om.codec.profile = CodecProfile::Main10;
|
||||
om.codec.level = Some(41);
|
||||
om.codec.rate_control = RateControlMode::Quality;
|
||||
om.codec.quality = 100;
|
||||
om.keyframe_interval = 12;
|
||||
let rate = FrameRate::new(30, 1);
|
||||
let h = hevc_config(64, 32, rate, &om);
|
||||
assert_eq!(h.profile, effectcraft_hevcenc::Profile::Main10);
|
||||
assert_eq!(h.level_idc, Some(123));
|
||||
assert_eq!(h.rate, effectcraft_hevcenc::RateControl::ConstantQp(4));
|
||||
assert_eq!(h.keyint, 12);
|
||||
let a = av1_config(64, 32, rate, &om);
|
||||
assert_eq!((a.bit_depth, a.level_idx, a.keyint), (10, Some(9), 12));
|
||||
om.codec.rate_control = RateControlMode::Bitrate;
|
||||
om.keyframe_interval = 0;
|
||||
om.bitrate_kbps = 3000;
|
||||
let a = av1_config(64, 32, rate, &om);
|
||||
assert_eq!((a.rate, a.keyint), (effectcraft_av1enc::RateControl::Bitrate { kbps: 3000 }, 60));
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,9 @@
|
|||
//! | PNG / JPEG / TIFF sequence | one file per frame | 8-bit | PNG, TIFF | — |
|
||||
//! | OpenEXR sequence | one file per frame | 32-bit float, linear light, premultiplied | yes | — |
|
||||
//! | Animated GIF | GIF89a | 256-colour palette per frame (NeuQuant) | 1-bit | — |
|
||||
//! | WebM | WebM (Matroska) | VP9 profile 0 (`effectcraft-vp9enc`: key + inter frames, a key frame every 2 s, loop filter), 8-bit 4:2:0 | VP9 alpha (BlockAdditional) | Opus (`effectcraft-opusenc`), 48 kHz stereo |
|
||||
//! | WebM | WebM (Matroska) | VP9 profile 0 (`effectcraft-vp9enc`: key + inter frames, a key frame every 2 s, loop filter), 8-bit 4:2:0; or AV1 (`effectcraft-av1enc`) | VP9 alpha (BlockAdditional) | Opus (`effectcraft-opusenc`: SILK / hybrid / CELT by bitrate and application), 48 kHz |
|
||||
//! | HEVC | MP4 (`hvc1`) | `effectcraft-hevcenc` Main / Main 10 4:2:0, I + P slices, bitrate or constant quality, level, key-frame interval | no | AAC-LC (FilmCraft) |
|
||||
//! | AV1 | MP4 (`av01`) | `effectcraft-av1enc` Main 8/10-bit 4:2:0, key + inter frames | no | AAC-LC (FilmCraft) |
|
||||
//! | WAV / AIFF | RIFF WAVE / AIFF | — | — | PCM 16/24-bit (WAV also 32-bit float), mono/stereo (audio only) |
|
||||
//!
|
||||
//! Every frame goes through [`pipeline`]: the Render Settings overrides (Effects, Solo Switches,
|
||||
|
|
@ -27,9 +29,11 @@
|
|||
//! output is a single pixel wide/high).
|
||||
|
||||
mod encode;
|
||||
mod hevc_av1;
|
||||
mod out;
|
||||
mod pipeline;
|
||||
mod webm;
|
||||
mod webm_av1;
|
||||
|
||||
use web_time::Instant;
|
||||
|
||||
|
|
@ -173,7 +177,7 @@ fn export_with(cx: &Cx, progress: &mut dyn FnMut(&Progress) -> bool, t0: Instant
|
|||
st.advance(0)?;
|
||||
let (w, h) = output_size(comp, cx.settings, cx.output);
|
||||
let mut report = match cx.output.format {
|
||||
OutputFormat::H264 | OutputFormat::ProRes => encode::movie(cx, comp, w, h, &mut st)?,
|
||||
OutputFormat::H264 | OutputFormat::Hevc | OutputFormat::Av1 | OutputFormat::ProRes => encode::movie(cx, comp, w, h, &mut st)?,
|
||||
OutputFormat::Gif => gif_export(cx, comp, w, h, &mut st)?,
|
||||
OutputFormat::WebM => webm::webm(cx, comp, w, h, &mut st)?,
|
||||
OutputFormat::Wav => webm::audio_file(cx, comp, false, &mut st)?,
|
||||
|
|
|
|||
|
|
@ -22,25 +22,24 @@ use rayon::prelude::*;
|
|||
use crate::encode::{mix, pcm_bytes};
|
||||
use crate::{Cx, Report, Result, State, batch_size, io, wants_audio};
|
||||
|
||||
const OPUS_RATE: u32 = 48_000;
|
||||
const OPUS_BITRATE: u32 = 192_000;
|
||||
pub(crate) const OPUS_RATE: u32 = 48_000;
|
||||
|
||||
// Element ids (with their length markers).
|
||||
const EBML: u32 = 0x1A45_DFA3;
|
||||
const SEGMENT: u32 = 0x1853_8067;
|
||||
const INFO: u32 = 0x1549_A966;
|
||||
const TRACKS: u32 = 0x1654_AE6B;
|
||||
const TRACK_ENTRY: u32 = 0xAE;
|
||||
const CLUSTER: u32 = 0x1F43_B675;
|
||||
pub(crate) const EBML: u32 = 0x1A45_DFA3;
|
||||
pub(crate) const SEGMENT: u32 = 0x1853_8067;
|
||||
pub(crate) const INFO: u32 = 0x1549_A966;
|
||||
pub(crate) const TRACKS: u32 = 0x1654_AE6B;
|
||||
pub(crate) const TRACK_ENTRY: u32 = 0xAE;
|
||||
pub(crate) const CLUSTER: u32 = 0x1F43_B675;
|
||||
|
||||
fn id_bytes(id: u32) -> Vec<u8> {
|
||||
pub(crate) fn id_bytes(id: u32) -> Vec<u8> {
|
||||
let b = id.to_be_bytes();
|
||||
let skip = b.iter().position(|x| *x != 0).unwrap_or(3);
|
||||
b[skip..].to_vec()
|
||||
}
|
||||
|
||||
/// EBML variable-size integer for an element data size (shortest form).
|
||||
fn vint(n: u64) -> Vec<u8> {
|
||||
pub(crate) fn vint(n: u64) -> Vec<u8> {
|
||||
for len in 1..=8u32 {
|
||||
let max = (1u64 << (7 * len)) - 2;
|
||||
if n <= max {
|
||||
|
|
@ -53,23 +52,23 @@ fn vint(n: u64) -> Vec<u8> {
|
|||
v
|
||||
}
|
||||
|
||||
fn el(out: &mut Vec<u8>, id: u32, data: &[u8]) {
|
||||
pub(crate) fn el(out: &mut Vec<u8>, id: u32, data: &[u8]) {
|
||||
out.extend(id_bytes(id));
|
||||
out.extend(vint(data.len() as u64));
|
||||
out.extend_from_slice(data);
|
||||
}
|
||||
|
||||
fn el_uint(out: &mut Vec<u8>, id: u32, v: u64) {
|
||||
pub(crate) fn el_uint(out: &mut Vec<u8>, id: u32, v: u64) {
|
||||
let b = v.to_be_bytes();
|
||||
let skip = b.iter().position(|x| *x != 0).unwrap_or(7);
|
||||
el(out, id, &b[skip..]);
|
||||
}
|
||||
|
||||
fn el_float(out: &mut Vec<u8>, id: u32, v: f64) {
|
||||
pub(crate) fn el_float(out: &mut Vec<u8>, id: u32, v: f64) {
|
||||
el(out, id, &v.to_be_bytes());
|
||||
}
|
||||
|
||||
fn el_str(out: &mut Vec<u8>, id: u32, s: &str) {
|
||||
pub(crate) fn el_str(out: &mut Vec<u8>, id: u32, s: &str) {
|
||||
el(out, id, s.as_bytes());
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +124,20 @@ fn cluster(frames: &mut [Frame]) -> Vec<u8> {
|
|||
out
|
||||
}
|
||||
|
||||
/// The Opus encoder of a WebM output: the module's channel count, bitrate and application.
|
||||
pub(crate) fn opus_encoder(job: &Cx) -> effectcraft_opusenc::OpusEncoder {
|
||||
let channels = if job.output.audio_channels == 1 { 1 } else { 2 };
|
||||
let app = match job.output.opus_application {
|
||||
effectcraft_project::render_queue::OpusApplication::Audio => effectcraft_opusenc::Application::Audio,
|
||||
effectcraft_project::render_queue::OpusApplication::Voip => effectcraft_opusenc::Application::Voip,
|
||||
};
|
||||
effectcraft_opusenc::OpusEncoder::with_application(channels, job.output.opus_bitrate_kbps.clamp(6, 510) * 1000, app)
|
||||
}
|
||||
|
||||
pub(crate) fn webm(job: &Cx, comp: &Comp, w: u32, h: u32, st: &mut State) -> Result<Report> {
|
||||
if job.output.webm_codec == effectcraft_project::render_queue::WebmVideoCodec::Av1 {
|
||||
return crate::webm_av1::webm_av1(job, comp, w, h, st);
|
||||
}
|
||||
let rate = job.settings.rate(comp);
|
||||
let alpha = job.output.channels == Channels::Rgba;
|
||||
let colour_channels = if job.output.channels == Channels::Alpha { Channels::Alpha } else { Channels::Rgb };
|
||||
|
|
@ -145,7 +157,7 @@ pub(crate) fn webm(job: &Cx, comp: &Comp, w: u32, h: u32, st: &mut State) -> Res
|
|||
let mut alpha_enc = alpha.then(|| effectcraft_vp9enc::Vp9Encoder::new(cfg.clone()));
|
||||
let with_audio = wants_audio(job);
|
||||
let opus_channels = if job.output.audio_channels == 1 { 1usize } else { 2 };
|
||||
let mut opus = with_audio.then(|| effectcraft_opusenc::OpusEncoder::new(opus_channels as _, OPUS_BITRATE));
|
||||
let mut opus = with_audio.then(|| opus_encoder(job));
|
||||
let total = st.total;
|
||||
let frame_ms = |k: u64| -> i64 { ((k as i128 * 1000 * rate.den as i128 + rate.num as i128 / 2) / rate.num as i128) as i64 };
|
||||
let duration_ms = frame_ms(total) as f64;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
//! WebM with AV1 video (`V_AV1`, CodecPrivate = the `av1C` record, per the AV1-in-Matroska
|
||||
//! codec mapping) and Opus audio. Frames are rendered in parallel batches and encoded in order
|
||||
//! (AV1 inter frames reference the previous frame); each batch becomes one Cluster whose
|
||||
//! SimpleBlocks carry the key-frame flag only on AV1 key frames (the Cues-less layout of the
|
||||
//! VP9 writer: players seek by scanning clusters).
|
||||
|
||||
use std::io::{Seek, SeekFrom, Write};
|
||||
|
||||
use effectcraft_project::Comp;
|
||||
use effectcraft_project::render_queue::Channels;
|
||||
use effectcraft_time::{TICKS_PER_SECOND, Tick};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::encode::VideoEncoder;
|
||||
use crate::encode::mix;
|
||||
use crate::webm::{CLUSTER, EBML, INFO, OPUS_RATE, SEGMENT, TRACK_ENTRY, TRACKS, el, el_float, el_str, el_uint, id_bytes, opus_encoder, vint};
|
||||
use crate::{Cx, Report, Result, State, batch_size, io, wants_audio};
|
||||
|
||||
struct Block {
|
||||
ms: i64,
|
||||
track: u64,
|
||||
key: bool,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
fn cluster(blocks: &mut [Block]) -> Vec<u8> {
|
||||
blocks.sort_by_key(|b| (b.ms, b.track));
|
||||
let base = blocks.first().map_or(0, |b| b.ms);
|
||||
let mut body = vec![];
|
||||
el_uint(&mut body, 0xE7, base.max(0) as u64);
|
||||
for b in blocks.iter() {
|
||||
let rel = (b.ms - base).clamp(i16::MIN as i64, i16::MAX as i64) as i16;
|
||||
let mut p = vint(b.track);
|
||||
p.extend_from_slice(&rel.to_be_bytes());
|
||||
p.push(if b.key { 0x80 } else { 0x00 });
|
||||
p.extend_from_slice(&b.data);
|
||||
el(&mut body, 0xA3, &p);
|
||||
}
|
||||
let mut out = vec![];
|
||||
el(&mut out, CLUSTER, &body);
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn webm_av1(job: &Cx, comp: &Comp, w: u32, h: u32, st: &mut State) -> Result<Report> {
|
||||
let rate = job.settings.rate(comp);
|
||||
let mut venc = crate::hevc_av1::Av1::new(w, h, rate, job.output)?;
|
||||
let with_audio = wants_audio(job);
|
||||
let opus_channels = if job.output.audio_channels == 1 { 1usize } else { 2 };
|
||||
let mut opus = with_audio.then(|| opus_encoder(job));
|
||||
let total = st.total;
|
||||
let frame_ms = |k: u64| -> i64 { ((k as i128 * 1000 * rate.den as i128 + rate.num as i128 / 2) / rate.num as i128) as i64 };
|
||||
let duration_ms = frame_ms(total) as f64;
|
||||
|
||||
let path = job.place(job.path, 1);
|
||||
let mut file = crate::out::create(job.sink, &path)?;
|
||||
let mut head = vec![];
|
||||
let mut eb = vec![];
|
||||
el_uint(&mut eb, 0x4286, 1);
|
||||
el_uint(&mut eb, 0x42F7, 1);
|
||||
el_uint(&mut eb, 0x42F2, 4);
|
||||
el_uint(&mut eb, 0x42F3, 8);
|
||||
el_str(&mut eb, 0x4282, "webm");
|
||||
el_uint(&mut eb, 0x4287, 4);
|
||||
el_uint(&mut eb, 0x4285, 2);
|
||||
el(&mut head, EBML, &eb);
|
||||
head.extend(id_bytes(SEGMENT));
|
||||
let size_pos = head.len() as u64;
|
||||
head.extend_from_slice(&[0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
|
||||
let seg_start = head.len() as u64;
|
||||
let mut info = vec![];
|
||||
el_uint(&mut info, 0x2AD7B1, 1_000_000);
|
||||
el_float(&mut info, 0x4489, duration_ms);
|
||||
el_str(&mut info, 0x4D80, "EffectCraft");
|
||||
el_str(&mut info, 0x5741, "EffectCraft");
|
||||
el(&mut head, INFO, &info);
|
||||
let mut tracks = vec![];
|
||||
let mut v = vec![];
|
||||
el_uint(&mut v, 0xD7, 1);
|
||||
el_uint(&mut v, 0x73C5, 1);
|
||||
el_uint(&mut v, 0x83, 1);
|
||||
el_uint(&mut v, 0x9C, 0);
|
||||
el_str(&mut v, 0x86, "V_AV1");
|
||||
el(&mut v, 0x63A2, &venc.av1c());
|
||||
el_uint(&mut v, 0x23E383, (1_000_000_000u128 * rate.den as u128 / rate.num.max(1) as u128) as u64);
|
||||
let mut video = vec![];
|
||||
el_uint(&mut video, 0xB0, w as u64);
|
||||
el_uint(&mut video, 0xBA, h as u64);
|
||||
el(&mut v, 0xE0, &video);
|
||||
el(&mut tracks, TRACK_ENTRY, &v);
|
||||
if let Some(o) = &opus {
|
||||
let mut a = vec![];
|
||||
el_uint(&mut a, 0xD7, 2);
|
||||
el_uint(&mut a, 0x73C5, 2);
|
||||
el_uint(&mut a, 0x83, 2);
|
||||
el_uint(&mut a, 0x9C, 0);
|
||||
el_str(&mut a, 0x86, "A_OPUS");
|
||||
el(&mut a, 0x63A2, &o.opus_head());
|
||||
el_uint(&mut a, 0x56AA, o.pre_skip() as u64 * 1_000_000_000 / OPUS_RATE as u64);
|
||||
el_uint(&mut a, 0x56BB, 80_000_000);
|
||||
let mut au = vec![];
|
||||
el_float(&mut au, 0xB5, OPUS_RATE as f64);
|
||||
el_uint(&mut au, 0x9F, opus_channels as u64);
|
||||
el(&mut a, 0xE1, &au);
|
||||
el(&mut tracks, TRACK_ENTRY, &a);
|
||||
}
|
||||
el(&mut head, TRACKS, &tracks);
|
||||
file.write_all(&head).map_err(io)?;
|
||||
let mut seg_len = head.len() as u64 - seg_start;
|
||||
|
||||
let (span_start, span_end) = job.settings.span(comp);
|
||||
let mut cursor = span_start.to_units_floor(OPUS_RATE as i64);
|
||||
let mut pcm_left: Vec<f32> = vec![];
|
||||
let mut packets = 0u64;
|
||||
let mut audio_until = |until: Tick, blocks: &mut Vec<Block>, finish: bool| {
|
||||
let Some(enc) = opus.as_mut() else { return };
|
||||
let end = until.to_units_floor(OPUS_RATE as i64);
|
||||
if end > cursor {
|
||||
let n = (end - cursor) as usize;
|
||||
let start = Tick(((cursor as i128 * TICKS_PER_SECOND as i128) / OPUS_RATE as i128) as i64);
|
||||
pcm_left.extend(mix(job, start, n, OPUS_RATE));
|
||||
cursor = end;
|
||||
}
|
||||
let fs = effectcraft_opusenc::OpusEncoder::FRAME_SIZE * opus_channels;
|
||||
if finish && !pcm_left.is_empty() {
|
||||
let pad = (fs - pcm_left.len() % fs) % fs + fs;
|
||||
pcm_left.extend(std::iter::repeat_n(0.0, pad));
|
||||
}
|
||||
while pcm_left.len() >= fs {
|
||||
let pkt = enc.encode_float(&pcm_left[..fs]);
|
||||
pcm_left.drain(..fs);
|
||||
blocks.push(Block { ms: packets as i64 * 20, track: 2, key: true, data: pkt });
|
||||
packets += 1;
|
||||
}
|
||||
};
|
||||
|
||||
let batch = batch_size();
|
||||
let mut i = 0;
|
||||
let flush_blocks = |blocks: &mut Vec<Block>, file: &mut crate::out::Out, seg_len: &mut u64| -> Result<()> {
|
||||
if !blocks.is_empty() {
|
||||
let c = cluster(blocks);
|
||||
file.write_all(&c).map_err(io)?;
|
||||
*seg_len += c.len() as u64;
|
||||
blocks.clear();
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
let mut blocks = vec![];
|
||||
while i < total {
|
||||
let end = (i + batch).min(total);
|
||||
// RGB, or the alpha matte alone (Channels: Alpha); AV1 in WebM carries no alpha track.
|
||||
let channels = if job.output.channels == Channels::Alpha { Channels::Alpha } else { Channels::Rgb };
|
||||
let frames: Vec<Vec<u8>> = (i..end).into_par_iter().map(|k| job.pixels(&job.frame(comp, k), comp, channels, w, h)).collect();
|
||||
for (j, px) in frames.iter().enumerate() {
|
||||
let k = i + j as u64;
|
||||
for p in venc.encode(px, k)? {
|
||||
blocks.push(Block { ms: frame_ms(k), track: 1, key: p.key, data: p.data });
|
||||
}
|
||||
}
|
||||
let t_end = if end >= total { span_end } else { job.settings.frame_time(comp, end) };
|
||||
audio_until(t_end, &mut blocks, end >= total);
|
||||
flush_blocks(&mut blocks, &mut file, &mut seg_len)?;
|
||||
st.advance(end - i)?;
|
||||
i = end;
|
||||
}
|
||||
if total == 0 {
|
||||
audio_until(span_end, &mut blocks, true);
|
||||
flush_blocks(&mut blocks, &mut file, &mut seg_len)?;
|
||||
}
|
||||
let mut size = [0u8; 8];
|
||||
size[0] = 0x01;
|
||||
size[1..].copy_from_slice(&seg_len.to_be_bytes()[1..]);
|
||||
file.seek(SeekFrom::Start(size_pos)).map_err(io)?;
|
||||
file.write_all(&size).map_err(io)?;
|
||||
file.seek(SeekFrom::End(0)).map_err(io)?;
|
||||
let bytes = file.finish()?;
|
||||
Ok(Report { path, frames: 0, width: w, height: h, seconds: 0.0, bytes, audio: with_audio, log: None, overflow: vec![] })
|
||||
}
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
//! HEVC (MP4 `hvc1`) and AV1 (MP4 `av01`, WebM `V_AV1`) export, decoded back through
|
||||
//! EffectCraft's own import path (FilmCraft's demuxers and HEVC / AV1 decoders) and — when
|
||||
//! installed — ffprobe / ffmpeg as external oracles (skipped otherwise).
|
||||
//!
|
||||
//! The comp: 64×48 @ 10 fps, 1.2 s (12 frames), dark-blue background, a red solid of 32×48
|
||||
//! centred, starting at frame 5 (inter frames code both static and changing blocks).
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use effectcraft_color::Label;
|
||||
use effectcraft_export::render_queue::{
|
||||
AudioOutput, CodecProfile, OpusApplication, OutputFormat, OutputModule, RateControlMode, RenderSettings, TimeSpan, WebmVideoCodec,
|
||||
};
|
||||
use effectcraft_export::{Job, Progress, export};
|
||||
use effectcraft_media::MediaPool;
|
||||
use effectcraft_project::{Comp, ItemId, ItemKind, LayerSource, Project, Solid, build};
|
||||
use effectcraft_raster::Image;
|
||||
use effectcraft_render::{FootageSource, NoFootage};
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
|
||||
const W: u32 = 64;
|
||||
const H: u32 = 48;
|
||||
const BG: [f32; 3] = [0.1, 0.1, 0.4];
|
||||
const FRAMES: u64 = 12;
|
||||
|
||||
fn out_dir(name: &str) -> PathBuf {
|
||||
let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/test-out/export-hevc-av1").join(name);
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).expect("mkdir");
|
||||
d
|
||||
}
|
||||
|
||||
fn project(audio: Option<&Path>) -> (Project, ItemId) {
|
||||
let mut p = Project::default();
|
||||
let mut comp = Comp::new(W, H, FrameRate::new(10, 1), Tick::from_seconds_f64(1.2));
|
||||
comp.background = BG;
|
||||
let sid = p.add_item("Red", Label::Red, None, ItemKind::Solid(Solid { color: [1.0, 0.0, 0.0], width: 32, height: H, pixel_aspect: 1.0 }));
|
||||
let mut l = build::layer(&mut p, &comp, "Red", LayerSource::Solid { item: sid }, (32, H), None);
|
||||
l.in_point = comp.frame_rate.tick_of(5);
|
||||
comp.layers.push(l);
|
||||
if let Some(path) = audio {
|
||||
let f = effectcraft_media::probe(path).expect("probe wav");
|
||||
let fid = p.add_item("tone.wav", Label::SeaFoam, None, ItemKind::Footage(f));
|
||||
let l = build::layer(&mut p, &comp, "tone", LayerSource::Footage { item: fid }, (0, 0), None);
|
||||
comp.layers.push(l);
|
||||
}
|
||||
let cid = p.add_item("Comp 1", Label::Sandstone, None, ItemKind::Comp(comp.into()));
|
||||
(p, cid)
|
||||
}
|
||||
|
||||
fn run(p: &Project, cid: ItemId, footage: &dyn FootageSource, om: &OutputModule, path: &Path) -> effectcraft_export::Report {
|
||||
let s = RenderSettings { time_span: TimeSpan::LengthOfComp, ..Default::default() };
|
||||
let path = path.to_string_lossy().to_string();
|
||||
let job = Job {
|
||||
project: p,
|
||||
footage,
|
||||
expr: None,
|
||||
accel: None,
|
||||
comp: cid,
|
||||
settings: &s,
|
||||
output: om,
|
||||
path: &path,
|
||||
sink: None,
|
||||
nested_switches: true,
|
||||
options: Default::default(),
|
||||
};
|
||||
let mut last = Progress::default();
|
||||
let r = export(&job, &mut |pr| {
|
||||
last = *pr;
|
||||
true
|
||||
})
|
||||
.expect("export");
|
||||
assert_eq!((last.done, r.frames), (FRAMES, FRAMES));
|
||||
r
|
||||
}
|
||||
|
||||
fn px(img: &Image, x: u32, y: u32) -> [f32; 4] {
|
||||
img.data[img.idx(x, y)]
|
||||
}
|
||||
|
||||
fn near(a: [f32; 3], b: [f32; 3], tol: f32) -> bool {
|
||||
a.iter().zip(b).all(|(x, y)| (x - y).abs() <= tol)
|
||||
}
|
||||
|
||||
fn check_frame(img: &Image, k: u64, tol: f32) {
|
||||
let corner = px(img, 2, 2);
|
||||
assert!(near([corner[0], corner[1], corner[2]], BG, tol), "frame {k} corner {corner:?}");
|
||||
let c = px(img, img.width / 2, img.height / 2);
|
||||
let want = if k >= 5 { [1.0, 0.0, 0.0] } else { BG };
|
||||
assert!(near([c[0], c[1], c[2]], want, tol), "frame {k} centre {c:?} want {want:?}");
|
||||
}
|
||||
|
||||
/// Import our file through the media layer and check every frame.
|
||||
fn decode_movie(path: &Path, tol: f32) -> effectcraft_project::Footage {
|
||||
let f = effectcraft_media::probe(path).expect("probe our output");
|
||||
assert_eq!((f.width, f.height), (W, H));
|
||||
assert!(f.has_video);
|
||||
let n = f.frame_rate.frame_at(f.duration - Tick(1)) + 1;
|
||||
assert_eq!(n as u64, FRAMES, "frame count from duration {:?} @ {:?}", f.duration, f.frame_rate);
|
||||
let pool = MediaPool::new();
|
||||
for k in 0..FRAMES {
|
||||
let img = pool.frame_at(&f, f.frame_rate.tick_of(k as i64)).expect("decode");
|
||||
check_frame(&img, k, tol);
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
fn have(tool: &str) -> bool {
|
||||
Command::new(tool).arg("-version").output().map(|o| o.status.success()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// `codec|w|h|pix_fmt|frames` of the video stream, or `None` without ffprobe.
|
||||
fn ffprobe_video(path: &Path) -> Option<String> {
|
||||
if !have("ffprobe") {
|
||||
eprintln!("ffprobe not found: skipping the oracle");
|
||||
return None;
|
||||
}
|
||||
let out = Command::new("ffprobe")
|
||||
.args(["-v", "error", "-select_streams", "v:0", "-count_frames", "-show_entries", "stream=codec_name,width,height,pix_fmt,nb_read_frames"])
|
||||
.args(["-of", "compact=p=0:nk=1"])
|
||||
.arg(path)
|
||||
.output()
|
||||
.expect("ffprobe");
|
||||
assert!(out.status.success() && out.stderr.is_empty(), "ffprobe rejected {}: {}", path.display(), String::from_utf8_lossy(&out.stderr));
|
||||
Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
/// Decode every frame with ffmpeg (no errors allowed) and check frames 0, 4, 5 and the last.
|
||||
fn ffmpeg_check(path: &Path, decoder: Option<&str>, tol: f32) {
|
||||
if !have("ffmpeg") {
|
||||
eprintln!("ffmpeg not found: skipping the oracle decode");
|
||||
return;
|
||||
}
|
||||
let dir = path.with_extension("ffmpeg");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let mut cmd = Command::new("ffmpeg");
|
||||
cmd.args(["-hide_banner", "-loglevel", "error", "-xerror", "-y"]);
|
||||
if let Some(d) = decoder {
|
||||
cmd.args(["-c:v", d]);
|
||||
}
|
||||
let out = cmd.arg("-i").arg(path).args(["-pix_fmt", "rgba"]).arg(dir.join("f%03d.png")).output().expect("ffmpeg");
|
||||
assert!(out.status.success() && out.stderr.is_empty(), "ffmpeg ({decoder:?}) failed on {}: {}", path.display(), String::from_utf8_lossy(&out.stderr));
|
||||
for k in [0, 4, 5, FRAMES - 1] {
|
||||
let img = image::open(dir.join(format!("f{:03}.png", k + 1))).expect("ffmpeg frame").to_rgba8();
|
||||
check_frame(&Image::from_rgba8(img.width(), img.height(), img.as_raw()), k, tol);
|
||||
}
|
||||
}
|
||||
|
||||
fn module(format: OutputFormat, keyint: u32) -> OutputModule {
|
||||
let mut om = OutputModule::for_format(format);
|
||||
om.keyframe_interval = keyint;
|
||||
om.codec.rate_control = RateControlMode::Quality;
|
||||
om.codec.quality = 80;
|
||||
om.audio = AudioOutput::Off;
|
||||
om
|
||||
}
|
||||
|
||||
/// Key-frame flags of the MP4 video samples (via ffprobe), or `None` without it.
|
||||
fn ffprobe_keyframes(path: &Path) -> Option<Vec<bool>> {
|
||||
if !have("ffprobe") {
|
||||
return None;
|
||||
}
|
||||
let out = Command::new("ffprobe")
|
||||
.args(["-v", "error", "-select_streams", "v:0", "-show_entries", "packet=flags", "-of", "csv=p=0"])
|
||||
.arg(path)
|
||||
.output()
|
||||
.expect("ffprobe");
|
||||
Some(String::from_utf8_lossy(&out.stdout).lines().map(|l| l.starts_with('K')).collect())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hevc_mp4_roundtrip() {
|
||||
let d = out_dir("hevc");
|
||||
let (p, cid) = project(None);
|
||||
let om = module(OutputFormat::Hevc, 4);
|
||||
let path = d.join("comp.mp4");
|
||||
let r = run(&p, cid, &NoFootage, &om, &path);
|
||||
assert!(r.bytes > 100 && !r.audio);
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
assert!(bytes.windows(4).any(|w| w == b"hvc1") && bytes.windows(4).any(|w| w == b"hvcC"));
|
||||
decode_movie(&path, 0.08);
|
||||
if let Some(s) = ffprobe_video(&path) {
|
||||
assert_eq!(s, "hevc|64|48|yuv420p|12");
|
||||
}
|
||||
if let Some(k) = ffprobe_keyframes(&path) {
|
||||
assert_eq!(k, (0..FRAMES).map(|i| i % 4 == 0).collect::<Vec<_>>(), "IDR every 4 frames");
|
||||
}
|
||||
ffmpeg_check(&path, None, 0.08);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hevc_main10_bitrate_mode() {
|
||||
let d = out_dir("hevc10");
|
||||
let (p, cid) = project(None);
|
||||
let mut om = module(OutputFormat::Hevc, 0);
|
||||
om.codec.profile = CodecProfile::Main10;
|
||||
om.codec.rate_control = RateControlMode::Bitrate;
|
||||
om.bitrate_kbps = 2_000;
|
||||
om.codec.level = Some(31);
|
||||
let path = d.join("comp10.mp4");
|
||||
run(&p, cid, &NoFootage, &om, &path);
|
||||
decode_movie(&path, 0.08);
|
||||
if let Some(s) = ffprobe_video(&path) {
|
||||
assert_eq!(s, "hevc|64|48|yuv420p10le|12");
|
||||
}
|
||||
ffmpeg_check(&path, None, 0.08);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn av1_mp4_roundtrip() {
|
||||
let d = out_dir("av1");
|
||||
let (p, cid) = project(None);
|
||||
let om = module(OutputFormat::Av1, 6);
|
||||
let path = d.join("comp.mp4");
|
||||
run(&p, cid, &NoFootage, &om, &path);
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
assert!(bytes.windows(4).any(|w| w == b"av01") && bytes.windows(4).any(|w| w == b"av1C"));
|
||||
decode_movie(&path, 0.08);
|
||||
if let Some(s) = ffprobe_video(&path) {
|
||||
assert_eq!(s, "av1|64|48|yuv420p|12");
|
||||
}
|
||||
if let Some(k) = ffprobe_keyframes(&path) {
|
||||
assert_eq!(k, (0..FRAMES).map(|i| i % 6 == 0).collect::<Vec<_>>(), "key frame every 6 frames");
|
||||
}
|
||||
ffmpeg_check(&path, Some("libdav1d"), 0.08);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn av1_webm_with_opus() {
|
||||
let d = out_dir("av1-webm");
|
||||
let wav = d.join("tone.wav");
|
||||
write_tone(&wav, 2.0);
|
||||
let (p, cid) = project(Some(&wav));
|
||||
let pool = MediaPool::new();
|
||||
let mut om = module(OutputFormat::WebM, 0);
|
||||
om.webm_codec = WebmVideoCodec::Av1;
|
||||
om.codec.profile = CodecProfile::Main10;
|
||||
om.audio = AudioOutput::Auto;
|
||||
om.opus_bitrate_kbps = 96;
|
||||
let path = d.join("comp.webm");
|
||||
let r = run(&p, cid, &pool, &om, &path);
|
||||
assert!(r.audio);
|
||||
let f = decode_movie(&path, 0.08);
|
||||
assert!(f.has_audio);
|
||||
let s = MediaPool::new().audio_samples(&f, Tick::from_seconds_f64(0.3), 24_000, 48_000);
|
||||
let l = rms(s.iter().step_by(2).copied());
|
||||
assert!((l - 0.3536).abs() < 0.06, "left RMS {l}");
|
||||
if let Some(s) = ffprobe_video(&path) {
|
||||
assert_eq!(s, "av1|64|48|yuv420p10le|12");
|
||||
}
|
||||
ffmpeg_check(&path, Some("libdav1d"), 0.08);
|
||||
}
|
||||
|
||||
/// Low-bitrate WebM audio goes through Opus SILK (voice) or hybrid (audio) coding and still
|
||||
/// decodes to the tone (FilmCraft's decoder, and ffmpeg when installed).
|
||||
#[test]
|
||||
fn webm_low_bitrate_opus_modes() {
|
||||
let d = out_dir("opus-modes");
|
||||
let wav = d.join("tone.wav");
|
||||
write_tone(&wav, 2.0);
|
||||
let (p, cid) = project(Some(&wav));
|
||||
let pool = MediaPool::new();
|
||||
for (kbps, app, name) in [(24u32, OpusApplication::Voip, "voice24.webm"), (40, OpusApplication::Audio, "audio40.webm")] {
|
||||
let mut om = OutputModule::for_format(OutputFormat::WebM);
|
||||
om.audio = AudioOutput::On;
|
||||
om.opus_bitrate_kbps = kbps;
|
||||
om.opus_application = app;
|
||||
let path = d.join(name);
|
||||
assert!(run(&p, cid, &pool, &om, &path).audio);
|
||||
let f = effectcraft_media::probe(&path).expect("probe");
|
||||
let s = MediaPool::new().audio_samples(&f, Tick::from_seconds_f64(0.3), 24_000, 48_000);
|
||||
let l = rms(s.iter().step_by(2).copied());
|
||||
assert!((l - 0.3536).abs() < 0.08, "{name}: left RMS {l}");
|
||||
if have("ffmpeg") {
|
||||
let out =
|
||||
Command::new("ffmpeg").args(["-v", "error", "-xerror", "-i"]).arg(&path).args(["-map", "0:a", "-f", "null", "-"]).output().expect("ffmpeg");
|
||||
assert!(out.status.success() && out.stderr.is_empty(), "{name}: {}", String::from_utf8_lossy(&out.stderr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rms(v: impl Iterator<Item = f32>) -> f32 {
|
||||
let (s, n) = v.fold((0.0f64, 0usize), |(s, n), x| (s + (x as f64) * (x as f64), n + 1));
|
||||
(s / n.max(1) as f64).sqrt() as f32
|
||||
}
|
||||
|
||||
/// 16-bit stereo WAV with a 440 Hz sine at amplitude 0.5 (left) and silence (right).
|
||||
fn write_tone(path: &Path, secs: f64) {
|
||||
let sr = 48_000u32;
|
||||
let n = (secs * sr as f64) as usize;
|
||||
let mut data = Vec::with_capacity(n * 4);
|
||||
for i in 0..n {
|
||||
let s = (0.5 * (2.0 * std::f64::consts::PI * 440.0 * i as f64 / sr as f64).sin() * 32767.0).round() as i16;
|
||||
data.extend_from_slice(&s.to_le_bytes());
|
||||
data.extend_from_slice(&0i16.to_le_bytes());
|
||||
}
|
||||
let mut w = Vec::new();
|
||||
w.extend_from_slice(b"RIFF");
|
||||
w.extend_from_slice(&(36 + data.len() as u32).to_le_bytes());
|
||||
w.extend_from_slice(b"WAVEfmt ");
|
||||
w.extend_from_slice(&16u32.to_le_bytes());
|
||||
w.extend_from_slice(&1u16.to_le_bytes());
|
||||
w.extend_from_slice(&2u16.to_le_bytes());
|
||||
w.extend_from_slice(&sr.to_le_bytes());
|
||||
w.extend_from_slice(&(sr * 4).to_le_bytes());
|
||||
w.extend_from_slice(&4u16.to_le_bytes());
|
||||
w.extend_from_slice(&16u16.to_le_bytes());
|
||||
w.extend_from_slice(b"data");
|
||||
w.extend_from_slice(&(data.len() as u32).to_le_bytes());
|
||||
w.extend_from_slice(&data);
|
||||
std::fs::write(path, w).expect("write wav");
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "effectcraft-hevcenc"
|
||||
description = "Clean-room pure-Rust HEVC (H.265) encoder (Main / Main 10, I and P slices, CABAC) for MP4 export, from ITU-T H.265"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dev-dependencies]
|
||||
filmcraft-hevc = { workspace = true }
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
# effectcraft-hevcenc
|
||||
|
||||
Clean-room, pure-Rust HEVC / H.265 encoder for EffectCraft's MP4 export (layer L0, `std` only,
|
||||
no `unsafe`, builds for `wasm32-unknown-unknown`).
|
||||
|
||||
## What it produces
|
||||
|
||||
- **Profiles:** Main (8-bit) and Main 10 (10-bit), 4:2:0, Main tier. `general_level_idc` is taken
|
||||
from the config or chosen as the lowest level whose MaxLumaPs, MaxLumaSr (and, in bitrate mode,
|
||||
MaxBR) fit (Table A.8).
|
||||
- **Parameter sets:** VPS, SPS (with VUI: BT.709 colour description, `video_full_range_flag`,
|
||||
timing info from the frame rate) and PPS, as raw NAL units (`Encoder::parameter_sets`) and as an
|
||||
`hvcC` record (ISO/IEC 14496-15 §8.3.3.1, `lengthSizeMinusOne` 3, complete VPS / SPS / PPS
|
||||
arrays, `Encoder::hvcc`).
|
||||
- **Pictures:** one slice per picture; `IDR_N_LP` every `keyint` pictures (`1` = all intra, `0` =
|
||||
only the first), `TRAIL_R` P pictures otherwise, each referencing the previous picture (one
|
||||
short-term RPS in the SPS, `num_ref_idx_l0_active` 1, no reordering, pts = dts). Packets are
|
||||
length-prefixed (4-byte big-endian) NAL units without parameter sets.
|
||||
- **Picture size:** any even width / height; the coded size is padded to a multiple of 8 by edge
|
||||
replication and cropped with the conformance window. Odd sizes are rejected with
|
||||
`Error::InvalidConfig` (4:2:0 conformance-window offsets are in units of two luma samples).
|
||||
|
||||
## Coding tools
|
||||
|
||||
- 64x64 CTUs, coding units 64x64 .. 8x8 chosen by rate-distortion optimisation: every candidate is
|
||||
actually predicted, transformed, quantised and reconstructed, and its rate is measured with a
|
||||
CABAC bit estimator that evolves the same context states as the real coder. Split decisions
|
||||
compare the best unsplit candidate with the recursively coded children (early exit after a skip).
|
||||
- **Intra:** all 35 modes (SATD search on a coarse grid plus refinement and the MPMs), 2Nx2N at
|
||||
8..32 and NxN (four 4x4 PUs) at 8x8, chroma in DM mode, MPM / remaining-mode coding, reference
|
||||
substitution, [1 2 1] and strong (32x32) smoothing, DC / angular boundary filters.
|
||||
- **Inter (P):** skip and merge (spatial candidates + zero candidates, `MaxNumMergeCand` 5) and AMVP
|
||||
with mvd coding; integer motion search (predictor starts + square pattern, steps 16..1) and
|
||||
half / quarter-sample refinement on pre-interpolated half-sample planes; the coded prediction uses
|
||||
the normative 8-tap / 4-tap interpolation filters and reference-picture edge clamping.
|
||||
- **Transforms:** DST-VII 4x4 (intra luma) and DCT 4..32 (inverse by even/odd decomposition,
|
||||
bit-identical to the matrix form), dead-zone quantisation (1/3 intra, 1/6 inter), flat scaling,
|
||||
sign data hiding, inferred transform splits only (`max_transform_hierarchy_depth_*` 0, so 64x64
|
||||
CUs use four 32x32 TUs).
|
||||
- **Entropy coding:** CABAC (context initialisation by slice QP and init type, decision / bypass /
|
||||
terminate bins, flush), `end_of_slice_segment_flag`, byte alignment, emulation prevention.
|
||||
- **In-loop filters:** the deblocking filter is on (the encoder deblocks its own reconstruction, so
|
||||
references match the decoder); SAO is disabled.
|
||||
- **Rate control:** `ConstantQp(qp)` or `Bitrate { kbps }`, a per-picture QP from a bits-x-Qstep
|
||||
model per picture type (I pictures get roughly 4x the bits of a P picture) with a buffer that
|
||||
spreads over- and undershoot over the following pictures.
|
||||
|
||||
Not implemented: B pictures / reordering, multiple references, TMVP, AMP and other inter partition
|
||||
shapes, residual quadtree splits, transform skip, scaling lists, PCM, SAO, RDOQ, cu_qp_delta /
|
||||
adaptive quantisation, tiles / WPP / multiple slices, range extensions (4:2:2, 4:4:4, > 10 bits).
|
||||
|
||||
## Measurements
|
||||
|
||||
On a shared, heavily loaded machine, opt-level 3: about 0.45 s of CPU per 1920x1080 picture
|
||||
(ffmpeg `testsrc2`, QP 22 / 32). On ffmpeg's `mandelbrot` 1080p source (8 pictures, IPPP, QP 32)
|
||||
the stream is about 12 % larger at about 0.2 dB lower mean PSNR-Y than x265 `--preset medium` at
|
||||
the same QP without psy tuning (110.5 kB / 42.3 dB vs 98.3 kB / 42.5 dB). The synthetic test image
|
||||
reaches 45.8 dB PSNR-Y at QP 22.
|
||||
|
||||
## Tests
|
||||
|
||||
`cargo test -p effectcraft-hevcenc`:
|
||||
|
||||
- every stream is decoded with the first-party `filmcraft-hevc` decoder and compared bit-exactly
|
||||
with the encoder's reconstruction (`Encoder::last_reconstruction`, hidden API);
|
||||
- when `ffmpeg` is on `PATH`, the Annex-B elementary stream is decoded with
|
||||
`ffmpeg -v error -xerror` (no stderr output allowed) and compared bit-exactly as well (skipped
|
||||
otherwise). ffmpeg is used only as an external test oracle;
|
||||
- covered: 8-bit and 10-bit, all-intra and IP, QP 0 noise (escape codes), QP 0..51, strong
|
||||
deblocking at high QP, sizes 8x8 .. 200x136 with cropping, POC wrap (300 pictures without IDR),
|
||||
bitrate mode within 2x of the target, `hvcC` layout;
|
||||
- `bench_1080p` (ignored) reports speed, size and PSNR on 1080p content generated by ffmpeg.
|
||||
|
||||
## Specifications
|
||||
|
||||
- ITU-T H.265 (V8, 08/2021) | ISO/IEC 23008-2, *High efficiency video coding*: 7 (syntax and
|
||||
semantics), 8.4 - 8.7 (intra / inter prediction, scaling and transforms, deblocking), 9.3 (CABAC,
|
||||
including the informative encoder description of 9.3.4), Annex A (profiles, tiers and levels),
|
||||
Annex E (VUI).
|
||||
- ISO/IEC 14496-15 (2019), §8.3.3.1 HEVCDecoderConfigurationRecord.
|
||||
|
||||
Implemented from the specification text. Constant tables (CABAC context layout and initValues,
|
||||
rangeTabLPS / transIdx, scan orders, the DCT / DST matrices, intra angles, interpolation filters,
|
||||
the chroma QP table, the deblocking beta / tC tables) and the decoder-side reconstruction
|
||||
arithmetic (intra reference substitution / filtering / prediction, fractional interpolation,
|
||||
deblocking) are mirrored from FilmCraft's first-party, spec-derived `filmcraft-hevc` decoder (same
|
||||
organisation, MIT OR Apache-2.0), so that encoder and decoder reconstruct identically. No
|
||||
third-party encoder or decoder source (x265, libde265, ffmpeg, HM, ...) was consulted.
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
//! RBSP bit writer (7.2, 9.2 Exp-Golomb) and NAL unit encapsulation with emulation prevention (7.3.1).
|
||||
|
||||
/// MSB-first bit writer.
|
||||
#[derive(Default)]
|
||||
pub struct BitWriter {
|
||||
buf: Vec<u8>,
|
||||
acc: u64,
|
||||
nbits: u32,
|
||||
}
|
||||
|
||||
impl BitWriter {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Write the `n` (<= 32) low bits of `v`.
|
||||
#[inline]
|
||||
pub fn put(&mut self, v: u32, n: u32) {
|
||||
debug_assert!(n <= 32);
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
let v = (v as u64) & ((1u64 << n) - 1);
|
||||
self.acc = (self.acc << n) | v;
|
||||
self.nbits += n;
|
||||
while self.nbits >= 8 {
|
||||
self.nbits -= 8;
|
||||
self.buf.push((self.acc >> self.nbits) as u8);
|
||||
}
|
||||
self.acc &= (1u64 << self.nbits) - 1;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn bit(&mut self, b: u32) {
|
||||
self.put(b & 1, 1);
|
||||
}
|
||||
|
||||
pub fn flag(&mut self, b: bool) {
|
||||
self.put(b as u32, 1);
|
||||
}
|
||||
|
||||
/// ue(v).
|
||||
pub fn ue(&mut self, v: u32) {
|
||||
let x = v as u64 + 1;
|
||||
let len = 64 - x.leading_zeros(); // bits of x
|
||||
// len - 1 leading zeros, then x in len bits
|
||||
self.put(0, len - 1);
|
||||
if len > 32 {
|
||||
self.put((x >> 32) as u32, len - 32);
|
||||
self.put(x as u32, 32);
|
||||
} else {
|
||||
self.put(x as u32, len);
|
||||
}
|
||||
}
|
||||
|
||||
/// se(v).
|
||||
pub fn se(&mut self, v: i32) {
|
||||
let k = if v > 0 { 2 * v as u32 - 1 } else { (-(v as i64) as u32) * 2 };
|
||||
self.ue(k);
|
||||
}
|
||||
|
||||
pub fn is_aligned(&self) -> bool {
|
||||
self.nbits == 0
|
||||
}
|
||||
|
||||
/// rbsp_trailing_bits(): stop bit and zero alignment.
|
||||
pub fn trailing(&mut self) {
|
||||
self.bit(1);
|
||||
self.align_zero();
|
||||
}
|
||||
|
||||
pub fn align_zero(&mut self) {
|
||||
if self.nbits > 0 {
|
||||
self.put(0, 8 - self.nbits);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_bytes(mut self) -> Vec<u8> {
|
||||
self.align_zero();
|
||||
self.buf
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a NAL unit (2-byte header, `nuh_layer_id` 0, `TemporalId` 0) from an RBSP, inserting
|
||||
/// emulation prevention bytes.
|
||||
pub fn nal_unit(nal_type: u8, rbsp: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(rbsp.len() + rbsp.len() / 64 + 4);
|
||||
out.push(nal_type << 1);
|
||||
out.push(1);
|
||||
let mut zeros = 0;
|
||||
for &b in rbsp {
|
||||
if zeros >= 2 && b <= 3 {
|
||||
out.push(3);
|
||||
zeros = 0;
|
||||
}
|
||||
out.push(b);
|
||||
if b == 0 {
|
||||
zeros += 1;
|
||||
} else {
|
||||
zeros = 0;
|
||||
}
|
||||
}
|
||||
// cabac_zero_words / trailing zero bytes never occur: every RBSP ends with the stop bit.
|
||||
out
|
||||
}
|
||||
|
||||
pub const NAL_TRAIL_R: u8 = 1;
|
||||
pub const NAL_IDR_N_LP: u8 = 20;
|
||||
pub const NAL_VPS: u8 = 32;
|
||||
pub const NAL_SPS: u8 = 33;
|
||||
pub const NAL_PPS: u8 = 34;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn exp_golomb() {
|
||||
let mut w = BitWriter::new();
|
||||
w.ue(0); // 1
|
||||
w.ue(1); // 010
|
||||
w.ue(2); // 011
|
||||
w.ue(3); // 00100
|
||||
w.se(-1); // ue(2) = 011
|
||||
w.trailing();
|
||||
// 1 010 011 00100 011 + stop bit: 1010 0110 0100 0111
|
||||
assert_eq!(w.into_bytes(), vec![0xa6, 0x47]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emulation_prevention() {
|
||||
let n = nal_unit(1, &[0, 0, 1, 0, 0, 0, 0, 0, 4]);
|
||||
assert_eq!(&n[2..], &[0, 0, 3, 1, 0, 0, 3, 0, 0, 3, 0, 4]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
//! CABAC: context initialisation (9.3.2.2), the arithmetic encoder (9.3.4.x, informative encoder
|
||||
//! description: EncodeDecision, EncodeBypass, EncodeTerminate, EncodeFlush) and a fractional-bit
|
||||
//! estimator with the same context-state evolution, used for rate-distortion decisions.
|
||||
|
||||
use crate::bitstream::BitWriter;
|
||||
use crate::tables::{CABAC_INIT, NEXT_STATE, NUM_CTX, RANGE_TAB_LPS};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Context states, each `pStateIdx << 1 | valMps`.
|
||||
pub type Contexts = [u8; NUM_CTX];
|
||||
|
||||
/// Initialise all context variables for `slice_qp` and `init_type` (9.3.2.2).
|
||||
pub fn init_contexts(slice_qp: i32, init_type: usize) -> Contexts {
|
||||
let mut ctx = [0u8; NUM_CTX];
|
||||
let qp = slice_qp.clamp(0, 51);
|
||||
for (c, &v) in ctx.iter_mut().zip(CABAC_INIT[init_type].iter()) {
|
||||
let slope = (v >> 4) as i32;
|
||||
let offset = (v & 15) as i32;
|
||||
let m = slope * 5 - 45;
|
||||
let n = (offset << 3) - 16;
|
||||
let pre = (((m * qp) >> 4) + n).clamp(1, 126);
|
||||
*c = if pre <= 63 { ((63 - pre) << 1) as u8 } else { (((pre - 64) << 1) | 1) as u8 };
|
||||
}
|
||||
ctx
|
||||
}
|
||||
|
||||
/// Destination of binarised syntax elements: the real arithmetic coder or the bit estimator.
|
||||
pub trait Sink {
|
||||
fn decision(&mut self, ctx: usize, bin: u32);
|
||||
fn bypass(&mut self, bin: u32);
|
||||
/// `n` bypass bins carrying the low `n` bits of `v`, MSB first.
|
||||
fn bypass_bits(&mut self, v: u32, n: u32) {
|
||||
for i in (0..n).rev() {
|
||||
self.bypass((v >> i) & 1);
|
||||
}
|
||||
}
|
||||
fn terminate(&mut self, bin: u32);
|
||||
}
|
||||
|
||||
/// Arithmetic encoder writing into a bit writer.
|
||||
pub struct CabacEncoder {
|
||||
pub ctx: Contexts,
|
||||
low: u32,
|
||||
range: u32,
|
||||
outstanding: u32,
|
||||
first: bool,
|
||||
pub w: BitWriter,
|
||||
}
|
||||
|
||||
impl CabacEncoder {
|
||||
/// Start encoding slice data into `w` (which must be byte aligned).
|
||||
pub fn new(w: BitWriter, ctx: Contexts) -> Self {
|
||||
debug_assert!(w.is_aligned());
|
||||
CabacEncoder { ctx, low: 0, range: 510, outstanding: 0, first: true, w }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn put_bit(&mut self, b: u32) {
|
||||
if self.first {
|
||||
self.first = false;
|
||||
} else {
|
||||
self.w.bit(b);
|
||||
}
|
||||
while self.outstanding > 0 {
|
||||
self.w.bit(1 - b);
|
||||
self.outstanding -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn renorm(&mut self) {
|
||||
while self.range < 256 {
|
||||
if self.low < 256 {
|
||||
self.put_bit(0);
|
||||
} else if self.low >= 512 {
|
||||
self.low -= 512;
|
||||
self.put_bit(1);
|
||||
} else {
|
||||
self.low -= 256;
|
||||
self.outstanding += 1;
|
||||
}
|
||||
self.range <<= 1;
|
||||
self.low <<= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// EncodeFlush after a terminating bin equal to 1; the final bit written is the
|
||||
/// rbsp_stop_one_bit. Returns the writer (byte aligned with zero bits).
|
||||
pub fn finish(mut self) -> BitWriter {
|
||||
self.range = 2;
|
||||
self.renorm();
|
||||
let b = (self.low >> 9) & 1;
|
||||
self.put_bit(b);
|
||||
self.w.put(((self.low >> 7) & 3) | 1, 2);
|
||||
self.w.align_zero();
|
||||
self.w
|
||||
}
|
||||
}
|
||||
|
||||
impl Sink for CabacEncoder {
|
||||
#[inline]
|
||||
fn decision(&mut self, ctx_idx: usize, bin: u32) {
|
||||
let s = self.ctx[ctx_idx] as usize;
|
||||
let mps = (s & 1) as u32;
|
||||
let q = ((self.range >> 6) & 3) as usize;
|
||||
let lps = RANGE_TAB_LPS[s >> 1][q] as u32;
|
||||
self.range -= lps;
|
||||
if bin != mps {
|
||||
self.low += self.range;
|
||||
self.range = lps;
|
||||
self.ctx[ctx_idx] = NEXT_STATE[s][1];
|
||||
} else {
|
||||
self.ctx[ctx_idx] = NEXT_STATE[s][0];
|
||||
}
|
||||
self.renorm();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bypass(&mut self, bin: u32) {
|
||||
self.low <<= 1;
|
||||
if bin != 0 {
|
||||
self.low += self.range;
|
||||
}
|
||||
if self.low >= 1024 {
|
||||
self.put_bit(1);
|
||||
self.low -= 1024;
|
||||
} else if self.low < 512 {
|
||||
self.put_bit(0);
|
||||
} else {
|
||||
self.low -= 512;
|
||||
self.outstanding += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn terminate(&mut self, bin: u32) {
|
||||
self.range -= 2;
|
||||
if bin != 0 {
|
||||
self.low += self.range;
|
||||
// the caller flushes with finish()
|
||||
} else {
|
||||
self.renorm();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fractional bit cost (1/32768 bit units) of coding the MPS / LPS in each state.
|
||||
fn cost_table() -> &'static [[u32; 2]; 64] {
|
||||
static T: OnceLock<[[u32; 2]; 64]> = OnceLock::new();
|
||||
T.get_or_init(|| {
|
||||
let mut t = [[0u32; 2]; 64];
|
||||
// pLPS(s) = 0.5 * alpha^s with alpha = (0.01875 / 0.5)^(1/63) (9.3.4.3 design)
|
||||
let alpha = (0.01875f64 / 0.5).powf(1.0 / 63.0);
|
||||
for (s, e) in t.iter_mut().enumerate() {
|
||||
let p = 0.5 * alpha.powi(s as i32);
|
||||
e[0] = (-(1.0 - p).log2() * 32768.0).round() as u32;
|
||||
e[1] = (-p.log2() * 32768.0).round() as u32;
|
||||
}
|
||||
t
|
||||
})
|
||||
}
|
||||
|
||||
pub const BIT: u64 = 32768;
|
||||
|
||||
/// Bit estimator: tracks context states like the encoder and accumulates the ideal code length.
|
||||
#[derive(Clone)]
|
||||
pub struct Estimator {
|
||||
pub ctx: Contexts,
|
||||
/// Accumulated cost in 1/32768 bits.
|
||||
pub bits: u64,
|
||||
table: &'static [[u32; 2]; 64],
|
||||
}
|
||||
|
||||
impl Estimator {
|
||||
pub fn new(ctx: Contexts) -> Self {
|
||||
Estimator { ctx, bits: 0, table: cost_table() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Sink for Estimator {
|
||||
#[inline]
|
||||
fn decision(&mut self, ctx_idx: usize, bin: u32) {
|
||||
let s = self.ctx[ctx_idx] as usize;
|
||||
let lps = (bin != (s & 1) as u32) as usize;
|
||||
self.bits += self.table[s >> 1][lps] as u64;
|
||||
self.ctx[ctx_idx] = NEXT_STATE[s][lps];
|
||||
}
|
||||
#[inline]
|
||||
fn bypass(&mut self, _bin: u32) {
|
||||
self.bits += BIT;
|
||||
}
|
||||
#[inline]
|
||||
fn bypass_bits(&mut self, _v: u32, n: u32) {
|
||||
self.bits += BIT * n as u64;
|
||||
}
|
||||
fn terminate(&mut self, _bin: u32) {
|
||||
self.bits += 7; // ~0 bits for a 0 bin
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn context_init_formula() {
|
||||
let ctx = init_contexts(30, 0);
|
||||
// initValue 154: m = 0, n = 64 -> pStateIdx 0, valMps 1
|
||||
assert_eq!(ctx[crate::tables::CU_TRANSQUANT_BYPASS], 1);
|
||||
// split_cu_flag ctx 0 (initValue 139) at QP 30: preCtxState 62
|
||||
assert_eq!(ctx[crate::tables::SPLIT_CU], (63 - 62) << 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn costs_are_sane() {
|
||||
let t = cost_table();
|
||||
assert_eq!(t[0][0], 32768);
|
||||
assert_eq!(t[0][1], 32768);
|
||||
assert!(t[62][0] < 1000 && t[62][1] > 5 * 32768);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
//! Deblocking filter (8.7.2) over a whole reconstructed picture, mirrored from the filmcraft-hevc
|
||||
//! decoder: all vertical edges on the 8x8 grid first, then all horizontal edges. One slice, one QP,
|
||||
//! uni-directional prediction from a single reference picture, offsets 0.
|
||||
|
||||
use crate::frame::{Blk, E_EDGE_H, E_EDGE_V, F_CBF, F_INTRA};
|
||||
use crate::tables::{BETA_TABLE, TC_TABLE, qpc_420};
|
||||
|
||||
#[inline]
|
||||
fn tc_beta(q: i32, bs: i32, bd: u32) -> (i32, i32) {
|
||||
let qb = q.clamp(0, 51);
|
||||
let qt = (q + 2 * (bs - 1)).clamp(0, 53);
|
||||
((TC_TABLE[qt as usize] as i32) << (bd - 8), (BETA_TABLE[qb as usize] as i32) << (bd - 8))
|
||||
}
|
||||
|
||||
/// Boundary strength (8.7.2.4) between 4x4 blocks p and q on a transform / prediction edge.
|
||||
fn boundary_strength(p: &Blk, q: &Blk) -> i32 {
|
||||
if (p.flags | q.flags) & F_INTRA != 0 {
|
||||
return 2;
|
||||
}
|
||||
if (p.flags | q.flags) & F_CBF != 0 {
|
||||
return 1;
|
||||
}
|
||||
let far = (p.mv[0] as i32 - q.mv[0] as i32).abs() >= 4 || (p.mv[1] as i32 - q.mv[1] as i32).abs() >= 4;
|
||||
far as i32
|
||||
}
|
||||
|
||||
/// Deblock the planes in place. `qp` is the slice QpY (without QpBdOffset).
|
||||
pub fn deblock(planes: &mut [Vec<u16>; 3], blk: &[Blk], w: usize, h: usize, qp: i32, bd: u32) {
|
||||
let (w4, h4) = (w / 4, h / 4);
|
||||
let cw = w / 2;
|
||||
// chroma tc (bS 2 only; QpC from the averaged luma QP, no offsets)
|
||||
let qt = (qpc_420(qp) + 2).clamp(0, 53);
|
||||
let tc_c = (TC_TABLE[qt as usize] as i32) << (bd - 8);
|
||||
// vertical edges
|
||||
for by in 0..h4 {
|
||||
for bx in (2..w4).step_by(2) {
|
||||
let qi = by * w4 + bx;
|
||||
if blk[qi].flags & E_EDGE_V == 0 {
|
||||
continue;
|
||||
}
|
||||
let bs = boundary_strength(&blk[qi - 1], &blk[qi]);
|
||||
if bs == 0 {
|
||||
continue;
|
||||
}
|
||||
let (x, y) = (bx * 4, by * 4);
|
||||
let (tc, beta) = tc_beta(qp, bs, bd);
|
||||
filter_luma(&mut planes[0], y * w + x, 1, w, tc, beta, bd);
|
||||
// chroma: 8x8 chroma grid (16 luma samples), one 4-sample chroma segment per 8 luma rows
|
||||
if bs == 2 && bx % 4 == 0 && by % 2 == 0 {
|
||||
for c in 1..3 {
|
||||
filter_chroma(&mut planes[c], (y / 2) * cw + x / 2, 1, cw, tc_c, bd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// horizontal edges
|
||||
for by in (2..h4).step_by(2) {
|
||||
for bx in 0..w4 {
|
||||
let qi = by * w4 + bx;
|
||||
if blk[qi].flags & E_EDGE_H == 0 {
|
||||
continue;
|
||||
}
|
||||
let bs = boundary_strength(&blk[qi - w4], &blk[qi]);
|
||||
if bs == 0 {
|
||||
continue;
|
||||
}
|
||||
let (x, y) = (bx * 4, by * 4);
|
||||
let (tc, beta) = tc_beta(qp, bs, bd);
|
||||
filter_luma(&mut planes[0], y * w + x, w, 1, tc, beta, bd);
|
||||
if bs == 2 && by % 4 == 0 && bx % 2 == 0 {
|
||||
for c in 1..3 {
|
||||
filter_chroma(&mut planes[c], (y / 2) * cw + x / 2, cw, 1, tc_c, bd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter one 4-sample luma edge segment. `o` = offset of q0 of the first line, `step` = distance
|
||||
/// between samples across the edge, `along` = distance between lines.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn filter_luma(s: &mut [u16], o: usize, step: usize, along: usize, tc: i32, beta: i32, bd: u32) {
|
||||
let px = |s: &[u16], k: usize, i: usize| s[o + k * along - (i + 1) * step] as i32;
|
||||
let qx = |s: &[u16], k: usize, i: usize| s[o + k * along + i * step] as i32;
|
||||
let dp0 = (px(s, 0, 2) - 2 * px(s, 0, 1) + px(s, 0, 0)).abs();
|
||||
let dp3 = (px(s, 3, 2) - 2 * px(s, 3, 1) + px(s, 3, 0)).abs();
|
||||
let dq0 = (qx(s, 0, 2) - 2 * qx(s, 0, 1) + qx(s, 0, 0)).abs();
|
||||
let dq3 = (qx(s, 3, 2) - 2 * qx(s, 3, 1) + qx(s, 3, 0)).abs();
|
||||
let (dpq0, dpq3) = (dp0 + dq0, dp3 + dq3);
|
||||
let (dp, dq) = (dp0 + dp3, dq0 + dq3);
|
||||
let d = dpq0 + dpq3;
|
||||
if d >= beta {
|
||||
return;
|
||||
}
|
||||
let dsam = |s: &[u16], k: usize, dpq: i32| -> bool {
|
||||
dpq < (beta >> 2)
|
||||
&& (px(s, k, 3) - px(s, k, 0)).abs() + (qx(s, k, 0) - qx(s, k, 3)).abs() < (beta >> 3)
|
||||
&& (px(s, k, 0) - qx(s, k, 0)).abs() < ((5 * tc + 1) >> 1)
|
||||
};
|
||||
let strong = dsam(s, 0, 2 * dpq0) && dsam(s, 3, 2 * dpq3);
|
||||
let dep = dp < ((beta + (beta >> 1)) >> 3);
|
||||
let deq = dq < ((beta + (beta >> 1)) >> 3);
|
||||
let max = (1i32 << bd) - 1;
|
||||
for k in 0..4 {
|
||||
let base = o + k * along;
|
||||
let p = |i: usize| s[base - (i + 1) * step] as i32;
|
||||
let q = |i: usize| s[base + i * step] as i32;
|
||||
let (p0, p1, p2, p3) = (p(0), p(1), p(2), p(3));
|
||||
let (q0, q1, q2, q3) = (q(0), q(1), q(2), q(3));
|
||||
if strong {
|
||||
let t2 = 2 * tc;
|
||||
s[base - step] = ((p2 + 2 * p1 + 2 * p0 + 2 * q0 + q1 + 4) >> 3).clamp(p0 - t2, p0 + t2) as u16;
|
||||
s[base - 2 * step] = ((p2 + p1 + p0 + q0 + 2) >> 2).clamp(p1 - t2, p1 + t2) as u16;
|
||||
s[base - 3 * step] = ((2 * p3 + 3 * p2 + p1 + p0 + q0 + 4) >> 3).clamp(p2 - t2, p2 + t2) as u16;
|
||||
s[base] = ((p1 + 2 * p0 + 2 * q0 + 2 * q1 + q2 + 4) >> 3).clamp(q0 - t2, q0 + t2) as u16;
|
||||
s[base + step] = ((p0 + q0 + q1 + q2 + 2) >> 2).clamp(q1 - t2, q1 + t2) as u16;
|
||||
s[base + 2 * step] = ((p0 + q0 + q1 + 3 * q2 + 2 * q3 + 4) >> 3).clamp(q2 - t2, q2 + t2) as u16;
|
||||
} else {
|
||||
let mut delta = (9 * (q0 - p0) - 3 * (q1 - p1) + 8) >> 4;
|
||||
if delta.abs() >= tc * 10 {
|
||||
continue;
|
||||
}
|
||||
delta = delta.clamp(-tc, tc);
|
||||
s[base - step] = (p0 + delta).clamp(0, max) as u16;
|
||||
s[base] = (q0 - delta).clamp(0, max) as u16;
|
||||
if dep {
|
||||
let dp = ((((p2 + p0 + 1) >> 1) - p1 + delta) >> 1).clamp(-(tc >> 1), tc >> 1);
|
||||
s[base - 2 * step] = (p1 + dp).clamp(0, max) as u16;
|
||||
}
|
||||
if deq {
|
||||
let dq = ((((q2 + q0 + 1) >> 1) - q1 - delta) >> 1).clamp(-(tc >> 1), tc >> 1);
|
||||
s[base + step] = (q1 + dq).clamp(0, max) as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter one 4-sample chroma edge segment.
|
||||
fn filter_chroma(s: &mut [u16], o: usize, step: usize, along: usize, tc: i32, bd: u32) {
|
||||
let max = (1i32 << bd) - 1;
|
||||
for k in 0..4 {
|
||||
let base = o + k * along;
|
||||
let (p0, p1) = (s[base - step] as i32, s[base - 2 * step] as i32);
|
||||
let (q0, q1) = (s[base] as i32, s[base + step] as i32);
|
||||
let delta = ((((q0 - p0) << 2) + p1 - q1 + 4) >> 3).clamp(-tc, tc);
|
||||
s[base - step] = (p0 + delta).clamp(0, max) as u16;
|
||||
s[base] = (q0 - delta).clamp(0, max) as u16;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,155 @@
|
|||
//! Fractional sample interpolation (8.5.3.3.3) and default weighted prediction (8-262), mirrored
|
||||
//! from the filmcraft-hevc decoder. Reference sample fetches clamp to the picture (8-228, 8-229).
|
||||
|
||||
use crate::tables::{CHROMA_FILTER, LUMA_FILTER};
|
||||
|
||||
/// One plane of a reference picture.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PlaneRef<'a> {
|
||||
pub data: &'a [u16],
|
||||
pub w: usize,
|
||||
pub h: usize,
|
||||
}
|
||||
|
||||
impl PlaneRef<'_> {
|
||||
/// Copy a w x h window at (x0, y0) with edge replication.
|
||||
fn window(&self, x0: i32, y0: i32, w: usize, h: usize, out: &mut [i16], os: usize) {
|
||||
let (pw, ph) = (self.w as i32, self.h as i32);
|
||||
let inside_x = x0 >= 0 && x0 + w as i32 <= pw;
|
||||
for r in 0..h {
|
||||
let yy = (y0 + r as i32).clamp(0, ph - 1) as usize;
|
||||
let row = &self.data[yy * self.w..yy * self.w + self.w];
|
||||
let dst = &mut out[r * os..r * os + w];
|
||||
if inside_x {
|
||||
for (d, &s) in dst.iter_mut().zip(&row[x0 as usize..x0 as usize + w]) {
|
||||
*d = s as i16;
|
||||
}
|
||||
} else {
|
||||
for (c, d) in dst.iter_mut().enumerate() {
|
||||
*d = row[(x0 + c as i32).clamp(0, pw - 1) as usize] as i16;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Luma prediction samples (14-bit intermediate) of a w x h block at (x, y) displaced by the
|
||||
/// quarter-sample vector `mv`, into `out` (stride w).
|
||||
pub fn mc_luma(f: PlaneRef, bd: u32, x: i32, y: i32, mv: [i16; 2], w: usize, h: usize, out: &mut [i16]) {
|
||||
let (fx, fy) = ((mv[0] & 3) as usize, (mv[1] & 3) as usize);
|
||||
let xi = x + (mv[0] as i32 >> 2);
|
||||
let yi = y + (mv[1] as i32 >> 2);
|
||||
let shift1 = bd.min(12) - 8;
|
||||
let shift3 = 14 - bd;
|
||||
let ws = w + 7;
|
||||
let mut win = vec![0i16; ws * (h + 7)];
|
||||
if fx == 0 && fy == 0 {
|
||||
f.window(xi, yi, w, h, &mut win, ws);
|
||||
for r in 0..h {
|
||||
for c in 0..w {
|
||||
out[r * w + c] = win[r * ws + c] << shift3;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
f.window(xi - 3, yi - 3, w + 7, h + 7, &mut win, ws);
|
||||
let hf = &LUMA_FILTER[fx];
|
||||
let vf = &LUMA_FILTER[fy];
|
||||
if fy == 0 {
|
||||
for r in 0..h {
|
||||
let row = &win[(r + 3) * ws..];
|
||||
for c in 0..w {
|
||||
let s: i32 = (0..8).map(|k| hf[k] as i32 * row[c + k] as i32).sum();
|
||||
out[r * w + c] = (s >> shift1) as i16;
|
||||
}
|
||||
}
|
||||
} else if fx == 0 {
|
||||
for r in 0..h {
|
||||
for c in 0..w {
|
||||
let s: i32 = (0..8).map(|k| vf[k] as i32 * win[(r + k) * ws + c + 3] as i32).sum();
|
||||
out[r * w + c] = (s >> shift1) as i16;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut tmp = vec![0i16; ws * (h + 7)];
|
||||
for r in 0..h + 7 {
|
||||
let row = &win[r * ws..];
|
||||
for c in 0..w {
|
||||
let s: i32 = (0..8).map(|k| hf[k] as i32 * row[c + k] as i32).sum();
|
||||
tmp[r * ws + c] = (s >> shift1) as i16;
|
||||
}
|
||||
}
|
||||
for r in 0..h {
|
||||
for c in 0..w {
|
||||
let s: i32 = (0..8).map(|k| vf[k] as i32 * tmp[(r + k) * ws + c] as i32).sum();
|
||||
out[r * w + c] = (s >> 6) as i16;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Chroma prediction samples of a w x h block at chroma position (x, y) with a 1/8-sample vector.
|
||||
pub fn mc_chroma(f: PlaneRef, bd: u32, x: i32, y: i32, mv: [i16; 2], w: usize, h: usize, out: &mut [i16]) {
|
||||
let (fx, fy) = ((mv[0] & 7) as usize, (mv[1] & 7) as usize);
|
||||
let xi = x + (mv[0] as i32 >> 3);
|
||||
let yi = y + (mv[1] as i32 >> 3);
|
||||
let shift1 = bd.min(12) - 8;
|
||||
let shift3 = 14 - bd;
|
||||
let ws = w + 7;
|
||||
let mut win = vec![0i16; ws * (h + 7)];
|
||||
if fx == 0 && fy == 0 {
|
||||
f.window(xi, yi, w, h, &mut win, ws);
|
||||
for r in 0..h {
|
||||
for c in 0..w {
|
||||
out[r * w + c] = win[r * ws + c] << shift3;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
f.window(xi - 1, yi - 1, w + 3, h + 3, &mut win, ws);
|
||||
let hf = &CHROMA_FILTER[fx];
|
||||
let vf = &CHROMA_FILTER[fy];
|
||||
if fy == 0 {
|
||||
for r in 0..h {
|
||||
let row = &win[(r + 1) * ws..];
|
||||
for c in 0..w {
|
||||
let s: i32 = (0..4).map(|k| hf[k] as i32 * row[c + k] as i32).sum();
|
||||
out[r * w + c] = (s >> shift1) as i16;
|
||||
}
|
||||
}
|
||||
} else if fx == 0 {
|
||||
for r in 0..h {
|
||||
for c in 0..w {
|
||||
let s: i32 = (0..4).map(|k| vf[k] as i32 * win[(r + k) * ws + c + 1] as i32).sum();
|
||||
out[r * w + c] = (s >> shift1) as i16;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut tmp = vec![0i16; ws * (h + 7)];
|
||||
for r in 0..h + 3 {
|
||||
let row = &win[r * ws..];
|
||||
for c in 0..w {
|
||||
let s: i32 = (0..4).map(|k| hf[k] as i32 * row[c + k] as i32).sum();
|
||||
tmp[r * ws + c] = (s >> shift1) as i16;
|
||||
}
|
||||
}
|
||||
for r in 0..h {
|
||||
for c in 0..w {
|
||||
let s: i32 = (0..4).map(|k| vf[k] as i32 * tmp[(r + k) * ws + c] as i32).sum();
|
||||
out[r * w + c] = (s >> 6) as i16;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default weighted prediction, one list (8-262): 14-bit samples to `bd`-bit samples.
|
||||
pub fn put_uni(p: &[i16], w: usize, h: usize, bd: u32, dst: &mut [u16], ds: usize) {
|
||||
let shift = 14 - bd;
|
||||
let off = if shift > 0 { 1 << (shift - 1) } else { 0 };
|
||||
let max = (1i32 << bd) - 1;
|
||||
for r in 0..h {
|
||||
for c in 0..w {
|
||||
dst[r * ds + c] = ((p[r * w + c] as i32 + off) >> shift).clamp(0, max) as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
//! Intra sample prediction (8.4.4.2), mirrored from the filmcraft-hevc decoder so the encoder
|
||||
//! reconstructs exactly like a decoder: reference substitution, filtering (incl. strong intra
|
||||
//! smoothing), planar, DC and the 33 angular modes.
|
||||
|
||||
use crate::tables::{INTRA_PRED_ANGLE, INV_ANGLE};
|
||||
|
||||
/// Reference samples of an nTbS block: `left[0]` = `top[0]` = p[-1][-1], `left[1 + y]` = p[-1][y],
|
||||
/// `top[1 + x]` = p[x][-1] for x, y = 0..2*nTbS-1.
|
||||
#[derive(Clone)]
|
||||
pub struct Refs {
|
||||
pub left: [u16; 129],
|
||||
pub top: [u16; 129],
|
||||
}
|
||||
|
||||
impl Default for Refs {
|
||||
fn default() -> Self {
|
||||
Refs { left: [0; 129], top: [0; 129] }
|
||||
}
|
||||
}
|
||||
|
||||
/// Substitution process (8.4.4.2.2). `avail_left[1 + y]` / `avail_top[1 + x]` (index 0 = corner) tell
|
||||
/// which samples are available; unavailable ones are replaced.
|
||||
pub fn substitute(r: &mut Refs, avail_left: &[bool; 129], avail_top: &[bool; 129], n: usize, bit_depth: u32) {
|
||||
let n2 = 2 * n;
|
||||
// Sequence order: p[-1][2n-1] .. p[-1][-1], p[0][-1] .. p[2n-1][-1]
|
||||
let any = avail_left[..=n2].iter().any(|&a| a) || avail_top[1..=n2].iter().any(|&a| a);
|
||||
if !any {
|
||||
let v = 1u16 << (bit_depth - 1);
|
||||
r.left[..=n2].fill(v);
|
||||
r.top[..=n2].fill(v);
|
||||
return;
|
||||
}
|
||||
if !avail_left[n2] {
|
||||
// search upwards then along the top
|
||||
let mut found = None;
|
||||
for y in (0..n2).rev() {
|
||||
if avail_left[y] {
|
||||
found = Some(r.left[y]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if found.is_none() {
|
||||
for x in 1..=n2 {
|
||||
if avail_top[x] {
|
||||
found = Some(r.top[x]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
r.left[n2] = found.unwrap_or(0);
|
||||
}
|
||||
for y in (0..n2).rev() {
|
||||
if !avail_left[y] {
|
||||
r.left[y] = r.left[y + 1];
|
||||
}
|
||||
}
|
||||
r.top[0] = r.left[0];
|
||||
for x in 1..=n2 {
|
||||
if !avail_top[x] {
|
||||
r.top[x] = r.top[x - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Filtering process of neighbouring samples (8.4.4.2.3) for luma (and 4:4:4 chroma).
|
||||
pub fn filter(r: &mut Refs, mode: u32, n: usize, strong_enabled: bool, is_luma: bool, bit_depth: u32) {
|
||||
if mode == 1 || n == 4 {
|
||||
return;
|
||||
}
|
||||
let min_dist = (mode as i32 - 26).abs().min((mode as i32 - 10).abs());
|
||||
let thres = match n {
|
||||
8 => 7,
|
||||
16 => 1,
|
||||
_ => 0,
|
||||
};
|
||||
if min_dist <= thres {
|
||||
return;
|
||||
}
|
||||
let n2 = 2 * n;
|
||||
let c = r.left[0] as i32;
|
||||
if strong_enabled && is_luma && n == 32 {
|
||||
let (bl, tr) = (r.left[n2] as i32, r.top[n2] as i32);
|
||||
let thr = 1 << (bit_depth - 5);
|
||||
if (c + tr - 2 * r.top[n] as i32).abs() < thr && (c + bl - 2 * r.left[n] as i32).abs() < thr {
|
||||
for i in 0..63 {
|
||||
r.left[1 + i] = (((63 - i as i32) * c + (i as i32 + 1) * bl + 32) >> 6) as u16;
|
||||
r.top[1 + i] = (((63 - i as i32) * c + (i as i32 + 1) * tr + 32) >> 6) as u16;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
let (l, t) = (r.left, r.top);
|
||||
let corner = ((l[1] as u32 + 2 * c as u32 + t[1] as u32 + 2) >> 2) as u16;
|
||||
for i in 1..n2 {
|
||||
r.left[i] = ((l[i - 1] as u32 + 2 * l[i] as u32 + l[i + 1] as u32 + 2) >> 2) as u16;
|
||||
r.top[i] = ((t[i - 1] as u32 + 2 * t[i] as u32 + t[i + 1] as u32 + 2) >> 2) as u16;
|
||||
}
|
||||
r.left[0] = corner;
|
||||
r.top[0] = corner;
|
||||
}
|
||||
|
||||
/// Predict an n x n block with `mode` into `out` (stride `os`). `edge_filters`: DC / horizontal /
|
||||
/// vertical boundary smoothing (luma blocks smaller than 32x32).
|
||||
pub fn predict(r: &Refs, mode: u32, n: usize, edge_filters: bool, bit_depth: u32, out: &mut [u16], os: usize) {
|
||||
let max = (1i32 << bit_depth) - 1;
|
||||
match mode {
|
||||
0 => {
|
||||
// planar
|
||||
let log2 = n.trailing_zeros();
|
||||
let tr = r.top[1 + n] as i32;
|
||||
let bl = r.left[1 + n] as i32;
|
||||
for y in 0..n {
|
||||
let l = r.left[1 + y] as i32;
|
||||
for x in 0..n {
|
||||
let t = r.top[1 + x] as i32;
|
||||
let v = ((n - 1 - x) as i32 * l + (x as i32 + 1) * tr + (n - 1 - y) as i32 * t + (y as i32 + 1) * bl + n as i32) >> (log2 + 1);
|
||||
out[y * os + x] = v as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
let log2 = n.trailing_zeros();
|
||||
let sum: u32 = r.top[1..=n].iter().map(|&v| v as u32).sum::<u32>() + r.left[1..=n].iter().map(|&v| v as u32).sum::<u32>();
|
||||
let dc = ((sum + n as u32) >> (log2 + 1)) as u16;
|
||||
for y in 0..n {
|
||||
out[y * os..y * os + n].fill(dc);
|
||||
}
|
||||
if edge_filters && n < 32 {
|
||||
let d = dc as u32;
|
||||
out[0] = ((r.left[1] as u32 + 2 * d + r.top[1] as u32 + 2) >> 2) as u16;
|
||||
for x in 1..n {
|
||||
out[x] = ((r.top[1 + x] as u32 + 3 * d + 2) >> 2) as u16;
|
||||
}
|
||||
for y in 1..n {
|
||||
out[y * os] = ((r.left[1 + y] as u32 + 3 * d + 2) >> 2) as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let angle = INTRA_PRED_ANGLE[mode as usize] as i32;
|
||||
// reference array ref[-n..=2n] stored at offset n
|
||||
let mut refa = [0i32; 3 * 64 + 1];
|
||||
let base = n as i32;
|
||||
let (main, side) = if mode >= 18 { (&r.top, &r.left) } else { (&r.left, &r.top) };
|
||||
for x in 0..=n {
|
||||
refa[(base + x as i32) as usize] = main[x] as i32; // ref[x] = p[-1+x][-1]
|
||||
}
|
||||
if angle < 0 {
|
||||
let lim = (n as i32 * angle) >> 5;
|
||||
if lim < -1 {
|
||||
let inv = INV_ANGLE[(mode - 11) as usize] as i32;
|
||||
for x in lim..=-1 {
|
||||
let idx = -1 + ((x * inv + 128) >> 8);
|
||||
refa[(base + x) as usize] = side[(idx + 1) as usize] as i32;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for x in n + 1..=2 * n {
|
||||
refa[(base + x as i32) as usize] = main[x] as i32;
|
||||
}
|
||||
}
|
||||
let horizontal = mode < 18;
|
||||
for j in 0..n {
|
||||
let pos = (j as i32 + 1) * angle;
|
||||
let idx = pos >> 5;
|
||||
let fact = pos & 31;
|
||||
for i in 0..n {
|
||||
let k = (base + i as i32 + idx + 1) as usize;
|
||||
let v = if fact != 0 { ((32 - fact) * refa[k] + fact * refa[k + 1] + 16) >> 5 } else { refa[k] };
|
||||
if horizontal {
|
||||
out[i * os + j] = v as u16;
|
||||
} else {
|
||||
out[j * os + i] = v as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
if edge_filters && n < 32 {
|
||||
if mode == 26 {
|
||||
for y in 0..n {
|
||||
let v = r.top[1] as i32 + ((r.left[1 + y] as i32 - r.left[0] as i32) >> 1);
|
||||
out[y * os] = v.clamp(0, max) as u16;
|
||||
}
|
||||
} else if mode == 10 {
|
||||
for x in 0..n {
|
||||
let v = r.left[1] as i32 + ((r.top[1 + x] as i32 - r.top[0] as i32) >> 1);
|
||||
out[x] = v.clamp(0, max) as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn refs(f: impl Fn(i32) -> u16) -> Refs {
|
||||
let mut r = Refs::default();
|
||||
r.left[0] = f(0);
|
||||
r.top[0] = f(0);
|
||||
for i in 1..129 {
|
||||
r.left[i] = f(-(i as i32));
|
||||
r.top[i] = f(i as i32);
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dc_and_planar_flat() {
|
||||
let r = refs(|_| 100);
|
||||
let mut out = [0u16; 64];
|
||||
predict(&r, 1, 8, true, 8, &mut out, 8);
|
||||
assert!(out.iter().all(|&v| v == 100));
|
||||
predict(&r, 0, 8, true, 8, &mut out, 8);
|
||||
assert!(out.iter().all(|&v| v == 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pure_vertical_and_horizontal() {
|
||||
let r = refs(|i| (128 + i) as u16);
|
||||
let mut out = [0u16; 16];
|
||||
predict(&r, 26, 4, false, 8, &mut out, 4);
|
||||
for y in 0..4 {
|
||||
for x in 0..4 {
|
||||
assert_eq!(out[y * 4 + x], r.top[1 + x]);
|
||||
}
|
||||
}
|
||||
predict(&r, 10, 4, false, 8, &mut out, 4);
|
||||
for y in 0..4 {
|
||||
for x in 0..4 {
|
||||
assert_eq!(out[y * 4 + x], r.left[1 + y]);
|
||||
}
|
||||
}
|
||||
// diagonal mode 34 reads top[x + y + 2]
|
||||
predict(&r, 34, 4, false, 8, &mut out, 4);
|
||||
assert_eq!(out[4 + 1], r.top[1 + 1 + 1 + 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn substitution() {
|
||||
let mut r = Refs::default();
|
||||
let mut al = [false; 129];
|
||||
let at = [false; 129];
|
||||
r.left[3] = 77;
|
||||
al[3] = true;
|
||||
substitute(&mut r, &al, &at, 4, 8);
|
||||
assert!(r.left[..=8].iter().all(|&v| v == 77));
|
||||
assert!(r.top[..=8].iter().all(|&v| v == 77));
|
||||
let mut r = Refs::default();
|
||||
substitute(&mut r, &[false; 129], &[false; 129], 4, 10);
|
||||
assert_eq!(r.top[5], 512);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,356 @@
|
|||
//! Clean-room pure-Rust HEVC (H.265) encoder, implemented from ITU-T Rec. H.265 (ISO/IEC 23008-2).
|
||||
//!
|
||||
//! Main and Main 10 profiles, 4:2:0, one slice per picture: IDR pictures and single-reference P
|
||||
//! pictures (no reordering), 64x64 CTUs with rate-distortion coding-unit decisions (8x8 .. 64x64),
|
||||
//! intra (35 modes, NxN at 8x8), merge / skip and AMVP inter prediction with quarter-sample motion,
|
||||
//! DCT / DST transforms and CABAC. The deblocking filter is on; SAO is not used.
|
||||
//!
|
||||
//! ```
|
||||
//! use effectcraft_hevcenc::{Encoder, EncoderConfig, Frame};
|
||||
//! let mut enc = Encoder::new(EncoderConfig::new(64, 48, 30, 1)).unwrap();
|
||||
//! let (y, c) = (vec![128u16; 64 * 48], vec![128u16; 32 * 24]);
|
||||
//! let packet = enc.encode(&Frame { y: &y, u: &c, v: &c, y_stride: 64, uv_stride: 32 });
|
||||
//! assert!(packet.keyframe && !packet.data.is_empty());
|
||||
//! let _hvcc = enc.hvcc();
|
||||
//! ```
|
||||
|
||||
// Index loops over fixed-size blocks read more clearly than iterator chains in codec code.
|
||||
#![allow(clippy::needless_range_loop, clippy::too_many_arguments)]
|
||||
|
||||
mod bitstream;
|
||||
mod cabac;
|
||||
mod deblock;
|
||||
mod frame;
|
||||
mod inter;
|
||||
mod intra;
|
||||
mod me;
|
||||
mod params;
|
||||
#[rustfmt::skip]
|
||||
mod tables;
|
||||
mod transform;
|
||||
|
||||
use bitstream::{BitWriter, NAL_IDR_N_LP, NAL_TRAIL_R, nal_unit};
|
||||
use frame::{FrameCoder, SliceKind};
|
||||
use params::{LOG2_MAX_POC_LSB, LOG2_MIN_CB, MAX_MERGE_CAND, SeqParams};
|
||||
|
||||
/// Profile written in the parameter sets.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Profile {
|
||||
/// Main: 8-bit 4:2:0.
|
||||
#[default]
|
||||
Main,
|
||||
/// Main 10: 10-bit 4:2:0.
|
||||
Main10,
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub fn bit_depth(self) -> u8 {
|
||||
match self {
|
||||
Profile::Main => 8,
|
||||
Profile::Main10 => 10,
|
||||
}
|
||||
}
|
||||
fn idc(self) -> u8 {
|
||||
match self {
|
||||
Profile::Main => 1,
|
||||
Profile::Main10 => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rate control.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RateControl {
|
||||
/// Constant QP (0–51; lower is better).
|
||||
ConstantQp(u8),
|
||||
/// Average bitrate target in kbit/s (frame-level QP adaptation).
|
||||
Bitrate { kbps: u32 },
|
||||
}
|
||||
|
||||
/// Encoder settings.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct EncoderConfig {
|
||||
/// Picture size; must be even (4:2:0 cropping is in units of two luma samples).
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
/// Frame rate numerator / denominator (VUI timing and rate control).
|
||||
pub fps_num: u32,
|
||||
pub fps_den: u32,
|
||||
pub profile: Profile,
|
||||
/// `general_level_idc` (30 × level, e.g. 93 = level 3.1); `None` picks the lowest level that fits.
|
||||
pub level_idc: Option<u8>,
|
||||
pub rate: RateControl,
|
||||
/// Frames between IDR pictures (1 = all intra, 0 = only the first picture).
|
||||
pub keyint: u32,
|
||||
/// BT.709 limited range is signalled in the VUI unless this is set.
|
||||
pub full_range: bool,
|
||||
}
|
||||
|
||||
impl EncoderConfig {
|
||||
pub fn new(width: u32, height: u32, fps_num: u32, fps_den: u32) -> Self {
|
||||
EncoderConfig {
|
||||
width,
|
||||
height,
|
||||
fps_num,
|
||||
fps_den,
|
||||
profile: Profile::Main,
|
||||
level_idc: None,
|
||||
rate: RateControl::ConstantQp(28),
|
||||
keyint: 60,
|
||||
full_range: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One input picture, 4:2:0, samples at the profile's bit depth (8-bit values 0–255 for Main).
|
||||
/// Luma is `width` x `height`, chroma `width / 2` x `height / 2`.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Frame<'a> {
|
||||
pub y: &'a [u16],
|
||||
pub u: &'a [u16],
|
||||
pub v: &'a [u16],
|
||||
pub y_stride: usize,
|
||||
pub uv_stride: usize,
|
||||
}
|
||||
|
||||
/// One coded picture (an access unit): NAL units, each prefixed by a 4-byte big-endian length
|
||||
/// (ISO/IEC 14496-15 sample format; parameter sets are only in [`Encoder::hvcc`]).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Packet {
|
||||
pub data: Vec<u8>,
|
||||
/// IDR picture.
|
||||
pub keyframe: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
InvalidConfig(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::InvalidConfig(s) => write!(f, "invalid HEVC encoder config: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
/// Frame-level rate control for [`RateControl::Bitrate`]: a bits ≈ X / Qstep model per picture type
|
||||
/// and a buffer that spreads over- / undershoot over the following pictures.
|
||||
struct RateCtl {
|
||||
target: f64,
|
||||
/// sum(actual) - sum(target), bits.
|
||||
buffer: f64,
|
||||
/// Complexity X = bits * Qstep for I and P pictures.
|
||||
x: [Option<f64>; 2],
|
||||
last_qp: [i32; 2],
|
||||
init_qp: i32,
|
||||
}
|
||||
|
||||
fn qstep(qp: i32) -> f64 {
|
||||
2f64.powf((qp as f64 - 4.0) / 6.0)
|
||||
}
|
||||
|
||||
impl RateCtl {
|
||||
fn new(kbps: u32, cfg: &EncoderConfig) -> Self {
|
||||
let fps = cfg.fps_num as f64 / cfg.fps_den.max(1) as f64;
|
||||
let target = kbps as f64 * 1000.0 / fps.max(0.001);
|
||||
let bpp = target / (cfg.width as f64 * cfg.height as f64);
|
||||
let init_qp = (28.0 - 6.0 * (bpp / 0.08).log2()).round().clamp(10.0, 46.0) as i32;
|
||||
RateCtl { target, buffer: 0.0, x: [None, None], last_qp: [init_qp - 3, init_qp], init_qp }
|
||||
}
|
||||
|
||||
fn qp(&self, idr: bool) -> i32 {
|
||||
let t = idr as usize ^ 1; // 0 = I, 1 = P
|
||||
let tp = (self.target - self.buffer / 12.0).clamp(0.3 * self.target, 2.0 * self.target);
|
||||
let want = if idr { 4.0 * tp } else { tp };
|
||||
let x = match (self.x[t], self.x[0]) {
|
||||
(Some(x), _) => x,
|
||||
(None, Some(xi)) => xi / 5.0,
|
||||
(None, None) => return if idr { self.init_qp - 3 } else { self.init_qp },
|
||||
};
|
||||
let qp = (4.0 + 6.0 * (x / want).log2()).round() as i32;
|
||||
qp.clamp(self.last_qp[t] - 6, self.last_qp[t] + 6).clamp(1, 51)
|
||||
}
|
||||
|
||||
fn update(&mut self, idr: bool, qp: i32, bits: f64) {
|
||||
let t = idr as usize ^ 1;
|
||||
let x = bits * qstep(qp);
|
||||
self.x[t] = Some(match self.x[t] {
|
||||
Some(old) => 0.5 * old + 0.5 * x,
|
||||
None => x,
|
||||
});
|
||||
self.last_qp[t] = qp;
|
||||
self.buffer += bits - self.target;
|
||||
}
|
||||
}
|
||||
|
||||
/// HEVC encoder. Pictures come out in input order (no reordering: I and P only).
|
||||
pub struct Encoder {
|
||||
cfg: EncoderConfig,
|
||||
seq: SeqParams,
|
||||
sets: [Vec<u8>; 3],
|
||||
frames: u64,
|
||||
/// POC of the previous picture (relative to the last IDR).
|
||||
poc: u32,
|
||||
reference: Option<[Vec<u16>; 3]>,
|
||||
recon: Option<[Vec<u16>; 3]>,
|
||||
rc: Option<RateCtl>,
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
pub fn new(cfg: EncoderConfig) -> Result<Encoder, Error> {
|
||||
if cfg.width == 0 || cfg.height == 0 {
|
||||
return Err(Error::InvalidConfig("empty frame".into()));
|
||||
}
|
||||
if !cfg.width.is_multiple_of(2) || !cfg.height.is_multiple_of(2) {
|
||||
return Err(Error::InvalidConfig(format!("{}x{}: 4:2:0 HEVC needs even dimensions", cfg.width, cfg.height)));
|
||||
}
|
||||
if cfg.width > 16384 || cfg.height > 16384 {
|
||||
return Err(Error::InvalidConfig("picture larger than 16384 samples".into()));
|
||||
}
|
||||
if cfg.fps_num == 0 || cfg.fps_den == 0 {
|
||||
return Err(Error::InvalidConfig("zero frame rate".into()));
|
||||
}
|
||||
match cfg.rate {
|
||||
RateControl::ConstantQp(q) if q > 51 => return Err(Error::InvalidConfig(format!("QP {q} > 51"))),
|
||||
RateControl::Bitrate { kbps: 0 } => return Err(Error::InvalidConfig("zero bitrate".into())),
|
||||
_ => {}
|
||||
}
|
||||
let min_cb = 1u32 << LOG2_MIN_CB;
|
||||
let (coded_width, coded_height) = (cfg.width.div_ceil(min_cb) * min_cb, cfg.height.div_ceil(min_cb) * min_cb);
|
||||
let kbps = match cfg.rate {
|
||||
RateControl::Bitrate { kbps } => Some(kbps),
|
||||
RateControl::ConstantQp(_) => None,
|
||||
};
|
||||
let level_idc = cfg.level_idc.unwrap_or_else(|| params::choose_level(coded_width, coded_height, cfg.fps_num, cfg.fps_den, kbps));
|
||||
let seq = SeqParams {
|
||||
coded_width,
|
||||
coded_height,
|
||||
width: cfg.width,
|
||||
height: cfg.height,
|
||||
bit_depth: cfg.profile.bit_depth() as u32,
|
||||
profile_idc: cfg.profile.idc(),
|
||||
level_idc,
|
||||
full_range: cfg.full_range,
|
||||
fps_num: cfg.fps_num,
|
||||
fps_den: cfg.fps_den,
|
||||
};
|
||||
let sets = [params::vps(&seq), params::sps(&seq), params::pps()];
|
||||
let rc = kbps.map(|k| RateCtl::new(k, &cfg));
|
||||
Ok(Encoder { cfg, seq, sets, frames: 0, poc: 0, reference: None, recon: None, rc })
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &EncoderConfig {
|
||||
&self.cfg
|
||||
}
|
||||
|
||||
/// VPS, SPS, PPS NAL units (2-byte NAL header, emulation prevention, no start codes or length
|
||||
/// prefixes).
|
||||
pub fn parameter_sets(&self) -> [Vec<u8>; 3] {
|
||||
self.sets.clone()
|
||||
}
|
||||
|
||||
/// HEVCDecoderConfigurationRecord (`hvcC` payload) with the parameter sets.
|
||||
pub fn hvcc(&self) -> Vec<u8> {
|
||||
params::hvcc(&self.seq, &self.sets)
|
||||
}
|
||||
|
||||
/// `general_level_idc` written in the parameter sets.
|
||||
pub fn level_idc(&self) -> u8 {
|
||||
self.seq.level_idc
|
||||
}
|
||||
|
||||
/// Reconstruction of the last encoded picture (what a decoder outputs), cropped planes
|
||||
/// `[Y, Cb, Cr]` with strides `width` and `width / 2`. For tests.
|
||||
#[doc(hidden)]
|
||||
pub fn last_reconstruction(&self) -> Option<&[Vec<u16>; 3]> {
|
||||
self.recon.as_ref()
|
||||
}
|
||||
|
||||
/// Copy the input into coded-size planes, replicating the right / bottom edges.
|
||||
fn pad_input(&self, f: &Frame) -> [Vec<u16>; 3] {
|
||||
let max = (1u32 << self.seq.bit_depth) - 1;
|
||||
let mut out: [Vec<u16>; 3] = Default::default();
|
||||
for (c, plane) in out.iter_mut().enumerate() {
|
||||
let (src, stride, w, h, cw, ch) = if c == 0 {
|
||||
(f.y, f.y_stride, self.seq.width, self.seq.height, self.seq.coded_width, self.seq.coded_height)
|
||||
} else {
|
||||
let s = if c == 1 { f.u } else { f.v };
|
||||
(s, f.uv_stride, self.seq.width / 2, self.seq.height / 2, self.seq.coded_width / 2, self.seq.coded_height / 2)
|
||||
};
|
||||
let (w, h, cw, ch) = (w as usize, h as usize, cw as usize, ch as usize);
|
||||
assert!(stride >= w && src.len() >= (h - 1) * stride + w, "HEVC encoder: plane {c} smaller than {w}x{h} (stride {stride})");
|
||||
plane.reserve(cw * ch);
|
||||
for y in 0..ch {
|
||||
let row = &src[y.min(h - 1) * stride..][..w];
|
||||
plane.extend(row.iter().map(|&v| (v as u32).min(max) as u16));
|
||||
let last = *plane.last().unwrap();
|
||||
plane.extend(std::iter::repeat_n(last, cw - w));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Encodes one picture.
|
||||
pub fn encode(&mut self, frame: &Frame) -> Packet {
|
||||
let src = self.pad_input(frame);
|
||||
let keyint = self.cfg.keyint as u64;
|
||||
let idr = self.reference.is_none() || (keyint > 0 && self.frames.is_multiple_of(keyint));
|
||||
self.poc = if idr { 0 } else { self.poc + 1 };
|
||||
let qp = match (&self.rc, self.cfg.rate) {
|
||||
(Some(rc), _) => rc.qp(idr),
|
||||
(None, RateControl::ConstantQp(q)) => q as i32,
|
||||
(None, RateControl::Bitrate { .. }) => 28,
|
||||
};
|
||||
let (w, h) = (self.seq.coded_width as usize, self.seq.coded_height as usize);
|
||||
let bd = self.seq.bit_depth;
|
||||
let kind = if idr { SliceKind::I } else { SliceKind::P };
|
||||
let me = if idr { None } else { self.reference.as_ref().map(|r| me::MeRef::new(&r[0], w, h, bd)) };
|
||||
let refp = if idr { None } else { self.reference.as_ref() };
|
||||
let mut fc = FrameCoder::new(bd, w, h, kind, qp, &src, refp, me.as_ref());
|
||||
|
||||
// slice_segment_header() (7.3.6.1)
|
||||
let mut bw = BitWriter::new();
|
||||
bw.put(1, 1); // first_slice_segment_in_pic_flag
|
||||
if idr {
|
||||
bw.put(0, 1); // no_output_of_prior_pics_flag
|
||||
}
|
||||
bw.ue(0); // slice_pic_parameter_set_id
|
||||
bw.ue(if idr { 2 } else { 1 }); // slice_type I / P
|
||||
if !idr {
|
||||
bw.put(self.poc & ((1 << LOG2_MAX_POC_LSB) - 1), LOG2_MAX_POC_LSB); // slice_pic_order_cnt_lsb
|
||||
bw.put(1, 1); // short_term_ref_pic_set_sps_flag (the only SPS set, no index coded)
|
||||
bw.put(0, 1); // num_ref_idx_active_override_flag
|
||||
bw.ue(5 - MAX_MERGE_CAND as u32); // five_minus_max_num_merge_cand
|
||||
}
|
||||
bw.se(qp - 26); // slice_qp_delta (init_qp = 26)
|
||||
// byte_alignment()
|
||||
bw.put(1, 1);
|
||||
bw.align_zero();
|
||||
|
||||
let ctx = cabac::init_contexts(qp, if idr { 0 } else { 1 });
|
||||
let mut enc = cabac::CabacEncoder::new(bw, ctx);
|
||||
fc.encode(&mut enc);
|
||||
fc.deblock();
|
||||
let rbsp = enc.finish().into_bytes();
|
||||
let nal = nal_unit(if idr { NAL_IDR_N_LP } else { NAL_TRAIL_R }, &rbsp);
|
||||
let mut data = Vec::with_capacity(nal.len() + 4);
|
||||
data.extend_from_slice(&(nal.len() as u32).to_be_bytes());
|
||||
data.extend_from_slice(&nal);
|
||||
|
||||
if let Some(rc) = &mut self.rc {
|
||||
rc.update(idr, qp, data.len() as f64 * 8.0);
|
||||
}
|
||||
// output picture: cropped reconstruction
|
||||
let rec = std::mem::take(&mut fc.rec);
|
||||
let (ow, oh) = (self.seq.width as usize, self.seq.height as usize);
|
||||
let crop = |p: &[u16], stride: usize, w: usize, h: usize| -> Vec<u16> { (0..h).flat_map(|y| p[y * stride..y * stride + w].iter().copied()).collect() };
|
||||
self.recon = Some([crop(&rec[0], w, ow, oh), crop(&rec[1], w / 2, ow / 2, oh / 2), crop(&rec[2], w / 2, ow / 2, oh / 2)]);
|
||||
self.reference = Some(rec);
|
||||
self.frames += 1;
|
||||
Packet { data, keyframe: idr }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
//! Motion search reference: the previous reconstructed luma plane padded by edge replication, plus
|
||||
//! half-sample planes interpolated with the 8-tap half-sample filter. Quarter-sample positions are
|
||||
//! approximated by averaging neighbouring half-grid samples. Used for estimation only; the coded
|
||||
//! prediction always uses the normative interpolation in `inter`.
|
||||
|
||||
use crate::tables::LUMA_FILTER;
|
||||
|
||||
const MARGIN: i32 = 80;
|
||||
|
||||
pub struct MeRef {
|
||||
/// [integer, horizontal half, vertical half, diagonal half] samples, each `stride` x `rows`.
|
||||
planes: [Vec<u16>; 4],
|
||||
stride: usize,
|
||||
rows: usize,
|
||||
w: i32,
|
||||
h: i32,
|
||||
}
|
||||
|
||||
impl MeRef {
|
||||
pub fn new(y: &[u16], w: usize, h: usize, bd: u32) -> MeRef {
|
||||
let m = MARGIN as usize;
|
||||
let (stride, rows) = (w + 2 * m, h + 2 * m);
|
||||
let mut pad = vec![0u16; stride * rows];
|
||||
for r in 0..rows {
|
||||
let sy = (r as i32 - MARGIN).clamp(0, h as i32 - 1) as usize;
|
||||
let src = &y[sy * w..sy * w + w];
|
||||
let row = &mut pad[r * stride..r * stride + stride];
|
||||
row[..m].fill(src[0]);
|
||||
row[m..m + w].copy_from_slice(src);
|
||||
row[m + w..].fill(src[w - 1]);
|
||||
}
|
||||
let max = (1i32 << bd) - 1;
|
||||
let f = &LUMA_FILTER[2];
|
||||
let filt_h = |p: &[u16]| -> Vec<u16> {
|
||||
let mut o = vec![0u16; stride * rows];
|
||||
for r in 0..rows {
|
||||
let row = &p[r * stride..r * stride + stride];
|
||||
for x in 0..stride {
|
||||
let mut s = 0i32;
|
||||
for k in 0..8 {
|
||||
let xx = (x as i32 - 3 + k as i32).clamp(0, stride as i32 - 1) as usize;
|
||||
s += f[k] as i32 * row[xx] as i32;
|
||||
}
|
||||
o[r * stride + x] = ((s + 32) >> 6).clamp(0, max) as u16;
|
||||
}
|
||||
}
|
||||
o
|
||||
};
|
||||
let filt_v = |p: &[u16]| -> Vec<u16> {
|
||||
let mut o = vec![0u16; stride * rows];
|
||||
let mut acc = vec![0i32; stride];
|
||||
for r in 0..rows {
|
||||
acc.fill(0);
|
||||
for k in 0..8 {
|
||||
let rr = (r as i32 - 3 + k as i32).clamp(0, rows as i32 - 1) as usize;
|
||||
let src = &p[rr * stride..rr * stride + stride];
|
||||
let coef = f[k] as i32;
|
||||
for (a, &s) in acc.iter_mut().zip(src) {
|
||||
*a += coef * s as i32;
|
||||
}
|
||||
}
|
||||
for (d, &a) in o[r * stride..r * stride + stride].iter_mut().zip(&acc) {
|
||||
*d = ((a + 32) >> 6).clamp(0, max) as u16;
|
||||
}
|
||||
}
|
||||
o
|
||||
};
|
||||
let hp = filt_h(&pad);
|
||||
let vp = filt_v(&pad);
|
||||
let dp = filt_v(&hp);
|
||||
MeRef { planes: [pad, hp, vp, dp], stride, rows, w: w as i32, h: h as i32 }
|
||||
}
|
||||
|
||||
/// Integer motion vector range keeping an n x n block (plus filter margin) inside the padding.
|
||||
pub fn int_range(&self, x: i32, y: i32, n: usize) -> (i32, i32, i32, i32) {
|
||||
let n = n as i32;
|
||||
let lo_x = -MARGIN + 4 - x;
|
||||
let hi_x = self.w + MARGIN - n - 4 - x;
|
||||
let lo_y = -MARGIN + 4 - y;
|
||||
let hi_y = self.h + MARGIN - n - 4 - y;
|
||||
(lo_x.max(-256), hi_x.min(256), lo_y.max(-256), hi_y.min(256))
|
||||
}
|
||||
|
||||
/// SAD of the source block against the integer-position reference block at (rx, ry).
|
||||
pub fn sad(&self, src: &[u16], ss: usize, rx: i32, ry: i32, n: usize) -> u32 {
|
||||
let p = &self.planes[0];
|
||||
let base = (ry + MARGIN) as usize * self.stride + (rx + MARGIN) as usize;
|
||||
let mut s = 0u32;
|
||||
for r in 0..n {
|
||||
let a = &src[r * ss..r * ss + n];
|
||||
let b = &p[base + r * self.stride..base + r * self.stride + n];
|
||||
s += a.iter().zip(b).map(|(&x, &y)| (x as i32 - y as i32).unsigned_abs()).sum::<u32>();
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Approximate luma prediction of an n x n block at (x, y) with quarter-sample vector `mv`.
|
||||
pub fn approx_pred(&self, x: i32, y: i32, n: usize, mv: [i16; 2], out: &mut [u16]) {
|
||||
// per axis: one or two (half-sample fraction, integer offset) taps
|
||||
let axis = |v: i32| -> [(usize, i32); 2] {
|
||||
if v & 1 == 0 {
|
||||
let hv = v >> 1;
|
||||
[((hv & 1) as usize, hv >> 1); 2]
|
||||
} else {
|
||||
let (a, b) = ((v - 1) >> 1, (v + 1) >> 1);
|
||||
[((a & 1) as usize, a >> 1), ((b & 1) as usize, b >> 1)]
|
||||
}
|
||||
};
|
||||
let (mx, my) = (mv[0] as i32, mv[1] as i32);
|
||||
let ax = axis(mx);
|
||||
let ay = axis(my);
|
||||
let two = mx & 1 != 0 || my & 1 != 0;
|
||||
let taps = [(ax[0], ay[0]), (ax[1], ay[1])];
|
||||
let ntaps = if two { 2 } else { 1 };
|
||||
let mut first = true;
|
||||
for &((fx, ox), (fy, oy)) in taps.iter().take(ntaps) {
|
||||
let plane = &self.planes[fx + 2 * fy];
|
||||
let (bx, by) = (x + ox + MARGIN, y + oy + MARGIN);
|
||||
let inside = bx >= 0 && by >= 0 && bx as usize + n <= self.stride && by as usize + n <= self.rows;
|
||||
for r in 0..n {
|
||||
let dst = &mut out[r * n..r * n + n];
|
||||
if inside {
|
||||
let o = (by as usize + r) * self.stride + bx as usize;
|
||||
let src = &plane[o..o + n];
|
||||
if first {
|
||||
dst.copy_from_slice(src);
|
||||
} else {
|
||||
for (d, &s) in dst.iter_mut().zip(src) {
|
||||
*d = ((*d as u32 + s as u32 + 1) >> 1) as u16;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let yy = (by + r as i32).clamp(0, self.rows as i32 - 1) as usize;
|
||||
for (c, d) in dst.iter_mut().enumerate() {
|
||||
let xx = (bx + c as i32).clamp(0, self.stride as i32 - 1) as usize;
|
||||
let s = plane[yy * self.stride + xx];
|
||||
*d = if first { s } else { ((*d as u32 + s as u32 + 1) >> 1) as u16 };
|
||||
}
|
||||
}
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
//! Parameter sets (7.3.2.1 VPS, 7.3.2.2 SPS with E.2.1 VUI, 7.3.2.3 PPS), profile_tier_level (7.3.3),
|
||||
//! level selection (Annex A, Table A.8) and the `hvcC` record (ISO/IEC 14496-15 §8.3.3.1).
|
||||
|
||||
use crate::bitstream::{BitWriter, NAL_PPS, NAL_SPS, NAL_VPS, nal_unit};
|
||||
|
||||
/// Fixed coding structure of the encoder.
|
||||
pub const LOG2_CTB: u32 = 6;
|
||||
pub const LOG2_MIN_CB: u32 = 3;
|
||||
pub const LOG2_MIN_TB: u32 = 2;
|
||||
pub const LOG2_MAX_TB: u32 = 5;
|
||||
pub const LOG2_MAX_POC_LSB: u32 = 8;
|
||||
/// MaxNumMergeCand.
|
||||
pub const MAX_MERGE_CAND: usize = 5;
|
||||
|
||||
/// Sequence-level settings shared by the parameter-set writers.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SeqParams {
|
||||
/// Coded (padded) size, multiples of the minimum CB size.
|
||||
pub coded_width: u32,
|
||||
pub coded_height: u32,
|
||||
/// Output (cropped) size.
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub bit_depth: u32,
|
||||
pub profile_idc: u8,
|
||||
pub level_idc: u8,
|
||||
pub full_range: bool,
|
||||
pub fps_num: u32,
|
||||
pub fps_den: u32,
|
||||
}
|
||||
|
||||
/// (level_idc, MaxLumaPs, MaxLumaSr, MaxBR main tier in kbit/s) of Table A.8 (and A.9 for luma rates).
|
||||
const LEVELS: [(u8, u64, u64, u64); 13] = [
|
||||
(30, 36_864, 552_960, 128),
|
||||
(60, 122_880, 3_686_400, 1_500),
|
||||
(63, 245_760, 7_372_800, 3_000),
|
||||
(90, 552_960, 16_588_800, 6_000),
|
||||
(93, 983_040, 33_177_600, 10_000),
|
||||
(120, 2_228_224, 66_846_720, 12_000),
|
||||
(123, 2_228_224, 133_693_440, 20_000),
|
||||
(150, 8_912_896, 267_386_880, 25_000),
|
||||
(153, 8_912_896, 534_773_760, 40_000),
|
||||
(156, 8_912_896, 1_069_547_520, 60_000),
|
||||
(180, 35_651_584, 1_069_547_520, 60_000),
|
||||
(183, 35_651_584, 2_139_095_040, 120_000),
|
||||
(186, 35_651_584, 4_278_190_080, 240_000),
|
||||
];
|
||||
|
||||
/// Lowest level whose picture size, sample rate and (when known) bitrate limits fit.
|
||||
pub fn choose_level(width: u32, height: u32, fps_num: u32, fps_den: u32, kbps: Option<u32>) -> u8 {
|
||||
let ps = width as u64 * height as u64;
|
||||
let sr = (ps as f64 * fps_num as f64 / fps_den.max(1) as f64).ceil() as u64;
|
||||
for &(idc, max_ps, max_sr, max_br) in &LEVELS {
|
||||
let max_dim = ((max_ps * 8) as f64).sqrt() as u64;
|
||||
if ps <= max_ps && (width as u64) <= max_dim && (height as u64) <= max_dim && sr <= max_sr && kbps.is_none_or(|k| k as u64 <= max_br) {
|
||||
return idc;
|
||||
}
|
||||
}
|
||||
186
|
||||
}
|
||||
|
||||
fn profile_tier_level(w: &mut BitWriter, p: &SeqParams) {
|
||||
w.put(0, 2); // general_profile_space
|
||||
w.put(0, 1); // general_tier_flag (Main tier)
|
||||
w.put(p.profile_idc as u32, 5);
|
||||
w.put(compat_flags(p.profile_idc), 32);
|
||||
w.put(1, 1); // general_progressive_source_flag
|
||||
w.put(0, 1); // general_interlaced_source_flag
|
||||
w.put(0, 1); // general_non_packed_constraint_flag
|
||||
w.put(1, 1); // general_frame_only_constraint_flag
|
||||
w.put(0, 32); // general_reserved_zero_43bits ...
|
||||
w.put(0, 11);
|
||||
w.put(0, 1); // general_inbld_flag / reserved
|
||||
w.put(p.level_idc as u32, 8);
|
||||
// sps_max_sub_layers_minus1 == 0: no sub-layer flags
|
||||
}
|
||||
|
||||
/// general_profile_compatibility_flag[j] as a 32-bit word (flag 0 is the MSB). A Main stream is also
|
||||
/// Main 10 compatible.
|
||||
fn compat_flags(profile_idc: u8) -> u32 {
|
||||
let mut f = 1u32 << (31 - profile_idc as u32);
|
||||
if profile_idc == 1 {
|
||||
f |= 1 << (31 - 2);
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
pub fn vps(p: &SeqParams) -> Vec<u8> {
|
||||
let mut w = BitWriter::new();
|
||||
w.put(0, 4); // vps_video_parameter_set_id
|
||||
w.put(1, 1); // vps_base_layer_internal_flag
|
||||
w.put(1, 1); // vps_base_layer_available_flag
|
||||
w.put(0, 6); // vps_max_layers_minus1
|
||||
w.put(0, 3); // vps_max_sub_layers_minus1
|
||||
w.put(1, 1); // vps_temporal_id_nesting_flag
|
||||
w.put(0xffff, 16);
|
||||
profile_tier_level(&mut w, p);
|
||||
w.put(1, 1); // vps_sub_layer_ordering_info_present_flag
|
||||
w.ue(1); // vps_max_dec_pic_buffering_minus1
|
||||
w.ue(0); // vps_max_num_reorder_pics
|
||||
w.ue(0); // vps_max_latency_increase_plus1
|
||||
w.put(0, 6); // vps_max_layer_id
|
||||
w.ue(0); // vps_num_layer_sets_minus1
|
||||
w.put(0, 1); // vps_timing_info_present_flag
|
||||
w.put(0, 1); // vps_extension_flag
|
||||
w.trailing();
|
||||
nal_unit(NAL_VPS, &w.into_bytes())
|
||||
}
|
||||
|
||||
pub fn sps(p: &SeqParams) -> Vec<u8> {
|
||||
let mut w = BitWriter::new();
|
||||
w.put(0, 4); // sps_video_parameter_set_id
|
||||
w.put(0, 3); // sps_max_sub_layers_minus1
|
||||
w.put(1, 1); // sps_temporal_id_nesting_flag
|
||||
profile_tier_level(&mut w, p);
|
||||
w.ue(0); // sps_seq_parameter_set_id
|
||||
w.ue(1); // chroma_format_idc 4:2:0
|
||||
w.ue(p.coded_width);
|
||||
w.ue(p.coded_height);
|
||||
let crop = p.coded_width != p.width || p.coded_height != p.height;
|
||||
w.flag(crop); // conformance_window_flag
|
||||
if crop {
|
||||
// offsets in chroma sample units (SubWidthC = SubHeightC = 2)
|
||||
w.ue(0);
|
||||
w.ue((p.coded_width - p.width) / 2);
|
||||
w.ue(0);
|
||||
w.ue((p.coded_height - p.height) / 2);
|
||||
}
|
||||
w.ue(p.bit_depth - 8); // bit_depth_luma_minus8
|
||||
w.ue(p.bit_depth - 8); // bit_depth_chroma_minus8
|
||||
w.ue(LOG2_MAX_POC_LSB - 4);
|
||||
w.put(1, 1); // sps_sub_layer_ordering_info_present_flag
|
||||
w.ue(1); // sps_max_dec_pic_buffering_minus1
|
||||
w.ue(0); // sps_max_num_reorder_pics
|
||||
w.ue(0); // sps_max_latency_increase_plus1
|
||||
w.ue(LOG2_MIN_CB - 3);
|
||||
w.ue(LOG2_CTB - LOG2_MIN_CB);
|
||||
w.ue(LOG2_MIN_TB - 2);
|
||||
w.ue(LOG2_MAX_TB - LOG2_MIN_TB);
|
||||
w.ue(0); // max_transform_hierarchy_depth_inter
|
||||
w.ue(0); // max_transform_hierarchy_depth_intra
|
||||
w.put(0, 1); // scaling_list_enabled_flag
|
||||
w.put(0, 1); // amp_enabled_flag
|
||||
w.put(0, 1); // sample_adaptive_offset_enabled_flag
|
||||
w.put(0, 1); // pcm_enabled_flag
|
||||
w.ue(1); // num_short_term_ref_pic_sets
|
||||
// st_ref_pic_set(0): one negative picture at delta POC -1, used by the current picture
|
||||
w.ue(1); // num_negative_pics
|
||||
w.ue(0); // num_positive_pics
|
||||
w.ue(0); // delta_poc_s0_minus1
|
||||
w.put(1, 1); // used_by_curr_pic_s0_flag
|
||||
w.put(0, 1); // long_term_ref_pics_present_flag
|
||||
w.put(0, 1); // sps_temporal_mvp_enabled_flag
|
||||
w.put(1, 1); // strong_intra_smoothing_enabled_flag
|
||||
w.put(1, 1); // vui_parameters_present_flag
|
||||
vui(&mut w, p);
|
||||
w.put(0, 1); // sps_extension_present_flag
|
||||
w.trailing();
|
||||
nal_unit(NAL_SPS, &w.into_bytes())
|
||||
}
|
||||
|
||||
fn vui(w: &mut BitWriter, p: &SeqParams) {
|
||||
w.put(0, 1); // aspect_ratio_info_present_flag
|
||||
w.put(0, 1); // overscan_info_present_flag
|
||||
w.put(1, 1); // video_signal_type_present_flag
|
||||
w.put(5, 3); // video_format: unspecified
|
||||
w.flag(p.full_range);
|
||||
w.put(1, 1); // colour_description_present_flag
|
||||
w.put(1, 8); // colour_primaries BT.709
|
||||
w.put(1, 8); // transfer_characteristics BT.709
|
||||
w.put(1, 8); // matrix_coeffs BT.709
|
||||
w.put(0, 1); // chroma_loc_info_present_flag
|
||||
w.put(0, 1); // neutral_chroma_indication_flag
|
||||
w.put(0, 1); // field_seq_flag
|
||||
w.put(0, 1); // frame_field_info_present_flag
|
||||
w.put(0, 1); // default_display_window_flag
|
||||
w.put(1, 1); // vui_timing_info_present_flag
|
||||
w.put(p.fps_den, 32); // vui_num_units_in_tick
|
||||
w.put(p.fps_num, 32); // vui_time_scale
|
||||
w.put(0, 1); // vui_poc_proportional_to_timing_flag
|
||||
w.put(0, 1); // vui_hrd_parameters_present_flag
|
||||
w.put(0, 1); // bitstream_restriction_flag
|
||||
}
|
||||
|
||||
pub fn pps() -> Vec<u8> {
|
||||
let mut w = BitWriter::new();
|
||||
w.ue(0); // pps_pic_parameter_set_id
|
||||
w.ue(0); // pps_seq_parameter_set_id
|
||||
w.put(0, 1); // dependent_slice_segments_enabled_flag
|
||||
w.put(0, 1); // output_flag_present_flag
|
||||
w.put(0, 3); // num_extra_slice_header_bits
|
||||
w.put(1, 1); // sign_data_hiding_enabled_flag
|
||||
w.put(0, 1); // cabac_init_present_flag
|
||||
w.ue(0); // num_ref_idx_l0_default_active_minus1
|
||||
w.ue(0); // num_ref_idx_l1_default_active_minus1
|
||||
w.se(0); // init_qp_minus26
|
||||
w.put(0, 1); // constrained_intra_pred_flag
|
||||
w.put(0, 1); // transform_skip_enabled_flag
|
||||
w.put(0, 1); // cu_qp_delta_enabled_flag
|
||||
w.se(0); // pps_cb_qp_offset
|
||||
w.se(0); // pps_cr_qp_offset
|
||||
w.put(0, 1); // pps_slice_chroma_qp_offsets_present_flag
|
||||
w.put(0, 1); // weighted_pred_flag
|
||||
w.put(0, 1); // weighted_bipred_flag
|
||||
w.put(0, 1); // transquant_bypass_enabled_flag
|
||||
w.put(0, 1); // tiles_enabled_flag
|
||||
w.put(0, 1); // entropy_coding_sync_enabled_flag
|
||||
w.put(0, 1); // pps_loop_filter_across_slices_enabled_flag
|
||||
w.put(1, 1); // deblocking_filter_control_present_flag
|
||||
w.put(0, 1); // deblocking_filter_override_enabled_flag
|
||||
w.put(0, 1); // pps_deblocking_filter_disabled_flag
|
||||
w.se(0); // pps_beta_offset_div2
|
||||
w.se(0); // pps_tc_offset_div2
|
||||
w.put(0, 1); // pps_scaling_list_data_present_flag
|
||||
w.put(0, 1); // lists_modification_present_flag
|
||||
w.ue(0); // log2_parallel_merge_level_minus2
|
||||
w.put(0, 1); // slice_segment_header_extension_present_flag
|
||||
w.put(0, 1); // pps_extension_present_flag
|
||||
w.trailing();
|
||||
nal_unit(NAL_PPS, &w.into_bytes())
|
||||
}
|
||||
|
||||
/// HEVCDecoderConfigurationRecord with one VPS, SPS and PPS (lengthSizeMinusOne = 3).
|
||||
pub fn hvcc(p: &SeqParams, sets: &[Vec<u8>; 3]) -> Vec<u8> {
|
||||
let mut o = vec![1u8]; // configurationVersion
|
||||
o.push(p.profile_idc); // profile_space 0, tier 0, profile_idc
|
||||
o.extend_from_slice(&compat_flags(p.profile_idc).to_be_bytes());
|
||||
// general_constraint_indicator_flags (48 bits): progressive, interlaced, non_packed, frame_only
|
||||
o.extend_from_slice(&[0x90, 0, 0, 0, 0, 0]);
|
||||
o.push(p.level_idc);
|
||||
o.extend_from_slice(&[0xf0, 0x00]); // reserved + min_spatial_segmentation_idc 0
|
||||
o.push(0xfc); // reserved + parallelismType 0
|
||||
o.push(0xfc | 1); // reserved + chroma_format_idc 1
|
||||
o.push(0xf8 | (p.bit_depth - 8) as u8);
|
||||
o.push(0xf8 | (p.bit_depth - 8) as u8);
|
||||
o.extend_from_slice(&[0, 0]); // avgFrameRate unspecified
|
||||
// constantFrameRate 0, numTemporalLayers 1, temporalIdNested 1, lengthSizeMinusOne 3
|
||||
o.push((1 << 3) | (1 << 2) | 3);
|
||||
o.push(3); // numOfArrays
|
||||
for (set, ty) in sets.iter().zip([NAL_VPS, NAL_SPS, NAL_PPS]) {
|
||||
o.push(0x80 | ty); // array_completeness 1
|
||||
o.extend_from_slice(&1u16.to_be_bytes());
|
||||
o.extend_from_slice(&(set.len() as u16).to_be_bytes());
|
||||
o.extend_from_slice(set);
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn levels() {
|
||||
assert_eq!(choose_level(1920, 1080, 30, 1, None), 120);
|
||||
assert_eq!(choose_level(1920, 1080, 60, 1, None), 123);
|
||||
assert_eq!(choose_level(3840, 2160, 30, 1, None), 150);
|
||||
assert_eq!(choose_level(1280, 720, 30, 1, None), 93);
|
||||
assert_eq!(choose_level(64, 64, 30, 1, None), 30);
|
||||
assert_eq!(choose_level(1920, 1080, 30, 1, Some(15_000)), 123);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
//! Constant tables of ITU-T H.265, mirrored from the first-party, spec-derived `filmcraft-hevc`
|
||||
//! decoder (same organisation, MIT OR Apache-2.0): CABAC context layout and initValues (9.3.2.2),
|
||||
//! rangeTabLPS / transIdx (Tables 9-52, 9-53), scans (6.5.3 - 6.5.5), transform matrices (8.6.4.2),
|
||||
//! intra angles (Tables 8-5, 8-6), interpolation filters (8.5.3.3.3) and the chroma QP mapping (Table 8-10).
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
/// Table 9-52 (same values as H.264) rangeTabLPS[pStateIdx][qCodIRangeIdx].
|
||||
#[rustfmt::skip]
|
||||
pub static RANGE_TAB_LPS: [[u8; 4]; 64] = [
|
||||
[128, 176, 208, 240], [128, 167, 197, 227], [128, 158, 187, 216], [123, 150, 178, 205],
|
||||
[116, 142, 169, 195], [111, 135, 160, 185], [105, 128, 152, 175], [100, 122, 144, 166],
|
||||
[95, 116, 137, 158], [90, 110, 130, 150], [85, 104, 123, 142], [81, 99, 117, 135],
|
||||
[77, 94, 111, 128], [73, 89, 105, 122], [69, 85, 100, 116], [66, 80, 95, 110],
|
||||
[62, 76, 90, 104], [59, 72, 86, 99], [56, 69, 81, 94], [53, 65, 77, 89],
|
||||
[51, 62, 73, 85], [48, 59, 69, 80], [46, 56, 66, 76], [43, 53, 63, 72],
|
||||
[41, 50, 59, 69], [39, 48, 56, 65], [37, 45, 54, 62], [35, 43, 51, 59],
|
||||
[33, 41, 48, 56], [32, 39, 46, 53], [30, 37, 43, 50], [29, 35, 41, 48],
|
||||
[27, 33, 39, 45], [26, 31, 37, 43], [24, 30, 35, 41], [23, 28, 33, 39],
|
||||
[22, 27, 32, 37], [21, 26, 30, 35], [20, 24, 29, 33], [19, 23, 27, 31],
|
||||
[18, 22, 26, 30], [17, 21, 25, 28], [16, 20, 23, 27], [15, 19, 22, 25],
|
||||
[14, 18, 21, 24], [14, 17, 20, 23], [13, 16, 19, 22], [12, 15, 18, 21],
|
||||
[12, 14, 17, 20], [11, 14, 16, 19], [11, 13, 15, 18], [10, 12, 15, 17],
|
||||
[10, 12, 14, 16], [9, 11, 13, 15], [9, 11, 12, 14], [8, 10, 12, 14],
|
||||
[8, 9, 11, 13], [7, 9, 11, 12], [7, 9, 10, 12], [7, 8, 10, 11],
|
||||
[6, 8, 9, 11], [6, 7, 9, 10], [6, 7, 8, 9], [2, 2, 2, 2],
|
||||
];
|
||||
|
||||
/// Table 9-53 transIdxLPS.
|
||||
#[rustfmt::skip]
|
||||
static TRANS_IDX_LPS: [u8; 64] = [
|
||||
0, 0, 1, 2, 2, 4, 4, 5, 6, 7, 8, 9, 9, 11, 11, 12,
|
||||
13, 13, 15, 15, 16, 16, 18, 18, 19, 19, 21, 21, 22, 22, 23, 24,
|
||||
24, 25, 26, 26, 27, 27, 28, 29, 29, 30, 30, 30, 31, 32, 32, 33,
|
||||
33, 33, 34, 34, 35, 35, 35, 36, 36, 36, 37, 37, 37, 38, 38, 63,
|
||||
];
|
||||
|
||||
/// Table 9-53 transIdxMPS.
|
||||
#[rustfmt::skip]
|
||||
static TRANS_IDX_MPS: [u8; 64] = [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
||||
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
|
||||
33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
|
||||
49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 62, 63,
|
||||
];
|
||||
|
||||
/// Combined state transition: index = (pStateIdx << 1 | valMPS) * 2 + bin_is_lps.
|
||||
/// Each context is stored as `pStateIdx << 1 | valMPS`.
|
||||
pub static NEXT_STATE: [[u8; 2]; 128] = {
|
||||
let mut t = [[0u8; 2]; 128];
|
||||
let mut s = 0;
|
||||
while s < 128 {
|
||||
let p = s >> 1;
|
||||
let mps = s & 1;
|
||||
// MPS path
|
||||
t[s][0] = (TRANS_IDX_MPS[p] << 1) | mps as u8;
|
||||
// LPS path
|
||||
let new_mps = if p == 0 { 1 - mps } else { mps };
|
||||
t[s][1] = (TRANS_IDX_LPS[p] << 1) | new_mps as u8;
|
||||
s += 1;
|
||||
}
|
||||
t
|
||||
};
|
||||
|
||||
/// Up-right diagonal scan (6.5.3) of a `N x N` block as (x, y) pairs.
|
||||
const fn diag<const L: usize>(n: usize) -> [(u8, u8); L] {
|
||||
let mut out = [(0u8, 0u8); L];
|
||||
let mut i = 0;
|
||||
let mut x: i32 = 0;
|
||||
let mut y: i32 = 0;
|
||||
while i < L {
|
||||
while y >= 0 {
|
||||
if (x as usize) < n && (y as usize) < n {
|
||||
out[i] = (x as u8, y as u8);
|
||||
i += 1;
|
||||
}
|
||||
y -= 1;
|
||||
x += 1;
|
||||
}
|
||||
y = x;
|
||||
x = 0;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Horizontal (6.5.4, `vertical == false`) or vertical (6.5.5) scan.
|
||||
const fn hv<const L: usize>(n: usize, vertical: bool) -> [(u8, u8); L] {
|
||||
let mut out = [(0u8, 0u8); L];
|
||||
let mut i = 0;
|
||||
while i < L {
|
||||
let (a, b) = ((i % n) as u8, (i / n) as u8);
|
||||
out[i] = if vertical { (b, a) } else { (a, b) };
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub static SCAN_1: [[(u8, u8); 1]; 3] = [[(0, 0)], [(0, 0)], [(0, 0)]];
|
||||
pub static SCAN_2: [[(u8, u8); 4]; 3] = [diag::<4>(2), hv::<4>(2, false), hv::<4>(2, true)];
|
||||
pub static SCAN_4: [[(u8, u8); 16]; 3] = [diag::<16>(4), hv::<16>(4, false), hv::<16>(4, true)];
|
||||
pub static SCAN_8: [[(u8, u8); 64]; 3] = [diag::<64>(8), hv::<64>(8, false), hv::<64>(8, true)];
|
||||
|
||||
/// ScanOrder[log2BlockSize][scanIdx] (log2BlockSize 0..=3).
|
||||
#[inline]
|
||||
pub fn scan_order(log2: u32, scan_idx: usize) -> &'static [(u8, u8)] {
|
||||
match log2 {
|
||||
0 => &SCAN_1[scan_idx],
|
||||
1 => &SCAN_2[scan_idx],
|
||||
2 => &SCAN_4[scan_idx],
|
||||
_ => &SCAN_8[scan_idx],
|
||||
}
|
||||
}
|
||||
|
||||
/// Up-right diagonal scan for log2 size 2 or 3.
|
||||
pub fn scan_diag(log2: u32) -> &'static [(u8, u8)] {
|
||||
scan_order(log2, 0)
|
||||
}
|
||||
|
||||
/// Table 8-10: QpC as a function of qPi (ChromaArrayType 1).
|
||||
#[inline]
|
||||
pub fn qpc_420(qpi: i32) -> i32 {
|
||||
if qpi < 30 {
|
||||
qpi
|
||||
} else if qpi > 43 {
|
||||
qpi - 6
|
||||
} else {
|
||||
QPC_30_43[(qpi - 30) as usize] as i32
|
||||
}
|
||||
}
|
||||
|
||||
/// Table 9-50 ctxIdxMap for 4x4 sig_coeff_flag contexts.
|
||||
pub static CTX_IDX_MAP: [u8; 16] = [0, 1, 4, 5, 2, 3, 4, 5, 6, 6, 8, 8, 7, 7, 8, 8];
|
||||
|
||||
/// levelScale (8-309).
|
||||
pub static LEVEL_SCALE: [i32; 6] = [40, 45, 51, 57, 64, 72];
|
||||
|
||||
/// DST-VII 4x4 matrix (8.6.4.2, trType 1).
|
||||
pub static DST_MATRIX: [[i8; 4]; 4] = [[29, 55, 74, 84], [74, 74, 0, -74], [84, -29, -74, 55], [55, -84, 74, -29]];
|
||||
|
||||
|
||||
// CABAC context layout: offset of each syntax element in the context array (Table 9-4).
|
||||
pub const SAO_MERGE: usize = 0;
|
||||
pub const SAO_TYPE: usize = 1;
|
||||
pub const SPLIT_CU: usize = 2;
|
||||
pub const CU_TRANSQUANT_BYPASS: usize = 5;
|
||||
pub const CU_SKIP: usize = 6;
|
||||
pub const PRED_MODE: usize = 9;
|
||||
pub const PART_MODE: usize = 10;
|
||||
pub const PREV_INTRA_LUMA: usize = 14;
|
||||
pub const INTRA_CHROMA: usize = 15;
|
||||
pub const RQT_ROOT_CBF: usize = 16;
|
||||
pub const MERGE_FLAG: usize = 17;
|
||||
pub const MERGE_IDX: usize = 18;
|
||||
pub const INTER_PRED_IDC: usize = 19;
|
||||
pub const REF_IDX: usize = 24;
|
||||
pub const MVP_FLAG: usize = 26;
|
||||
pub const SPLIT_TRANSFORM: usize = 27;
|
||||
pub const CBF_LUMA: usize = 30;
|
||||
pub const CBF_CHROMA: usize = 32;
|
||||
pub const ABS_MVD_GT0: usize = 37;
|
||||
pub const ABS_MVD_GT1: usize = 38;
|
||||
pub const CU_QP_DELTA_ABS: usize = 39;
|
||||
pub const TRANSFORM_SKIP: usize = 41;
|
||||
pub const LAST_X_PREFIX: usize = 43;
|
||||
pub const LAST_Y_PREFIX: usize = 61;
|
||||
pub const CODED_SUB_BLOCK: usize = 79;
|
||||
pub const SIG_COEFF: usize = 83;
|
||||
pub const GT1: usize = 127;
|
||||
pub const GT2: usize = 151;
|
||||
pub const EXPLICIT_RDPCM: usize = 157;
|
||||
pub const EXPLICIT_RDPCM_DIR: usize = 159;
|
||||
pub const CU_CHROMA_QP_OFFSET_FLAG: usize = 161;
|
||||
pub const CU_CHROMA_QP_OFFSET_IDX: usize = 162;
|
||||
pub const NUM_CTX: usize = 163;
|
||||
|
||||
/// initValue per initType (9.3.2.2).
|
||||
pub static CABAC_INIT: [[u8; NUM_CTX]; 3] = [
|
||||
[153, 200, 139, 141, 157, 154, 154, 154, 154, 154, 184, 154, 154, 154, 184, 63, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 153, 138, 138, 111, 141, 94, 138, 182, 154, 154, 154, 154, 154, 154, 139, 139, 110, 110, 124, 125, 140, 153, 125, 127, 140, 109, 111, 143, 127, 111, 79, 108, 123, 63, 110, 110, 124, 125, 140, 153, 125, 127, 140, 109, 111, 143, 127, 111, 79, 108, 123, 63, 91, 171, 134, 141, 111, 111, 125, 110, 110, 94, 124, 108, 124, 107, 125, 141, 179, 153, 125, 107, 125, 141, 179, 153, 125, 107, 125, 141, 179, 153, 125, 140, 139, 182, 182, 152, 136, 152, 136, 153, 136, 139, 111, 136, 139, 111, 141, 111, 140, 92, 137, 138, 140, 152, 138, 139, 153, 74, 149, 92, 139, 107, 122, 152, 140, 179, 166, 182, 140, 227, 122, 197, 138, 153, 136, 167, 152, 152, 154, 154, 154, 154, 154, 154],
|
||||
[153, 185, 107, 139, 126, 154, 197, 185, 201, 149, 154, 139, 154, 154, 154, 152, 79, 110, 122, 95, 79, 63, 31, 31, 153, 153, 168, 124, 138, 94, 153, 111, 149, 107, 167, 154, 154, 140, 198, 154, 154, 139, 139, 125, 110, 94, 110, 95, 79, 125, 111, 110, 78, 110, 111, 111, 95, 94, 108, 123, 108, 125, 110, 94, 110, 95, 79, 125, 111, 110, 78, 110, 111, 111, 95, 94, 108, 123, 108, 121, 140, 61, 154, 155, 154, 139, 153, 139, 123, 123, 63, 153, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 170, 153, 123, 123, 107, 121, 107, 121, 167, 151, 183, 140, 151, 183, 140, 140, 140, 154, 196, 196, 167, 154, 152, 167, 182, 182, 134, 149, 136, 153, 121, 136, 137, 169, 194, 166, 167, 154, 167, 137, 182, 107, 167, 91, 122, 107, 167, 139, 139, 139, 139, 154, 154],
|
||||
[153, 160, 107, 139, 126, 154, 197, 185, 201, 134, 154, 139, 154, 154, 183, 152, 79, 154, 137, 95, 79, 63, 31, 31, 153, 153, 168, 224, 167, 122, 153, 111, 149, 92, 167, 154, 154, 169, 198, 154, 154, 139, 139, 125, 110, 124, 110, 95, 94, 125, 111, 111, 79, 125, 126, 111, 111, 79, 108, 123, 93, 125, 110, 124, 110, 95, 94, 125, 111, 111, 79, 125, 126, 111, 111, 79, 108, 123, 93, 121, 140, 61, 154, 170, 154, 139, 153, 139, 123, 123, 63, 124, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 170, 153, 138, 138, 122, 121, 122, 121, 167, 151, 183, 140, 151, 183, 140, 140, 140, 154, 196, 167, 167, 154, 152, 167, 182, 182, 134, 149, 136, 153, 121, 136, 122, 169, 208, 166, 167, 154, 152, 167, 182, 107, 167, 91, 107, 107, 167, 139, 139, 139, 139, 154, 154],
|
||||
];
|
||||
|
||||
|
||||
/// Table 8-5: intraPredAngle for predModeIntra 0..34 (0 for planar / DC).
|
||||
pub static INTRA_PRED_ANGLE: [i8; 35] = [0, 0, 32, 26, 21, 17, 13, 9, 5, 2, 0, -2, -5, -9, -13, -17, -21, -26, -32, -26, -21, -17, -13, -9, -5, -2, 0, 2, 5, 9, 13, 17, 21, 26, 32];
|
||||
/// Table 8-6: invAngle for predModeIntra 11..25 (index = mode - 11).
|
||||
pub static INV_ANGLE: [i16; 15] = [-4096, -1638, -910, -630, -482, -390, -315, -256, -315, -390, -482, -630, -910, -1638, -4096];
|
||||
|
||||
/// Table 8-10: QpC for qPi = 30..43 (qPi < 30: QpC = qPi; qPi > 43: QpC = qPi - 6).
|
||||
pub static QPC_30_43: [u8; 14] = [29, 30, 31, 32, 33, 33, 34, 34, 35, 35, 36, 36, 37, 37];
|
||||
|
||||
|
||||
/// Luma 8-tap interpolation filter fL[xFrac][k] (8-236..8-238), taps at offsets -3..4.
|
||||
pub static LUMA_FILTER: [[i8; 8]; 4] = [[0, 0, 0, 64, 0, 0, 0, 0], [-1, 4, -10, 58, 17, -5, 1, 0], [-1, 4, -11, 40, 40, -11, 4, -1], [0, 1, -5, 17, 58, -10, 4, -1]];
|
||||
/// Chroma 4-tap interpolation filter fC[xFrac][k] (8-255..8-261), taps at offsets -1..2.
|
||||
pub static CHROMA_FILTER: [[i8; 4]; 8] = [[0, 64, 0, 0], [-2, 58, 10, -2], [-4, 54, 16, -2], [-6, 46, 28, -4], [-4, 36, 36, -4], [-4, 28, 46, -6], [-2, 16, 54, -4], [-2, 10, 58, -2]];
|
||||
|
||||
/// DCT matrix (8-319, 8-321): TRANS_MATRIX[n][m] = transMatrix[m][n] (row n = basis function).
|
||||
pub static TRANS_MATRIX: [[i8; 32]; 32] = [
|
||||
[64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64],
|
||||
[90, 90, 88, 85, 82, 78, 73, 67, 61, 54, 46, 38, 31, 22, 13, 4, -4, -13, -22, -31, -38, -46, -54, -61, -67, -73, -78, -82, -85, -88, -90, -90],
|
||||
[90, 87, 80, 70, 57, 43, 25, 9, -9, -25, -43, -57, -70, -80, -87, -90, -90, -87, -80, -70, -57, -43, -25, -9, 9, 25, 43, 57, 70, 80, 87, 90],
|
||||
[90, 82, 67, 46, 22, -4, -31, -54, -73, -85, -90, -88, -78, -61, -38, -13, 13, 38, 61, 78, 88, 90, 85, 73, 54, 31, 4, -22, -46, -67, -82, -90],
|
||||
[89, 75, 50, 18, -18, -50, -75, -89, -89, -75, -50, -18, 18, 50, 75, 89, 89, 75, 50, 18, -18, -50, -75, -89, -89, -75, -50, -18, 18, 50, 75, 89],
|
||||
[88, 67, 31, -13, -54, -82, -90, -78, -46, -4, 38, 73, 90, 85, 61, 22, -22, -61, -85, -90, -73, -38, 4, 46, 78, 90, 82, 54, 13, -31, -67, -88],
|
||||
[87, 57, 9, -43, -80, -90, -70, -25, 25, 70, 90, 80, 43, -9, -57, -87, -87, -57, -9, 43, 80, 90, 70, 25, -25, -70, -90, -80, -43, 9, 57, 87],
|
||||
[85, 46, -13, -67, -90, -73, -22, 38, 82, 88, 54, -4, -61, -90, -78, -31, 31, 78, 90, 61, 4, -54, -88, -82, -38, 22, 73, 90, 67, 13, -46, -85],
|
||||
[83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36, 83],
|
||||
[82, 22, -54, -90, -61, 13, 78, 85, 31, -46, -90, -67, 4, 73, 88, 38, -38, -88, -73, -4, 67, 90, 46, -31, -85, -78, -13, 61, 90, 54, -22, -82],
|
||||
[80, 9, -70, -87, -25, 57, 90, 43, -43, -90, -57, 25, 87, 70, -9, -80, -80, -9, 70, 87, 25, -57, -90, -43, 43, 90, 57, -25, -87, -70, 9, 80],
|
||||
[78, -4, -82, -73, 13, 85, 67, -22, -88, -61, 31, 90, 54, -38, -90, -46, 46, 90, 38, -54, -90, -31, 61, 88, 22, -67, -85, -13, 73, 82, 4, -78],
|
||||
[75, -18, -89, -50, 50, 89, 18, -75, -75, 18, 89, 50, -50, -89, -18, 75, 75, -18, -89, -50, 50, 89, 18, -75, -75, 18, 89, 50, -50, -89, -18, 75],
|
||||
[73, -31, -90, -22, 78, 67, -38, -90, -13, 82, 61, -46, -88, -4, 85, 54, -54, -85, 4, 88, 46, -61, -82, 13, 90, 38, -67, -78, 22, 90, 31, -73],
|
||||
[70, -43, -87, 9, 90, 25, -80, -57, 57, 80, -25, -90, -9, 87, 43, -70, -70, 43, 87, -9, -90, -25, 80, 57, -57, -80, 25, 90, 9, -87, -43, 70],
|
||||
[67, -54, -78, 38, 85, -22, -90, 4, 90, 13, -88, -31, 82, 46, -73, -61, 61, 73, -46, -82, 31, 88, -13, -90, -4, 90, 22, -85, -38, 78, 54, -67],
|
||||
[64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64],
|
||||
[61, -73, -46, 82, 31, -88, -13, 90, -4, -90, 22, 85, -38, -78, 54, 67, -67, -54, 78, 38, -85, -22, 90, 4, -90, 13, 88, -31, -82, 46, 73, -61],
|
||||
[57, -80, -25, 90, -9, -87, 43, 70, -70, -43, 87, 9, -90, 25, 80, -57, -57, 80, 25, -90, 9, 87, -43, -70, 70, 43, -87, -9, 90, -25, -80, 57],
|
||||
[54, -85, -4, 88, -46, -61, 82, 13, -90, 38, 67, -78, -22, 90, -31, -73, 73, 31, -90, 22, 78, -67, -38, 90, -13, -82, 61, 46, -88, 4, 85, -54],
|
||||
[50, -89, 18, 75, -75, -18, 89, -50, -50, 89, -18, -75, 75, 18, -89, 50, 50, -89, 18, 75, -75, -18, 89, -50, -50, 89, -18, -75, 75, 18, -89, 50],
|
||||
[46, -90, 38, 54, -90, 31, 61, -88, 22, 67, -85, 13, 73, -82, 4, 78, -78, -4, 82, -73, -13, 85, -67, -22, 88, -61, -31, 90, -54, -38, 90, -46],
|
||||
[43, -90, 57, 25, -87, 70, 9, -80, 80, -9, -70, 87, -25, -57, 90, -43, -43, 90, -57, -25, 87, -70, -9, 80, -80, 9, 70, -87, 25, 57, -90, 43],
|
||||
[38, -88, 73, -4, -67, 90, -46, -31, 85, -78, 13, 61, -90, 54, 22, -82, 82, -22, -54, 90, -61, -13, 78, -85, 31, 46, -90, 67, 4, -73, 88, -38],
|
||||
[36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83, 36],
|
||||
[31, -78, 90, -61, 4, 54, -88, 82, -38, -22, 73, -90, 67, -13, -46, 85, -85, 46, 13, -67, 90, -73, 22, 38, -82, 88, -54, -4, 61, -90, 78, -31],
|
||||
[25, -70, 90, -80, 43, 9, -57, 87, -87, 57, -9, -43, 80, -90, 70, -25, -25, 70, -90, 80, -43, -9, 57, -87, 87, -57, 9, 43, -80, 90, -70, 25],
|
||||
[22, -61, 85, -90, 73, -38, -4, 46, -78, 90, -82, 54, -13, -31, 67, -88, 88, -67, 31, 13, -54, 82, -90, 78, -46, 4, 38, -73, 90, -85, 61, -22],
|
||||
[18, -50, 75, -89, 89, -75, 50, -18, -18, 50, -75, 89, -89, 75, -50, 18, 18, -50, 75, -89, 89, -75, 50, -18, -18, 50, -75, 89, -89, 75, -50, 18],
|
||||
[13, -38, 61, -78, 88, -90, 85, -73, 54, -31, 4, 22, -46, 67, -82, 90, -90, 82, -67, 46, -22, -4, 31, -54, 73, -85, 90, -88, 78, -61, 38, -13],
|
||||
[9, -25, 43, -57, 70, -80, 87, -90, 90, -87, 80, -70, 57, -43, 25, -9, -9, 25, -43, 57, -70, 80, -87, 90, -90, 87, -80, 70, -57, 43, -25, 9],
|
||||
[4, -13, 22, -31, 38, -46, 54, -61, 67, -73, 78, -82, 85, -88, 90, -90, 90, -90, 88, -85, 82, -78, 73, -67, 61, -54, 46, -38, 31, -22, 13, -4],
|
||||
];
|
||||
/// Table 8-12: beta' for Q = 0..51.
|
||||
pub static BETA_TABLE: [u8; 52] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64];
|
||||
/// Table 8-12: tC' for Q = 0..53.
|
||||
pub static TC_TABLE: [u8; 54] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 8, 9, 10, 11, 13, 14, 16, 18, 20, 22, 24];
|
||||
|
|
@ -0,0 +1,328 @@
|
|||
//! Forward transforms (encoder side, any integer approximation is allowed), quantisation, and the
|
||||
//! normative scaling (8.6.3) and inverse transforms (8.6.4.2) the decoder applies. The inverse path
|
||||
//! is exact integer arithmetic: the even/odd decomposition computes the same sums as the matrix
|
||||
//! products of the specification.
|
||||
|
||||
use crate::tables::{DST_MATRIX, LEVEL_SCALE, TRANS_MATRIX};
|
||||
|
||||
#[inline]
|
||||
fn m(n: usize, k: usize, i: usize) -> i32 {
|
||||
// k-th basis function of the n-point DCT, sample i
|
||||
TRANS_MATRIX[k * (32 / n)][i] as i32
|
||||
}
|
||||
|
||||
/// 1-D forward DCT of `x` (length n) into `out`.
|
||||
fn fdct1d(x: &[i32], out: &mut [i32], n: usize) {
|
||||
if n <= 4 {
|
||||
for k in 0..n {
|
||||
let mut s = 0;
|
||||
for i in 0..n {
|
||||
s += m(n, k, i) * x[i];
|
||||
}
|
||||
out[k] = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
let h = n / 2;
|
||||
let mut e = [0i32; 16];
|
||||
let mut o = [0i32; 16];
|
||||
for i in 0..h {
|
||||
e[i] = x[i] + x[n - 1 - i];
|
||||
o[i] = x[i] - x[n - 1 - i];
|
||||
}
|
||||
let mut ev = [0i32; 16];
|
||||
fdct1d(&e[..h], &mut ev[..h], h);
|
||||
for k in 0..h {
|
||||
out[2 * k] = ev[k];
|
||||
let kk = 2 * k + 1;
|
||||
let mut s = 0;
|
||||
for i in 0..h {
|
||||
s += m(n, kk, i) * o[i];
|
||||
}
|
||||
out[kk] = s;
|
||||
}
|
||||
}
|
||||
|
||||
/// 1-D inverse DCT: x[i] = sum_k M[k][i] * c[k].
|
||||
fn idct1d(c: &[i32], out: &mut [i32], n: usize) {
|
||||
if n <= 4 {
|
||||
for i in 0..n {
|
||||
let mut s = 0;
|
||||
for k in 0..n {
|
||||
s += m(n, k, i) * c[k];
|
||||
}
|
||||
out[i] = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
let h = n / 2;
|
||||
let mut ce = [0i32; 16];
|
||||
for k in 0..h {
|
||||
ce[k] = c[2 * k];
|
||||
}
|
||||
let mut e = [0i32; 16];
|
||||
idct1d(&ce[..h], &mut e[..h], h);
|
||||
for i in 0..h {
|
||||
let mut o = 0;
|
||||
let mut k = 1;
|
||||
while k < n {
|
||||
let v = c[k];
|
||||
if v != 0 {
|
||||
o += m(n, k, i) * v;
|
||||
}
|
||||
k += 2;
|
||||
}
|
||||
out[i] = e[i] + o;
|
||||
out[n - 1 - i] = e[i] - o;
|
||||
}
|
||||
}
|
||||
|
||||
fn fdst1d(x: &[i32], out: &mut [i32]) {
|
||||
for k in 0..4 {
|
||||
out[k] = (0..4).map(|i| DST_MATRIX[k][i] as i32 * x[i]).sum();
|
||||
}
|
||||
}
|
||||
|
||||
fn idst1d(c: &[i32], out: &mut [i32]) {
|
||||
for i in 0..4 {
|
||||
out[i] = (0..4).map(|k| DST_MATRIX[k][i] as i32 * c[k]).sum();
|
||||
}
|
||||
}
|
||||
|
||||
/// Forward 2-D transform of the n x n residual `r` (row-major) into `c`.
|
||||
pub fn forward(r: &[i32], c: &mut [i32], n: usize, dst: bool, bit_depth: u32) {
|
||||
let log2 = n.trailing_zeros();
|
||||
let shift1 = log2 + bit_depth - 9;
|
||||
let shift2 = log2 + 6;
|
||||
let (r1, r2) = (1i32 << (shift1 - 1), 1i32 << (shift2 - 1));
|
||||
let mut tmp = vec![0i32; n * n];
|
||||
let mut out = [0i32; 32];
|
||||
// rows (horizontal frequencies)
|
||||
for y in 0..n {
|
||||
let row = &r[y * n..y * n + n];
|
||||
if dst {
|
||||
fdst1d(row, &mut out);
|
||||
} else {
|
||||
fdct1d(row, &mut out, n);
|
||||
}
|
||||
for k in 0..n {
|
||||
tmp[k * n + y] = (out[k] + r1) >> shift1; // transposed: tmp[kx][y]
|
||||
}
|
||||
}
|
||||
// columns
|
||||
let mut col = [0i32; 32];
|
||||
for kx in 0..n {
|
||||
col[..n].copy_from_slice(&tmp[kx * n..kx * n + n]);
|
||||
if dst {
|
||||
fdst1d(&col, &mut out);
|
||||
} else {
|
||||
fdct1d(&col, &mut out, n);
|
||||
}
|
||||
for ky in 0..n {
|
||||
c[ky * n + kx] = (out[ky] + r2) >> shift2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inverse 2-D transform (8.6.4.2) of scaled coefficients `d` in place, producing residuals.
|
||||
pub fn inverse(d: &mut [i32], n: usize, dst: bool, bit_depth: u32) {
|
||||
let bd_shift = 20 - bit_depth;
|
||||
let mut tmp = vec![0i32; n * n];
|
||||
let mut col = [0i32; 32];
|
||||
let mut out = [0i32; 32];
|
||||
// max non-zero row, to skip empty work
|
||||
let mut nz_cols = [false; 32];
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
if d[y * n + x] != 0 {
|
||||
nz_cols[x] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 1. columns
|
||||
for x in 0..n {
|
||||
if !nz_cols[x] {
|
||||
continue; // tmp column stays zero
|
||||
}
|
||||
for j in 0..n {
|
||||
col[j] = d[j * n + x];
|
||||
}
|
||||
if dst {
|
||||
idst1d(&col, &mut out);
|
||||
} else {
|
||||
idct1d(&col, &mut out, n);
|
||||
}
|
||||
for y in 0..n {
|
||||
tmp[y * n + x] = ((out[y] + 64) >> 7).clamp(-32768, 32767);
|
||||
}
|
||||
}
|
||||
// 2. rows
|
||||
let round = 1 << (bd_shift - 1);
|
||||
for y in 0..n {
|
||||
let row = &tmp[y * n..y * n + n];
|
||||
if dst {
|
||||
idst1d(row, &mut out);
|
||||
} else {
|
||||
idct1d(row, &mut out, n);
|
||||
}
|
||||
for x in 0..n {
|
||||
d[y * n + x] = (out[x] + round) >> bd_shift;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const QUANT_SCALE: [i64; 6] = [26214, 23302, 20560, 18396, 16384, 14564];
|
||||
|
||||
/// Quantise transform coefficients `c` into levels `l` with a dead-zone rounding offset
|
||||
/// (`intra`: 1/3, otherwise 1/6). `qp` includes QpBdOffset. Returns the number of non-zero levels.
|
||||
pub fn quantize(c: &[i32], l: &mut [i32], n: usize, qp: i32, intra: bool, bit_depth: u32) -> usize {
|
||||
let log2 = n.trailing_zeros() as i32;
|
||||
let tshift = 15 - bit_depth as i32 - log2;
|
||||
let qbits = 14 + qp / 6 + tshift;
|
||||
let scale = QUANT_SCALE[(qp % 6) as usize];
|
||||
let offset = (if intra { 171i64 } else { 85 }) << (qbits - 9);
|
||||
let mut nz = 0;
|
||||
for i in 0..n * n {
|
||||
let v = c[i] as i64;
|
||||
let a = ((v.abs() * scale + offset) >> qbits).min(32767);
|
||||
if a != 0 {
|
||||
nz += 1;
|
||||
}
|
||||
l[i] = if v < 0 { -(a as i32) } else { a as i32 };
|
||||
}
|
||||
nz
|
||||
}
|
||||
|
||||
/// Sign data hiding (7.3.8.11 signHidden): in every 4x4 sub-block whose first and last non-zero
|
||||
/// coefficients (in scan order) are more than 3 positions apart, the sign of the first one is not
|
||||
/// coded but inferred from the parity of the sum of absolute levels. Fix the parity where needed by
|
||||
/// the +-1 level change that adds the least quantisation error, without moving the first / last
|
||||
/// non-zero positions.
|
||||
pub fn sign_hiding(c: &[i32], l: &mut [i32], log2: u32, scan_idx: usize, qp: i32, bit_depth: u32) {
|
||||
use crate::tables::{SCAN_4, scan_order};
|
||||
let n = 1usize << log2;
|
||||
let tshift = 15 - bit_depth as i32 - log2 as i32;
|
||||
let qbits = 14 + qp / 6 + tshift;
|
||||
let scale = QUANT_SCALE[(qp % 6) as usize];
|
||||
let sb_scan = scan_order(log2 - 2, scan_idx);
|
||||
let pos = &SCAN_4[scan_idx];
|
||||
for &(xs, ys) in sb_scan {
|
||||
let idx = |p: usize| (ys as usize * 4 + pos[p].1 as usize) * n + xs as usize * 4 + pos[p].0 as usize;
|
||||
let (Some(first), Some(last)) = ((0..16).find(|&p| l[idx(p)] != 0), (0..16).rev().find(|&p| l[idx(p)] != 0)) else { continue };
|
||||
if last - first <= 3 {
|
||||
continue;
|
||||
}
|
||||
let sum: i32 = (0..16).map(|p| l[idx(p)].abs()).sum();
|
||||
let negative = l[idx(first)] < 0;
|
||||
if negative == (sum & 1 == 1) {
|
||||
continue;
|
||||
}
|
||||
// cost of +-1 changes in 1/256 units of quantisation error
|
||||
let mut best: Option<(i64, usize, i32)> = None;
|
||||
for p in first..=last {
|
||||
let i = idx(p);
|
||||
let q = ((c[i] as i64).abs() * scale) >> (qbits - 8);
|
||||
let a = l[i].abs() as i64;
|
||||
let err = |lv: i64| (q - (lv << 8)).abs();
|
||||
let mut consider = |cost: i64, d: i32| {
|
||||
if best.is_none_or(|b| cost < b.0) {
|
||||
best = Some((cost, i, d));
|
||||
}
|
||||
};
|
||||
if a < 32767 && (a > 0 || (p > first && p < last)) {
|
||||
consider(err(a + 1) - err(a), 1);
|
||||
}
|
||||
if a > 1 || (a == 1 && p > first && p < last) {
|
||||
consider(err(a - 1) - err(a), -1);
|
||||
}
|
||||
}
|
||||
if let Some((_, i, d)) = best {
|
||||
let a = l[i].abs() + d;
|
||||
l[i] = if c[i] < 0 { -a } else { a };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scaling process (8.6.2 / 8.6.3, flat scaling matrix): levels to scaled coefficients.
|
||||
pub fn dequantize(l: &[i32], d: &mut [i32], n: usize, qp: i32, bit_depth: u32) {
|
||||
let log2 = n.trailing_zeros();
|
||||
let bd_shift = bit_depth + log2 - 5;
|
||||
let round = 1i64 << (bd_shift - 1);
|
||||
let scale = (LEVEL_SCALE[(qp % 6) as usize] as i64) << (qp / 6);
|
||||
for i in 0..n * n {
|
||||
let v = l[i];
|
||||
d[i] = if v == 0 { 0 } else { ((v as i64 * 16 * scale + round) >> bd_shift).clamp(-32768, 32767) as i32 };
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Direct matrix form of the inverse transform, as written in the specification.
|
||||
fn inverse_ref(c: &mut [i32], n: usize, dst: bool, bd: u32) {
|
||||
let mut tmp = vec![0i32; n * n];
|
||||
let step = 32 / n;
|
||||
for x in 0..n {
|
||||
for y in 0..n {
|
||||
let mut s = 0i32;
|
||||
for j in 0..n {
|
||||
let m = if dst { DST_MATRIX[j][y] as i32 } else { TRANS_MATRIX[j * step][y] as i32 };
|
||||
s += m * c[j * n + x];
|
||||
}
|
||||
tmp[y * n + x] = ((s + 64) >> 7).clamp(-32768, 32767);
|
||||
}
|
||||
}
|
||||
let bd_shift = 20 - bd;
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
let mut s = 0i32;
|
||||
for j in 0..n {
|
||||
let m = if dst { DST_MATRIX[j][x] as i32 } else { TRANS_MATRIX[j * step][x] as i32 };
|
||||
s += m * tmp[y * n + j];
|
||||
}
|
||||
c[y * n + x] = (s + (1 << (bd_shift - 1))) >> bd_shift;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inverse_matches_matrix_form() {
|
||||
let mut seed = 7u32;
|
||||
for &(n, dst) in &[(4usize, true), (4, false), (8, false), (16, false), (32, false)] {
|
||||
for _ in 0..20 {
|
||||
let mut c = vec![0i32; n * n];
|
||||
for v in c.iter_mut() {
|
||||
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
if (seed >> 24).is_multiple_of(3) {
|
||||
*v = ((seed >> 8) % 2001) as i32 - 1000;
|
||||
}
|
||||
}
|
||||
let mut a = c.clone();
|
||||
let mut b = c.clone();
|
||||
inverse(&mut a, n, dst, 8);
|
||||
inverse_ref(&mut b, n, dst, 8);
|
||||
assert_eq!(a, b, "n {n} dst {dst}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_is_close() {
|
||||
for &(n, dst) in &[(4usize, true), (8, false), (32, false)] {
|
||||
for bd in [8u32, 10] {
|
||||
let r: Vec<i32> = (0..n * n).map(|i| ((i * 37 + i / n * 11) % 61) as i32 - 30).collect();
|
||||
let mut c = vec![0; n * n];
|
||||
forward(&r, &mut c, n, dst, bd);
|
||||
let mut l = vec![0; n * n];
|
||||
let qp = 4;
|
||||
quantize(&c, &mut l, n, qp, true, bd);
|
||||
let mut d = vec![0; n * n];
|
||||
dequantize(&l, &mut d, n, qp, bd);
|
||||
inverse(&mut d, n, dst, bd);
|
||||
let err: i32 = r.iter().zip(&d).map(|(a, b)| (a - b).abs()).max().unwrap();
|
||||
assert!(err <= 2, "n {n} bd {bd} err {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc_fingerprint":9559367331539902066,"outputs":{"4233666815994275565":{"success":true,"status":"","code":0,"stdout":"rustc 1.96.0 (ac68faa20 2026-05-25)\nbinary: rustc\ncommit-hash: ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96\ncommit-date: 2026-05-25\nhost: aarch64-apple-darwin\nrelease: 1.96.0\nLLVM version: 22.1.2\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/bt/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
Signature: 8a477f597d28d172789f06886806bc55
|
||||
# This file is a cache directory tag created by cargo.
|
||||
# For information about cache directory tags see https://bford.info/cachedir/
|
||||
|
|
@ -0,0 +1 @@
|
|||
096ac002c23a6667
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc":17467972547956852243,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":5408242616063297496,"profile":3185167420583542146,"path":6080947815111469725,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/crossbeam-deque-5b3c4336788f7174/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
This file has an mtime of when this was started.
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -0,0 +1 @@
|
|||
a673fdef9376ddb1
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc":17467972547956852243,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":15353977948366730291,"profile":3185167420583542146,"path":2714696676876033032,"deps":[[2543204310390312751,"crossbeam_epoch",false,15863831904650651516],[11050506297539643678,"crossbeam_utils",false,11092378776446818952],[15481973119957668846,"build_script_build",false,7352081084529830974]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/crossbeam-deque-d9775930f3a59590/dep-lib-crossbeam_deque","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
|
|
@ -0,0 +1 @@
|
|||
3eec441bcbd60766
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc":17467972547956852243,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[15481973119957668846,"build_script_build",false,7450707238475295241]],"local":[{"RerunIfChanged":{"output":"debug/build/crossbeam-deque-f51f1776f2c514b1/output","paths":["build.rs"]}}],"rustflags":[],"config":0,"compile_kind":0}
|
||||
|
|
@ -0,0 +1 @@
|
|||
16d4cd1f76c06772
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc":17467972547956852243,"features":"[\"alloc\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"loom\", \"loom-crate\", \"nightly\", \"std\"]","target":5408242616063297496,"profile":3185167420583542146,"path":5028293854929153649,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/crossbeam-epoch-1347d2207ea061ce/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -0,0 +1 @@
|
|||
c36dd00ce8039b4d
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc":17467972547956852243,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[2543204310390312751,"build_script_build",false,8243769256497239062]],"local":[{"RerunIfChanged":{"output":"debug/build/crossbeam-epoch-2092bf031b9d9a01/output","paths":["build.rs"]}}],"rustflags":[],"config":0,"compile_kind":0}
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -0,0 +1 @@
|
|||
7ce7352d11a727dc
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc":17467972547956852243,"features":"[\"alloc\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"loom\", \"loom-crate\", \"nightly\", \"std\"]","target":16242420667881341737,"profile":3185167420583542146,"path":7266900391393874633,"deps":[[2543204310390312751,"build_script_build",false,5592067657492884931],[11050506297539643678,"crossbeam_utils",false,11092378776446818952]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/crossbeam-epoch-72c18a120600cf42/dep-lib-crossbeam_epoch","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
|
|
@ -0,0 +1 @@
|
|||
bdec41fc81c92fa2
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc":17467972547956852243,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"loom\", \"nightly\", \"std\"]","target":5408242616063297496,"profile":3185167420583542146,"path":14844284517995534797,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/crossbeam-utils-5b2ff6828cdce4c3/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
This file has an mtime of when this was started.
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -0,0 +1 @@
|
|||
88da312cba0bf099
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc":17467972547956852243,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"loom\", \"nightly\", \"std\"]","target":9626079250877207070,"profile":3185167420583542146,"path":16937405451120399764,"deps":[[11050506297539643678,"build_script_build",false,8108134710804475658]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/crossbeam-utils-799e464d44773a7d/dep-lib-crossbeam_utils","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
|
|
@ -0,0 +1 @@
|
|||
0aef838c8fe18570
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc":17467972547956852243,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[11050506297539643678,"build_script_build",false,11686781118169869501]],"local":[{"RerunIfChanged":{"output":"debug/build/crossbeam-utils-99f1d686cf22430f/output","paths":["no_atomic.rs"]}}],"rustflags":[],"config":0,"compile_kind":0}
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -0,0 +1 @@
|
|||
e64536e1e71b0acd
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc":17467972547956852243,"features":"[]","declared_features":"[]","target":13741281541045977916,"profile":14861435456621666076,"path":1524816848624354599,"deps":[[5706705931987278984,"filmcraft_hevc",false,1065779467293311985],[10103226027885832861,"effectcraft_hevcenc",false,3919018516886026431]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/effectcraft-hevcenc-335f1f822197d831/dep-test-integration-test-roundtrip","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -0,0 +1 @@
|
|||
bf782bc793266336
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc":17467972547956852243,"features":"[]","declared_features":"[]","target":11125424694902762418,"profile":15209061013077869454,"path":6858098780819335099,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/effectcraft-hevcenc-955aee250b2c2a4e/dep-lib-effectcraft_hevcenc","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -0,0 +1 @@
|
|||
a449c4ea4d7d8d17
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc":17467972547956852243,"features":"[]","declared_features":"[\"default\", \"serde\", \"std\", \"use_std\"]","target":17124342308084364240,"profile":2785734998891497023,"path":5546998507963859579,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/either-72952a75ef45a16d/dep-lib-either","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -0,0 +1 @@
|
|||
059b6b4655b5b2c5
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc":17467972547956852243,"features":"[]","declared_features":"[]","target":18127681992696119206,"profile":12938959104297781942,"path":12854803809928081657,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/filmcraft-bitstream-2ad770771b023a77/dep-lib-filmcraft_bitstream","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -0,0 +1 @@
|
|||
f143ec5cc768ca0e
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"rustc":17467972547956852243,"features":"[\"default\", \"threads\"]","declared_features":"[\"default\", \"threads\"]","target":9847609124140232008,"profile":15209061013077869454,"path":2590569095805419776,"deps":[[1957009224993739128,"thiserror",false,17538703420373504327],[4943083844709074312,"filmcraft_bitstream",false,14245647949184604933],[11910974697091955563,"rayon",false,13044872296819726823]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/filmcraft-hevc-71cf1ac5fd5debba/dep-lib-filmcraft_hevc","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue