use super::engine::deblock::{EdgePos, EdgeQp, EdgeType, SamplePlane, filter_luma_block_edge};
use super::engine::inter_pred::{
RefPlane, default_weighted_pred, interp_chroma_block, interp_luma_block,
};
use super::engine::intra_pred::{
Component as IntraComponent, IntraPredParams, ReferenceSamples, intra_predict,
};
use super::engine::picture::{Picture, Plane};
use super::engine::sao::{
ResolvedSao, ResolvedSaoComponent, SaoBoundaries, apply_sao_picture_with_boundaries,
};
use super::engine::transform::{BlockParams, Component as TxComponent, PredMode, residual_block};
use super::engine::{BitReader, CabacEngine, ContextModel};
use super::{HevcDecoder, ParsedConfiguration, picture_to_rgba};
use crate::{
CancellationToken, Codec, CodecProfile, ColorRange, EncodedVideoSample, HardwarePreference,
Limits, PixelFormat, VideoDecoder, VideoDecoderConfig, VideoDimensions,
};
const INTER_CELL: usize = 64;
const INTER_PU_MIX: [(usize, bool, f64); 8] = [
(64, true, 0.432),
(64, false, 0.189),
(32, true, 0.162),
(32, false, 0.149),
(16, false, 0.036),
(16, true, 0.017),
(8, false, 0.009),
(8, true, 0.005),
];
const SAO_CTB_MIX: [[f64; 6]; 2] = [
[0.866780, 0.020362, 0.020777, 0.030656, 0.026697, 0.034729],
[0.944155, 0.009540, 0.001357, 0.004450, 0.015875, 0.024623],
];
const SAO_CTB_LOG2: u32 = 6;
const BIT_DEPTH: u8 = 8;
const TX_SIZES: [usize; 4] = [4, 8, 16, 32];
const CABAC_BINS: usize = 1 << 18;
#[derive(Debug, Clone, Copy)]
struct InterPu {
x: i32,
y: i32,
n: usize,
bi: bool,
phase: i32,
}
fn schedule_inter_pus(width: usize, height: usize) -> Vec<InterPu> {
let cells_x = (width / INTER_CELL).saturating_sub(1);
let cells_y = (height / INTER_CELL).saturating_sub(1);
let mut emitted = [0u64; INTER_PU_MIX.len()];
let mut total = 0u64;
let mut pus = Vec::new();
let mut phase = 0i32;
for cy in 0..cells_y {
for cx in 0..cells_x {
let class = (0..INTER_PU_MIX.len())
.max_by(|&a, &b| {
let deficit = |i: usize| {
INTER_PU_MIX[i]
.2
.mul_add(total as f64, -(emitted[i] as f64))
};
deficit(a)
.partial_cmp(&deficit(b))
.expect("the shares are finite")
})
.expect("the mix is not empty");
let (n, bi, _) = INTER_PU_MIX[class];
for by in (0..INTER_CELL).step_by(n) {
for bx in (0..INTER_CELL).step_by(n) {
pus.push(InterPu {
x: (cx * INTER_CELL + bx) as i32,
y: (cy * INTER_CELL + by) as i32,
n,
bi,
phase,
});
phase = (phase + 1) % 16;
}
}
let cell_samples = (INTER_CELL * INTER_CELL) as u64;
emitted[class] += cell_samples;
total += cell_samples;
}
}
pus
}
struct Digest(u64);
impl Digest {
fn new() -> Self {
Digest(0xcbf2_9ce4_8422_2325)
}
fn push_i32(&mut self, value: i32) {
for byte in value.to_le_bytes() {
self.0 ^= u64::from(byte);
self.0 = self.0.wrapping_mul(0x1000_0000_01b3);
}
}
fn push_all(&mut self, values: &[i32]) {
for &value in values {
self.push_i32(value);
}
}
fn push_u64(&mut self, value: u64) {
self.0 = (self.0 ^ value).wrapping_mul(0x1000_0000_01b3);
}
fn push_samples(&mut self, values: &[i32]) {
let mut chunks = values.chunks_exact(2);
for pair in &mut chunks {
self.push_u64(u64::from(pair[0] as u32) | (u64::from(pair[1] as u32) << 32));
}
for &value in chunks.remainder() {
self.push_u64(u64::from(value as u32));
}
}
fn push_bytes(&mut self, bytes: &[u8]) {
let mut chunks = bytes.chunks_exact(8);
for chunk in &mut chunks {
self.push_u64(u64::from_le_bytes(
chunk.try_into().expect("the chunk is eight bytes"),
));
}
for &byte in chunks.remainder() {
self.push_u64(u64::from(byte));
}
}
fn finish(self) -> Vec<u8> {
self.0.to_le_bytes().to_vec()
}
}
struct Lcg(u64);
impl Lcg {
fn new(seed: u64) -> Self {
Lcg(seed | 1)
}
fn next_u32(&mut self) -> u32 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(self.0 >> 33) as u32
}
fn below(&mut self, bound: u32) -> u32 {
self.next_u32() % bound
}
}
fn synthetic_luma(width: usize, height: usize, seed: u64) -> Vec<i32> {
let mut rng = Lcg::new(seed);
let mut plane = vec![0i32; width * height];
for y in 0..height {
let flat_band = (y / 32) % 2 == 1;
for x in 0..width {
let value = if flat_band {
let level = 40 + 12 * ((x / 8) % 5) as i32;
level + i32::from((x % 8) == 0)
} else {
let base = ((x * 3 + y * 5) % 200) as i32;
base + (rng.below(24) as i32) - 12
};
plane[y * width + x] = value.clamp(0, 255);
}
}
plane
}
pub struct HevcStageInputs {
width: usize,
height: usize,
luma: Vec<i32>,
intra_blocks: Vec<(ReferenceSamples, IntraPredParams)>,
intra_samples: u64,
tx_blocks: Vec<(Vec<i32>, BlockParams)>,
tx_samples: u64,
picture: Picture,
sao_ctbs: Vec<ResolvedSao>,
sao_boundaries: SaoBoundaries,
sao_filtered_samples: u64,
inter_pus: Vec<InterPu>,
inter_luma_samples: u64,
convert_config: VideoDecoderConfig,
cabac_bytes: Vec<u8>,
}
impl HevcStageInputs {
#[must_use]
pub fn new(width: usize, height: usize) -> Self {
assert!(width > 0 && height > 0, "the frame has to have samples");
let luma = synthetic_luma(width, height, 0x5EED_1234);
let intra_blocks = build_intra_blocks();
let intra_samples = intra_blocks
.iter()
.map(|(p, _)| (p.n_tbs() * p.n_tbs()) as u64)
.sum();
let tx_blocks = build_tx_blocks();
let tx_samples = tx_blocks.iter().map(|(l, _)| l.len() as u64).sum();
let (picture, sao_ctbs) = build_sao_inputs(width, height, &luma);
let (sao_boundaries, sao_filtered_samples) = build_sao_boundaries(width, height, &sao_ctbs);
let inter_pus = schedule_inter_pus(width, height);
let inter_luma_samples = inter_pus.iter().map(|pu| (pu.n * pu.n) as u64).sum();
Self {
width,
height,
luma,
intra_blocks,
intra_samples,
tx_blocks,
tx_samples,
picture,
sao_ctbs,
sao_boundaries,
sao_filtered_samples,
inter_pus,
inter_luma_samples,
convert_config: convert_config(width, height),
cabac_bytes: build_cabac_bitstream(),
}
}
#[must_use]
pub fn inter_pred_samples(&self) -> u64 {
self.inter_luma_samples
}
#[must_use]
pub fn intra_pred_samples(&self) -> u64 {
self.intra_samples
}
#[must_use]
pub fn deblock_samples(&self) -> u64 {
(self.luma_edges().len() * 4 * 8) as u64
}
#[must_use]
pub fn sao_samples(&self) -> u64 {
self.sao_filtered_samples
}
#[must_use]
pub fn inverse_transform_samples(&self) -> u64 {
self.tx_samples
}
#[must_use]
pub fn color_convert_samples(&self) -> u64 {
(self.width * self.height) as u64
}
#[must_use]
pub fn cabac_bins(&self) -> u64 {
CABAC_BINS as u64
}
#[must_use]
pub fn run_inter_pred(&self) -> Vec<u8> {
let luma = RefPlane::new(&self.luma, self.width, self.height)
.expect("the prepared plane matches its dimensions");
let (cw, ch) = (self.width / 2, self.height / 2);
let cb = RefPlane::new(self.picture.plane(Plane::Cb), cw, ch)
.expect("the prepared picture's Cb plane matches its dimensions");
let cr = RefPlane::new(self.picture.plane(Plane::Cr), cw, ch)
.expect("the prepared picture's Cr plane matches its dimensions");
let mut digest = Digest::new();
for pu in &self.inter_pus {
let n = pu.n;
let (fx0, fy0) = (pu.phase % 4, (pu.phase / 4) % 4);
let (fx1, fy1) = ((pu.phase + 1) % 4, (pu.phase + 3) % 4);
let l0 = interp_luma_block(&luma, pu.x, pu.y, fx0, fy0, n, n, BIT_DEPTH)
.expect("the unit lies inside the plane");
let l1 = if pu.bi {
interp_luma_block(&luma, pu.x + 1, pu.y + 1, fx1, fy1, n, n, BIT_DEPTH)
.expect("the unit lies inside the plane")
} else {
Vec::new()
};
let combined = default_weighted_pred(&l0, &l1, true, pu.bi, n, n, BIT_DEPTH)
.expect("both lists are the unit's size");
digest.push_all(&combined);
let (cn, cx, cy) = (n / 2, pu.x / 2, pu.y / 2);
let (cfx0, cfy0) = (pu.phase % 8, (pu.phase / 8) % 8);
let (cfx1, cfy1) = ((pu.phase + 3) % 8, (pu.phase + 5) % 8);
for plane in [&cb, &cr] {
let c0 = interp_chroma_block(plane, cx, cy, cfx0, cfy0, cn, cn, BIT_DEPTH)
.expect("the unit lies inside the plane");
let c1 = if pu.bi {
interp_chroma_block(plane, cx + 1, cy + 1, cfx1, cfy1, cn, cn, BIT_DEPTH)
.expect("the unit lies inside the plane")
} else {
Vec::new()
};
let combined = default_weighted_pred(&c0, &c1, true, pu.bi, cn, cn, BIT_DEPTH)
.expect("both lists are the unit's size");
digest.push_all(&combined);
}
}
digest.finish()
}
#[must_use]
pub fn run_intra_pred(&self) -> Vec<u8> {
let mut digest = Digest::new();
for (samples, params) in &self.intra_blocks {
let pred = intra_predict(samples, params).expect("the prepared block is well-formed");
digest.push_all(&pred);
}
digest.finish()
}
#[must_use]
pub fn run_deblock(&self) -> Vec<u8> {
let mut samples = self.luma.clone();
let mut plane = SamplePlane {
samples: &mut samples,
width: self.width,
stride: self.width,
};
let qp = EdgeQp {
qp_q: 37,
qp_p: 37,
beta_offset_div2: 0,
tc_offset_div2: 0,
bit_depth: BIT_DEPTH,
};
for pos in self.luma_edges() {
filter_luma_block_edge(&mut plane, pos, 2, qp);
}
let mut digest = Digest::new();
digest.push_all(&samples);
digest.finish()
}
#[must_use]
pub fn run_sao(&self) -> Vec<u8> {
let filtered = apply_sao_picture_with_boundaries(
self.picture.clone(),
&self.sao_ctbs,
SAO_CTB_LOG2,
1,
true,
true,
Some(&self.sao_boundaries),
);
let ctb = 1usize << SAO_CTB_LOG2;
let ctbs_x = self.width.div_ceil(ctb);
let mut digest = Digest::new();
for (i, resolved) in self.sao_ctbs.iter().enumerate() {
let (rx, ry) = (i % ctbs_x, i / ctbs_x);
for (cidx, plane) in [Plane::Luma, Plane::Cb, Plane::Cr].into_iter().enumerate() {
if resolved.components[cidx].sao_type_idx == 0 {
continue;
}
let step = if cidx == 0 { ctb } else { ctb / 2 };
let (pw, ph) = filtered.plane_dims(plane);
let (x0, y0) = (rx * step, ry * step);
let (w, h) = (step.min(pw - x0), step.min(ph - y0));
let samples = filtered.plane(plane);
for j in 0..h {
let o = (y0 + j) * pw + x0;
digest.push_samples(&samples[o..o + w]);
}
}
}
digest.finish()
}
#[must_use]
pub fn run_inverse_transform(&self) -> Vec<u8> {
let mut digest = Digest::new();
for (levels, params) in &self.tx_blocks {
let residual =
residual_block(levels, None, *params).expect("the prepared block is well-formed");
digest.push_all(&residual);
}
digest.finish()
}
#[must_use]
pub fn run_color_convert(&self) -> Vec<u8> {
let frame = picture_to_rgba(&self.picture, &self.convert_config, &Limits::default())
.expect("the prepared picture is Main-profile 8-bit 4:2:0");
let mut digest = Digest::new();
for plane in &frame.planes {
digest.push_bytes(&plane.data);
}
digest.finish()
}
#[must_use]
pub fn run_cabac(&self) -> Vec<u8> {
let mut contexts: Vec<ContextModel> = (0..64)
.map(|i| ContextModel::init(90 + (i as u8 % 60), 30))
.collect();
let mut engine =
CabacEngine::new(BitReader::new(&self.cabac_bytes)).expect("the buffer has bits");
let mut digest = Digest::new();
let mut acc = 0i32;
let context_count = contexts.len();
for i in 0..CABAC_BINS {
let bin = if i % 4 == 3 {
engine.decode_bypass()
} else {
engine.decode_decision(&mut contexts[i % context_count])
};
match bin {
Ok(bin) => acc = acc.wrapping_mul(3).wrapping_add(i32::from(bin)),
Err(_) => break,
}
digest.push_i32(acc);
}
digest.finish()
}
fn luma_edges(&self) -> Vec<EdgePos> {
let mut edges = Vec::new();
let mut ey = 0;
while ey + 4 <= self.height {
let mut ex = 8;
while ex + 8 <= self.width {
edges.push(EdgePos {
ex,
ey,
edge: EdgeType::Vertical,
});
ex += 8;
}
ey += 4;
}
edges
}
}
fn build_intra_blocks() -> Vec<(ReferenceSamples, IntraPredParams)> {
const REPEATS: usize = 24;
let mut rng = Lcg::new(0x17AB_5EED);
let mut blocks = Vec::new();
for _ in 0..REPEATS {
for n_tbs in TX_SIZES {
let want = 2 * n_tbs;
let left: Vec<i32> = (0..want).map(|_| rng.below(256) as i32).collect();
let top: Vec<i32> = (0..want).map(|_| rng.below(256) as i32).collect();
let corner = rng.below(256) as i32;
let samples = ReferenceSamples::new(n_tbs, corner, left, top)
.expect("the arrays are 2*nTbS long and nTbS is legal");
for mode in 0..=34u8 {
blocks.push((
samples.clone(),
IntraPredParams {
pred_mode_intra: mode,
cidx: IntraComponent::Luma,
bit_depth: BIT_DEPTH,
bit_depth_luma: BIT_DEPTH,
intra_smoothing_disabled: false,
strong_intra_smoothing_enabled: true,
chroma_array_type_3: false,
disable_boundary_filter: false,
},
));
}
}
}
blocks
}
fn build_tx_blocks() -> Vec<(Vec<i32>, BlockParams)> {
const BLOCKS_PER_SIZE: usize = 512;
let mut rng = Lcg::new(0x7A_5F0);
let mut blocks = Vec::new();
for n_tbs in TX_SIZES {
for i in 0..BLOCKS_PER_SIZE {
let mut levels = vec![0i32; n_tbs * n_tbs];
let coded = (n_tbs / 2).max(4);
for y in 0..coded {
for x in 0..coded {
let magnitude = (rng.below(64) as i32) - 32;
levels[y * n_tbs + x] = magnitude;
}
}
let pred_mode = if i % 2 == 0 {
PredMode::Intra
} else {
PredMode::Inter
};
blocks.push((
levels,
BlockParams {
n_tbs,
q_p: 26 + (i as u32 % 12),
component: TxComponent::Luma,
pred_mode,
bit_depth: BIT_DEPTH,
extended_precision: false,
transquant_bypass: false,
transform_skip: false,
transform_skip_rotation_enabled: false,
},
));
}
}
blocks
}
fn sao_class_for(shares: &[f64; 6], emitted: &[u64; 6], total: u64) -> usize {
(0..6)
.max_by(|&a, &b| {
let deficit = |i: usize| shares[i].mul_add(total as f64, -(emitted[i] as f64));
deficit(a)
.partial_cmp(&deficit(b))
.expect("the shares are finite")
})
.expect("the mix is not empty")
}
fn sao_component_for(class: usize, i: usize, cidx: usize) -> ResolvedSaoComponent {
match class {
0 => ResolvedSaoComponent::off(),
1 => ResolvedSaoComponent {
sao_type_idx: 1,
offset_val: [0, 3, -2, 1, -3],
band_position: ((i * 7 + cidx) % 28) as u8,
eo_class: 0,
},
_ => ResolvedSaoComponent {
sao_type_idx: 2,
offset_val: [0, 2, 1, -1, -2],
band_position: 0,
eo_class: (class - 2) as u8,
},
}
}
fn build_sao_inputs(width: usize, height: usize, luma: &[i32]) -> (Picture, Vec<ResolvedSao>) {
let mut picture = Picture::new(width, height, 1, BIT_DEPTH, BIT_DEPTH);
let (y_plane, _) = picture.plane_mut(Plane::Luma);
y_plane.copy_from_slice(luma);
let chroma = synthetic_luma(width / 2, height / 2, 0xC4B0);
for plane in [Plane::Cb, Plane::Cr] {
let (samples, _) = picture.plane_mut(plane);
samples.copy_from_slice(&chroma);
}
let ctb = 1usize << SAO_CTB_LOG2;
let ctbs_x = width.div_ceil(ctb);
let ctbs_y = height.div_ceil(ctb);
let mut grid = Vec::with_capacity(ctbs_x * ctbs_y);
let mut emitted = [[0u64; 6]; 2];
for i in 0..ctbs_x * ctbs_y {
let total = i as u64;
let luma_class = sao_class_for(&SAO_CTB_MIX[0], &emitted[0], total);
let chroma_class = sao_class_for(&SAO_CTB_MIX[1], &emitted[1], total);
emitted[0][luma_class] += 1;
emitted[1][chroma_class] += 1;
grid.push(ResolvedSao {
components: [
sao_component_for(luma_class, i, 0),
sao_component_for(chroma_class, i, 1),
sao_component_for(chroma_class, i, 2),
],
});
}
(picture, grid)
}
fn build_sao_boundaries(width: usize, height: usize, grid: &[ResolvedSao]) -> (SaoBoundaries, u64) {
let ctb = 1usize << SAO_CTB_LOG2;
let ctbs_x = width.div_ceil(ctb);
let ctbs_y = height.div_ceil(ctb);
let mut samples = 0u64;
for (i, resolved) in grid.iter().enumerate() {
let (rx, ry) = (i % ctbs_x, i / ctbs_x);
let w = ctb.min(width.saturating_sub(rx * ctb));
let h = ctb.min(height.saturating_sub(ry * ctb));
if resolved.components[0].sao_type_idx != 0 {
samples += (w * h) as u64;
}
for cidx in 1..3 {
if resolved.components[cidx].sao_type_idx != 0 {
samples += ((w / 2) * (h / 2)) as u64;
}
}
}
let n = ctbs_x * ctbs_y;
let boundaries = SaoBoundaries {
slice_addr_of_ctb: vec![0; n],
tile_id_of_ctb: vec![0; n],
pic_w_ctbs: ctbs_x,
ctb_log2_size_y: SAO_CTB_LOG2,
across_slices: true,
across_tiles: true,
filter_across_of_ctb: Some(vec![true; n]),
ctb_ts_of_rs: Some((0..n as u32).collect()),
};
(boundaries, samples)
}
fn build_cabac_bitstream() -> Vec<u8> {
let mut rng = Lcg::new(0xCABA_C0DE);
(0..CABAC_BINS).map(|_| rng.below(256) as u8).collect()
}
fn convert_config(width: usize, height: usize) -> VideoDecoderConfig {
let dimensions = VideoDimensions::new(width as u32, height as u32, &Limits::default())
.expect("the benchmark frame is within the default limits");
VideoDecoderConfig {
codec: Codec::Hevc,
profile: CodecProfile::HevcMain,
coded_dimensions: dimensions,
output_format: PixelFormat::Rgba8,
color_range: ColorRange::Limited,
hardware: HardwarePreference::Avoid,
configuration: Vec::new(),
}
}
#[must_use]
pub fn decode_frames(
configuration: &VideoDecoderConfig,
samples: &[EncodedVideoSample],
limits: &Limits,
frames: u64,
) -> Vec<u8> {
let mut decoder = bench_decoder(configuration, limits);
let cancellation = CancellationToken::new();
let mut digest = Digest::new();
let mut decoded = 0_u64;
for sample in samples {
for frame in decoder
.submit(sample, &cancellation)
.expect("the sample decodes")
{
for plane in &frame.frame.planes {
digest.push_bytes(&plane.data);
}
decoded += 1;
}
if decoded >= frames {
break;
}
}
assert_frame_count(decoded, frames);
digest.finish()
}
fn bench_decoder(configuration: &VideoDecoderConfig, limits: &Limits) -> HevcDecoder {
let parsed = ParsedConfiguration::parse(configuration, limits)
.expect("the sample's configuration record parses");
HevcDecoder::new(configuration.clone(), *limits, parsed)
.expect("the software HEVC decoder is constructible")
}
fn assert_frame_count(decoded: u64, frames: u64) {
assert!(
decoded >= frames,
"the sample yielded {decoded} decoded pictures, not the {frames} the group reports"
);
}
#[must_use]
pub fn decode_pictures(
configuration: &VideoDecoderConfig,
samples: &[EncodedVideoSample],
limits: &Limits,
frames: u64,
) -> Vec<u8> {
let mut decoder = bench_decoder(configuration, limits);
let cancellation = CancellationToken::new();
let mut digest = Digest::new();
let mut decoded = 0_u64;
for sample in samples {
for picture in decoder
.submit_pictures(sample, &cancellation)
.expect("the sample decodes")
{
for plane in [Plane::Luma, Plane::Cb, Plane::Cr] {
digest.push_samples(picture.plane(plane));
}
decoded += 1;
}
if decoded >= frames {
break;
}
}
assert_frame_count(decoded, frames);
digest.finish()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::simd::{self, SimdIsa};
fn small_inputs() -> HevcStageInputs {
HevcStageInputs::new(256, 128)
}
#[test]
fn the_inter_pred_schedule_runs_the_measured_prediction_unit_mix() {
let pus = schedule_inter_pus(1920, 1088);
let total: u64 = pus.iter().map(|pu| (pu.n * pu.n) as u64).sum();
assert!(total > 0, "the schedule emitted no prediction units");
for &(n, bi, share) in &INTER_PU_MIX {
let got: u64 = pus
.iter()
.filter(|pu| pu.n == n && pu.bi == bi)
.map(|pu| (pu.n * pu.n) as u64)
.sum();
let got = got as f64 / total as f64;
assert!(
(got - share).abs() < 0.01,
"{n}x{n} {} is {:.1}% of luma samples, measured {:.1}%",
if bi { "bi" } else { "uni" },
got * 100.0,
share * 100.0,
);
}
let bi: u64 = pus
.iter()
.filter(|pu| pu.bi)
.map(|pu| (pu.n * pu.n) as u64)
.sum();
let bi = bi as f64 / total as f64;
assert!((0.55..0.70).contains(&bi), "bi-predicted share is {bi}");
for side in [8usize, 16, 32, 64] {
assert!(
pus.iter().any(|pu| pu.n == side),
"no {side}x{side} prediction unit was scheduled"
);
}
assert!(
pus.iter()
.all(|pu| pu.n >= 8 && pu.x % 2 == 0 && pu.y % 2 == 0),
"a unit has no whole 4:2:0 chroma unit"
);
}
#[test]
fn every_stage_is_bit_exact_across_instruction_sets() {
let _guard = simd::test_lock();
let inputs = small_inputs();
type Stage = (&'static str, fn(&HevcStageInputs) -> Vec<u8>);
let stages: [Stage; 7] = [
("inter_pred", HevcStageInputs::run_inter_pred),
("intra_pred", HevcStageInputs::run_intra_pred),
("deblock", HevcStageInputs::run_deblock),
("sao", HevcStageInputs::run_sao),
("inverse_transform", HevcStageInputs::run_inverse_transform),
("color_convert", HevcStageInputs::run_color_convert),
("cabac", HevcStageInputs::run_cabac),
];
for (name, run) in stages {
simd::set_override(Some(SimdIsa::Scalar));
let reference = run(&inputs);
for isa in simd::available() {
simd::set_override(Some(isa));
assert_eq!(
run(&inputs),
reference,
"{name} on {} diverged from the scalar reference",
isa.name()
);
}
}
simd::set_override(None);
}
#[test]
fn the_sao_grid_runs_the_measured_parameter_mix() {
let inputs = HevcStageInputs::new(1920, 1088);
let total = inputs.sao_ctbs.len() as f64;
assert!(total > 0.0, "the grid has CTBs");
for (row, cidx) in [(0usize, 0usize), (1, 1), (1, 2)] {
for (class, &want) in SAO_CTB_MIX[row].iter().enumerate() {
let got = inputs
.sao_ctbs
.iter()
.filter(|r| {
let c = &r.components[cidx];
let seen = match c.sao_type_idx {
0 => 0,
1 => 1,
_ => 2 + usize::from(c.eo_class),
};
seen == class
})
.count() as f64
/ total;
assert!(
(got - want).abs() < 0.01,
"component {cidx} class {class}: {got:.4} against the measured {want:.4}"
);
}
}
assert!(
inputs.sao_ctbs.iter().all(|r| r.components[1].sao_type_idx
== r.components[2].sao_type_idx
&& r.components[1].eo_class == r.components[2].eo_class),
"Cb and Cr take the same SAO type and class"
);
let all = (1920u64 * 1088) * 3 / 2;
assert!(
inputs.sao_samples() < all / 4,
"{} filtered samples against {all} in the picture",
inputs.sao_samples()
);
}
#[test]
fn stage_digests_depend_on_the_stage_output() {
let _guard = simd::test_lock();
simd::set_override(Some(SimdIsa::Scalar));
let inputs = small_inputs();
let empty = Digest::new().finish();
let digests = [
inputs.run_inter_pred(),
inputs.run_intra_pred(),
inputs.run_deblock(),
inputs.run_sao(),
inputs.run_inverse_transform(),
inputs.run_color_convert(),
inputs.run_cabac(),
];
simd::set_override(None);
for digest in &digests {
assert_eq!(digest.len(), 8, "a digest is eight bytes");
assert_ne!(*digest, empty, "a stage folded nothing");
}
for (i, a) in digests.iter().enumerate() {
for b in &digests[i + 1..] {
assert_ne!(a, b, "two stages produced the same digest");
}
}
}
#[test]
fn the_deblocking_content_contains_flat_edges() {
let inputs = small_inputs();
let flat_edges = inputs
.luma_edges()
.iter()
.filter(|pos| {
let row = pos.ey * inputs.width;
let span = |from: usize| {
let window = &inputs.luma[row + from..row + from + 4];
window.iter().max().unwrap() - window.iter().min().unwrap()
};
span(pos.ex - 4) <= 2 && span(pos.ex) <= 2
})
.count();
assert!(
flat_edges > inputs.luma_edges().len() / 10,
"only {flat_edges} of {} edges are flat; the wide filter would go untimed",
inputs.luma_edges().len()
);
}
#[test]
fn reported_sample_counts_match_the_workloads() {
let inputs = small_inputs();
assert_eq!(inputs.inter_pred_samples(), 3 * 64 * 64);
assert!(
inputs.sao_samples() > 0 && inputs.sao_samples() < 256 * 128 * 3 / 2,
"sao_samples was {}",
inputs.sao_samples()
);
assert_eq!(
inputs.deblock_samples(),
inputs.luma_edges().len() as u64 * 32
);
assert_eq!(inputs.cabac_bins(), CABAC_BINS as u64);
assert!(inputs.intra_pred_samples() > 0);
assert!(inputs.inverse_transform_samples() > 0);
}
#[test]
fn the_picture_only_path_decodes_what_the_rgba_path_decodes() {
let (configuration, samples) = pcm_stream();
let limits = Limits::default();
let cancellation = CancellationToken::new();
let mut rgba_decoder = new_decoder(&configuration, &limits);
let mut expected = Vec::new();
for sample in &samples {
for frame in rgba_decoder.submit(sample, &cancellation).unwrap() {
expected.push(frame.frame);
}
}
assert!(
!expected.is_empty(),
"the generated stream has to produce output frames for this to test anything"
);
let mut picture_decoder = new_decoder(&configuration, &limits);
let mut actual = Vec::new();
for sample in &samples {
for picture in picture_decoder
.submit_pictures(sample, &cancellation)
.unwrap()
{
actual.push(picture_to_rgba(&picture, &configuration, &limits).unwrap());
}
}
assert_eq!(actual, expected);
}
#[test]
fn decode_pictures_folds_the_decoded_samples_reproducibly() {
let (configuration, samples) = pcm_stream();
let limits = Limits::default();
let one = decode_pictures(&configuration, &samples, &limits, 1);
assert_eq!(one, decode_pictures(&configuration, &samples, &limits, 1));
assert_ne!(one, decode_pictures(&configuration, &samples, &limits, 2));
assert_ne!(one, Digest::new().finish());
}
#[test]
fn the_two_whole_frame_paths_fold_the_same_number_of_frames() {
let (configuration, samples) = pcm_stream();
let limits = Limits::default();
let frames = decode_frames(&configuration, &samples, &limits, 2);
assert_eq!(frames, decode_frames(&configuration, &samples, &limits, 2));
assert_ne!(
frames,
decode_pictures(&configuration, &samples, &limits, 2)
);
assert_ne!(frames, Digest::new().finish());
}
#[test]
fn color_convert_folds_the_converted_frame() {
let inputs = small_inputs();
let converted = inputs.run_color_convert();
assert_eq!(converted, inputs.run_color_convert());
assert_ne!(converted, HevcStageInputs::new(128, 64).run_color_convert());
assert_eq!(inputs.color_convert_samples(), 256 * 128);
}
fn pcm_stream() -> (VideoDecoderConfig, Vec<EncodedVideoSample>) {
use crate::FrameIndex;
use zvidlib_hevc_encoder::encoder::{hvcc_box, length_prefixed_vcl};
use zvidlib_hevc_encoder::engine::encoder::pcm::encode_idr_pcm_au;
const SIDE: usize = 16;
const FRAMES: u64 = 4;
let mut rng = Lcg::new(0x8EEF_0220);
let y: Vec<u8> = (0..SIDE * SIDE).map(|_| rng.below(256) as u8).collect();
let chroma: Vec<u8> = (0..SIDE * SIDE / 4).map(|_| rng.below(256) as u8).collect();
let au = encode_idr_pcm_au(&y, &chroma, &chroma, SIDE, SIDE).unwrap();
let data = length_prefixed_vcl(&au).unwrap();
let mut configuration = convert_config(SIDE, SIDE);
configuration.configuration = hvcc_box(&au).unwrap();
let samples = (0..FRAMES)
.map(|index| EncodedVideoSample {
presentation_index: FrameIndex(index),
random_access: true,
data: data.clone(),
})
.collect();
(configuration, samples)
}
fn new_decoder(configuration: &VideoDecoderConfig, limits: &Limits) -> HevcDecoder {
let parsed = ParsedConfiguration::parse(configuration, limits).unwrap();
HevcDecoder::new(configuration.clone(), *limits, parsed).unwrap()
}
}