Skip to main content

decode_threads/
decode_threads.rs

1//! Decode-budget bench: a synthetic 12 MP (4000 × 3000) 4:2:0 still is
2//! coded as a multi-tile AV1 item (oxideav-av1's still encoder, tiles
3//! derived from the thread count), wrapped in an AVIF, then decoded
4//! through the registry decoder serial and under an
5//! `ExecutionContext` budget. Prints the wall clock of each and checks
6//! the outputs are byte-identical. A second leg codes the same picture
7//! as a HEIF `grid` of single-tile items (`encode_still_grid`, 4 × 3
8//! tiles) — the budget is then spent across the grid's tiles.
9//!
10//! ```text
11//! cargo run --release --example decode_threads -- [threads] [width] [height]
12//! ```
13
14use std::time::Instant;
15
16use oxideav_av1::encoder::still::{encode_still_yuv, StillOptions};
17use oxideav_av1::encoder::yuv_frame::{ChromaFormat, YuvFrame};
18use oxideav_av1::Av1CodecConfig;
19use oxideav_avif::{encode_still_grid, EncodeOptions, StillChroma, StillImage};
20use oxideav_core::{CodecId, CodecParameters, ExecutionContext, Frame, Packet, TimeBase};
21
22fn plane(w: u32, h: u32, seed: u32) -> Vec<u16> {
23    (0..u64::from(w) * u64::from(h))
24        .map(|i| {
25            let x = (i % u64::from(w)) as f64;
26            let y = (i / u64::from(w)) as f64;
27            let s = f64::from(seed);
28            let v = 128.0
29                + 60.0 * ((x * 0.013 + s).sin() * (y * 0.011 - s).cos())
30                + 30.0 * ((x * 0.21 + y * 0.17).sin())
31                + ((i.wrapping_mul(2654435761) >> 29) & 7) as f64;
32            v.clamp(0.0, 255.0) as u16
33        })
34        .collect()
35}
36
37fn decode(bytes: &[u8], threads: usize) -> (Vec<u8>, f64) {
38    let params = CodecParameters::video(CodecId::new(oxideav_avif::CODEC_ID_STR));
39    let mut dec = oxideav_avif::make_decoder(&params).expect("decoder");
40    if threads > 1 {
41        dec.set_execution_context(&ExecutionContext::with_threads(threads));
42    }
43    let t = Instant::now();
44    dec.send_packet(&Packet::new(0, TimeBase::new(1, 1), bytes.to_vec()))
45        .expect("decode");
46    let frame = match dec.receive_frame().expect("frame") {
47        Frame::Video(v) => v,
48        other => panic!("unexpected {other:?}"),
49    };
50    let secs = t.elapsed().as_secs_f64();
51    let mut out = Vec::new();
52    for p in &frame.planes[..frame.image_plane_count()] {
53        out.extend_from_slice(&p.data);
54    }
55    (out, secs)
56}
57
58fn main() {
59    let mut args = std::env::args().skip(1);
60    let threads: usize = args.next().and_then(|s| s.parse().ok()).unwrap_or(8);
61    let width: u32 = args.next().and_then(|s| s.parse().ok()).unwrap_or(4000);
62    let height: u32 = args.next().and_then(|s| s.parse().ok()).unwrap_or(3000);
63
64    let input = YuvFrame {
65        width,
66        height,
67        bit_depth: 8,
68        format: ChromaFormat::Yuv420,
69        y: plane(width, height, 1),
70        u: plane(width / 2, height / 2, 2),
71        v: plane(width / 2, height / 2, 3),
72    };
73    let t = Instant::now();
74    let coded = encode_still_yuv(&input, &StillOptions::new(120).threads(threads)).expect("encode");
75    let av1c = Av1CodecConfig::from_sequence_header(&coded.seq).to_bytes();
76    let file = oxideav_avif::encode_still_av1(
77        width,
78        height,
79        coded.temporal_unit_bytes,
80        av1c,
81        vec![8, 8, 8],
82    )
83    .expect("mux");
84    eprintln!(
85        "{width}x{height} 4:2:0 still: {} bytes, encoded in {:.2} s on {threads} threads",
86        file.len(),
87        t.elapsed().as_secs_f64()
88    );
89
90    let (serial, t1) = decode(&file, 1);
91    let (parallel, tn) = decode(&file, threads);
92    assert_eq!(serial, parallel, "byte-identical at any budget");
93    eprintln!(
94        "decode: serial {t1:.3} s, {threads} workers {tn:.3} s ({:.2}x), identical bytes",
95        t1 / tn
96    );
97
98    // The registry encoder on the same picture (quality 50): serial vs
99    // the budget — identical bytes, the wall clock is the point.
100    let registry_frame = Frame::Video(oxideav_core::frame::VideoFrame {
101        pts: Some(0),
102        planes: [
103            (&input.y, width),
104            (&input.u, width / 2),
105            (&input.v, width / 2),
106        ]
107        .iter()
108        .map(|(p, w)| oxideav_core::frame::VideoPlane {
109            stride: *w as usize,
110            data: p.iter().map(|&v| v as u8).collect(),
111        })
112        .collect(),
113    });
114    let mut p = CodecParameters::video(CodecId::new(oxideav_avif::CODEC_ID_STR));
115    p.width = Some(width);
116    p.height = Some(height);
117    p.pixel_format = Some(oxideav_core::PixelFormat::Yuv420P);
118    p.options = oxideav_core::CodecOptions::new().set("quality", "50");
119    let encode_registry = |budget: usize| -> (Vec<u8>, f64) {
120        let mut enc = oxideav_avif::make_encoder(&p).expect("encoder");
121        if budget > 1 {
122            enc.set_execution_context(&ExecutionContext::with_threads(budget));
123        }
124        let t = Instant::now();
125        enc.send_frame(&registry_frame).expect("send");
126        let pkt = enc.receive_packet().expect("packet");
127        (pkt.data, t.elapsed().as_secs_f64())
128    };
129    let (e1, te1) = encode_registry(1);
130    let (en, ten) = encode_registry(threads);
131    assert_eq!(e1, en, "registry encoder: byte-identical at any budget");
132    eprintln!(
133        "registry encode (quality 50): {} bytes, serial {te1:.2} s, {threads} workers {ten:.2} s ({:.2}x), identical bytes",
134        e1.len(),
135        te1 / ten
136    );
137
138    // The same picture as a 4 × 3 grid of single-tile items.
139    let t = Instant::now();
140    let still = StillImage::yuv(
141        width,
142        height,
143        8,
144        StillChroma::Yuv420,
145        input.y.clone(),
146        input.u.clone(),
147        input.v.clone(),
148    )
149    .expect("still");
150    let grid = encode_still_grid(&still, &EncodeOptions::new(120, 0, false), 4, 3).expect("grid");
151    eprintln!(
152        "{width}x{height} as a 4x3 grid: {} bytes, encoded in {:.2} s (serial encoder)",
153        grid.len(),
154        t.elapsed().as_secs_f64()
155    );
156    let (serial, t1) = decode(&grid, 1);
157    let (parallel, tn) = decode(&grid, threads);
158    assert_eq!(serial, parallel, "grid: byte-identical at any budget");
159    eprintln!(
160        "grid decode: serial {t1:.3} s, {threads} workers {tn:.3} s ({:.2}x), identical bytes",
161        t1 / tn
162    );
163}