mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 15:48:33 +00:00
Merge branch 'worktree-agent-a5ec24c82e394a621'
This commit is contained in:
commit
c005493c84
270 files changed
+18707
-110
No files matched your search
Generated
+19
@@ -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
@@ -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)?,
|
||||
|
||||
+27
-15
@@ -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/
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
+1
@@ -0,0 +1 @@
|
||||
096ac002c23a6667
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
a673fdef9376ddb1
|
||||
+1
@@ -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}
|
||||
+1
@@ -0,0 +1 @@
|
||||
3eec441bcbd60766
|
||||
+1
@@ -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}
|
||||
+1
@@ -0,0 +1 @@
|
||||
16d4cd1f76c06772
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
c36dd00ce8039b4d
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
7ce7352d11a727dc
|
||||
+1
@@ -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}
|
||||
+1
@@ -0,0 +1 @@
|
||||
bdec41fc81c92fa2
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
88da312cba0bf099
|
||||
+1
@@ -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}
|
||||
+1
@@ -0,0 +1 @@
|
||||
0aef838c8fe18570
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
e64536e1e71b0acd
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
bf782bc793266336
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
a449c4ea4d7d8d17
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
059b6b4655b5b2c5
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
f143ec5cc768ca0e
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
Loaded 100 of 270 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user