zvidlib_hevc_decoder/decode_bench.rs
1//! Prepared per-stage workloads for the HEVC decoder benchmark.
2//!
3//! `crates/zvidlib-av1/benches/codec.rs` is an external crate and the HEVC engine
4//! (`crate::engine`) is crate-private, so a benchmark cannot reach the
5//! individual decode stages the way it reaches the public AV1 kernels. This
6//! module is the narrow surface that closes that gap: one prepared workload per
7//! hot stage, each with its input construction in [`HevcStageInputs::new`] and
8//! only the kernel under test inside the `run_*` method.
9//!
10//! Every `run_*` method returns the eight bytes of an FNV-1a fold over all of
11//! the samples the stage produced. The bytes exist for the benchmark harness's
12//! bit-exactness guard, which compares one instruction set's arm against the
13//! scalar arm; folding rather than returning the samples themselves keeps a
14//! multi-megabyte allocation out of the timed loop, and the fold still depends
15//! on every output sample, so a backend that diverged anywhere changes it.
16//!
17//! None of these methods pin an instruction set. They call the same entry
18//! points the decoder calls, which resolve through
19//! [`crate::simd::set_override`], so the caller selects the arm exactly as it
20//! does for a whole-frame decode.
21
22use super::engine::deblock::{EdgePos, EdgeQp, EdgeType, SamplePlane, filter_luma_block_edge};
23use super::engine::inter_pred::{
24 RefPlane, default_weighted_pred, interp_chroma_block, interp_luma_block,
25};
26use super::engine::intra_pred::{
27 Component as IntraComponent, IntraPredParams, ReferenceSamples, intra_predict,
28};
29use super::engine::picture::{Picture, Plane};
30use super::engine::sao::{
31 ResolvedSao, ResolvedSaoComponent, SaoBoundaries, apply_sao_picture_with_boundaries,
32};
33use super::engine::transform::{BlockParams, Component as TxComponent, PredMode, residual_block};
34use super::engine::{BitReader, CabacEngine, ContextModel};
35use super::{HevcDecoder, ParsedConfiguration, picture_to_rgba};
36use crate::{
37 CancellationToken, Codec, CodecProfile, ColorRange, EncodedVideoSample, HardwarePreference,
38 Limits, PixelFormat, VideoDecoder, VideoDecoderConfig, VideoDimensions,
39};
40
41/// The largest inter prediction unit, and the cell the §8.5.3.3 workload's
42/// prediction-unit schedule is laid out on: every cell is filled by prediction
43/// units of one class, so a cell is either one 64x64 unit or the 4, 16 or 64
44/// smaller ones that tile it.
45const INTER_CELL: usize = 64;
46
47/// The §8.5.3.3 prediction-unit mix a real decode runs, as issue #280 measured
48/// it: 48 frames of `examples/media/BigBuckBunny.mp4` at 1920x1080, every
49/// prediction unit the decoder reconstructed, weighted by luma sample.
50///
51/// Each entry is `(luma side, bi-predicted, share of luma samples)`. The
52/// workload this drives replaced a uniform grid of 16x16 bi-predicted
53/// *luma-only* blocks, which that measurement showed was not what the stage
54/// runs on real content in any of the three ways that matter to a vector
55/// kernel:
56///
57/// * **Size.** 62% of predicted luma samples are in 64x64 units and 31% in
58/// 32x32; 16x16 is 5% and 8x8 is 1.4%. The old grid's block was the third
59/// most common size by sample and carried 5% of the real work.
60/// * **List utilisation.** 38% of predicted luma samples are uni-predicted,
61/// which runs one interpolation rather than two and takes the
62/// §8.5.3.3.4.2 single-list combine rather than the two-list one. The old
63/// grid was bi-predicted throughout.
64/// * **Component.** At 4:2:0 every luma sample brings half a chroma sample,
65/// through the §8.5.3.3.3.3 4-tap filter rather than the 8-tap one — a
66/// third of the stage's samples, on a kernel that measures materially lower
67/// than the luma one. The old grid predicted no chroma at all.
68///
69/// The clip codes 2Nx2N prediction units throughout, so every size here is
70/// square; a stream using the §7.3.8.5 asymmetric partitions would add
71/// rectangular units, and this table would be re-measured rather than guessed.
72const INTER_PU_MIX: [(usize, bool, f64); 8] = [
73 (64, true, 0.432),
74 (64, false, 0.189),
75 (32, true, 0.162),
76 (32, false, 0.149),
77 (16, false, 0.036),
78 (16, true, 0.017),
79 (8, false, 0.009),
80 (8, true, 0.005),
81];
82/// The §7.3.8.3 SAO parameter mix a real decode runs, as issue #310 measured
83/// it: 48 frames of `examples/media/BigBuckBunny.mp4` at 1920x1080, all 26,520
84/// coding tree blocks the decoder resolved parameters for, counted per colour
85/// component.
86///
87/// Row 0 is luma, row 1 chroma — Cb and Cr came out identical CTB for CTB, as
88/// §7.3.8.3 signals one `sao_type_idx_chroma` and one `SaoEoClass` for the
89/// pair. Each row is the share of CTBs taking, in order: no SAO at all
90/// (`SaoTypeIdx == 0`), band offset, and the four edge-offset classes
91/// (0-degree, 90-degree, 135-degree, 45-degree).
92///
93/// The workload this drives replaced a grid that put band or edge offset on
94/// *every* CTB of *every* component, which is not what the stage runs on real
95/// content in the way that matters most to it: **SAO is off on 86.7% of luma
96/// CTBs and 94.4% of chroma CTBs**, and an off CTB costs nothing at all. The
97/// old grid therefore filtered about eight times the samples a 1080p frame
98/// actually puts through the classifiers, so its ratio was the kernels'
99/// throughput on a saturated picture rather than their worth to a decode.
100const SAO_CTB_MIX: [[f64; 6]; 2] = [
101 // off, band, EO 0-deg, EO 90-deg, EO 135-deg, EO 45-deg
102 [0.866780, 0.020362, 0.020777, 0.030656, 0.026697, 0.034729],
103 [0.944155, 0.009540, 0.001357, 0.004450, 0.015875, 0.024623],
104];
105/// The CTB side the SAO stage is driven at, as `log2`.
106const SAO_CTB_LOG2: u32 = 6;
107/// Bit depth every stage runs at; the bundled sample is 8-bit Main.
108const BIT_DEPTH: u8 = 8;
109/// Transform-block sides the inverse-transform stage covers.
110const TX_SIZES: [usize; 4] = [4, 8, 16, 32];
111/// CABAC bins one iteration of the entropy stage decodes.
112const CABAC_BINS: usize = 1 << 18;
113
114/// One scheduled §8.5.3.3 prediction unit of the inter-prediction workload.
115#[derive(Debug, Clone, Copy)]
116struct InterPu {
117 /// Top-left luma position, in samples.
118 x: i32,
119 y: i32,
120 /// Luma side; the unit is square, as every unit the measured clip codes is.
121 n: usize,
122 /// `predFlagL1` — whether this unit runs two interpolations and the
123 /// two-list §8.5.3.3.4.2 combine, or one and the single-list one.
124 bi: bool,
125 /// Which fractional phase pair this unit filters at, so the schedule
126 /// sweeps the one- and two-dimensional filter paths and the copy path.
127 phase: i32,
128}
129
130/// Lay out one frame's worth of prediction units matching [`INTER_PU_MIX`].
131///
132/// The frame is walked in [`INTER_CELL`]-sized cells, and each cell is filled
133/// entirely by units of one class. The class is chosen greedily — whichever is
134/// furthest below its target share of luma samples so far — so the emitted mix
135/// converges on the measured one exactly rather than approximately, and does so
136/// deterministically, which is what the benchmark's bit-exactness guard needs.
137///
138/// A margin of one cell is left on the right and bottom edges because the
139/// workload offsets its second reference list by one sample; every unit and
140/// both of its lists therefore lie inside the plane.
141fn schedule_inter_pus(width: usize, height: usize) -> Vec<InterPu> {
142 let cells_x = (width / INTER_CELL).saturating_sub(1);
143 let cells_y = (height / INTER_CELL).saturating_sub(1);
144 let mut emitted = [0u64; INTER_PU_MIX.len()];
145 let mut total = 0u64;
146 let mut pus = Vec::new();
147 let mut phase = 0i32;
148 for cy in 0..cells_y {
149 for cx in 0..cells_x {
150 // Furthest below target, measured as a sample deficit so the very
151 // first cell (where every share is zero) still picks the largest.
152 let class = (0..INTER_PU_MIX.len())
153 .max_by(|&a, &b| {
154 let deficit = |i: usize| {
155 INTER_PU_MIX[i]
156 .2
157 .mul_add(total as f64, -(emitted[i] as f64))
158 };
159 deficit(a)
160 .partial_cmp(&deficit(b))
161 .expect("the shares are finite")
162 })
163 .expect("the mix is not empty");
164 let (n, bi, _) = INTER_PU_MIX[class];
165 for by in (0..INTER_CELL).step_by(n) {
166 for bx in (0..INTER_CELL).step_by(n) {
167 pus.push(InterPu {
168 x: (cx * INTER_CELL + bx) as i32,
169 y: (cy * INTER_CELL + by) as i32,
170 n,
171 bi,
172 phase,
173 });
174 phase = (phase + 1) % 16;
175 }
176 }
177 let cell_samples = (INTER_CELL * INTER_CELL) as u64;
178 emitted[class] += cell_samples;
179 total += cell_samples;
180 }
181 }
182 pus
183}
184
185/// An FNV-1a fold, so a stage's whole output identifies it in eight bytes.
186struct Digest(u64);
187
188impl Digest {
189 fn new() -> Self {
190 Digest(0xcbf2_9ce4_8422_2325)
191 }
192
193 fn push_i32(&mut self, value: i32) {
194 for byte in value.to_le_bytes() {
195 self.0 ^= u64::from(byte);
196 self.0 = self.0.wrapping_mul(0x1000_0000_01b3);
197 }
198 }
199
200 fn push_all(&mut self, values: &[i32]) {
201 for &value in values {
202 self.push_i32(value);
203 }
204 }
205
206 /// Folds `value` in one step.
207 fn push_u64(&mut self, value: u64) {
208 self.0 = (self.0 ^ value).wrapping_mul(0x1000_0000_01b3);
209 }
210
211 /// Folds a whole plane of decoded samples, two at a time.
212 ///
213 /// [`push_all`] costs a multiply per *byte*, which is nothing next to a
214 /// per-stage workload of a few thousand samples but is real work next to a
215 /// whole-frame group that folds several million per iteration, inside the
216 /// timed loop. This is the wide step for those; every sample still reaches
217 /// the accumulator, which is what the harness's bit-exactness guard needs.
218 ///
219 /// [`push_all`]: Self::push_all
220 fn push_samples(&mut self, values: &[i32]) {
221 let mut chunks = values.chunks_exact(2);
222 for pair in &mut chunks {
223 self.push_u64(u64::from(pair[0] as u32) | (u64::from(pair[1] as u32) << 32));
224 }
225 for &value in chunks.remainder() {
226 self.push_u64(u64::from(value as u32));
227 }
228 }
229
230 /// Folds a byte buffer eight bytes at a time, for the same reason as
231 /// [`push_samples`].
232 ///
233 /// [`push_samples`]: Self::push_samples
234 fn push_bytes(&mut self, bytes: &[u8]) {
235 let mut chunks = bytes.chunks_exact(8);
236 for chunk in &mut chunks {
237 self.push_u64(u64::from_le_bytes(
238 chunk.try_into().expect("the chunk is eight bytes"),
239 ));
240 }
241 for &byte in chunks.remainder() {
242 self.push_u64(u64::from(byte));
243 }
244 }
245
246 fn finish(self) -> Vec<u8> {
247 self.0.to_le_bytes().to_vec()
248 }
249}
250
251/// A deterministic, dependency-free generator for benchmark content.
252///
253/// Reproducibility is the point: the bit-exactness guard runs the same workload
254/// once per instruction set and compares the results, so the inputs have to be
255/// identical across those runs and across processes.
256struct Lcg(u64);
257
258impl Lcg {
259 fn new(seed: u64) -> Self {
260 Lcg(seed | 1)
261 }
262
263 fn next_u32(&mut self) -> u32 {
264 self.0 = self
265 .0
266 .wrapping_mul(6_364_136_223_846_793_005)
267 .wrapping_add(1_442_695_040_888_963_407);
268 (self.0 >> 33) as u32
269 }
270
271 fn below(&mut self, bound: u32) -> u32 {
272 self.next_u32() % bound
273 }
274}
275
276/// Synthetic 8-bit luma content mixing textured and flat regions.
277///
278/// The flat regions are what make this usable for deblocking: the §8.7.2.5.3
279/// wide-filter decision is gated on a flatness check across the edge, so a
280/// purely textured plane never reaches the strong filter and would measure only
281/// half the kernel. Every other stage sees a mixture, which is what real content
282/// looks like.
283fn synthetic_luma(width: usize, height: usize, seed: u64) -> Vec<i32> {
284 let mut rng = Lcg::new(seed);
285 let mut plane = vec![0i32; width * height];
286 for y in 0..height {
287 // Alternating 32-row bands: textured, then flat with a small step every
288 // 8 columns so the deblocking edges have something to smooth.
289 let flat_band = (y / 32) % 2 == 1;
290 for x in 0..width {
291 let value = if flat_band {
292 let level = 40 + 12 * ((x / 8) % 5) as i32;
293 level + i32::from((x % 8) == 0)
294 } else {
295 let base = ((x * 3 + y * 5) % 200) as i32;
296 base + (rng.below(24) as i32) - 12
297 };
298 plane[y * width + x] = value.clamp(0, 255);
299 }
300 }
301 plane
302}
303
304/// Prepared inputs for every HEVC stage the benchmark times.
305///
306/// Construction is the setup half — plane allocation, content generation,
307/// reference-sample and coefficient-block construction, CABAC bitstream
308/// synthesis — and never appears inside a timed loop.
309pub struct HevcStageInputs {
310 width: usize,
311 height: usize,
312 luma: Vec<i32>,
313 intra_blocks: Vec<(ReferenceSamples, IntraPredParams)>,
314 intra_samples: u64,
315 tx_blocks: Vec<(Vec<i32>, BlockParams)>,
316 tx_samples: u64,
317 /// A full 8-bit 4:2:0 picture of synthetic content, driven by both the
318 /// §8.7.3 SAO stage and the output-conversion stage — the second needs
319 /// exactly what the first already builds, a whole decoded picture.
320 picture: Picture,
321 sao_ctbs: Vec<ResolvedSao>,
322 /// The single-slice, single-tile §8.7.3.2 boundary grids the stage is
323 /// driven with. A decode always has these — whether a stream is
324 /// single-slice and single-tile is not known before it is parsed — so
325 /// running the stage without them (as this group did before issue #310)
326 /// measures a dispatch the decoder never takes.
327 sao_boundaries: SaoBoundaries,
328 /// Samples the SAO classifiers actually run over per run, luma plus
329 /// chroma: [`SAO_CTB_MIX`] leaves most CTBs with `SaoTypeIdx == 0`, and
330 /// those cost nothing.
331 sao_filtered_samples: u64,
332 /// The §8.5.3.3 prediction units one run of the stage reconstructs, in
333 /// raster order, built to [`INTER_PU_MIX`]. Scheduling them is setup, so
334 /// it happens here rather than inside the timed loop.
335 inter_pus: Vec<InterPu>,
336 inter_luma_samples: u64,
337 convert_config: VideoDecoderConfig,
338 cabac_bytes: Vec<u8>,
339}
340
341impl HevcStageInputs {
342 /// Builds every stage's inputs for a `width` x `height` luma frame.
343 ///
344 /// # Panics
345 /// Panics if the dimensions are not a multiple of the 64-sample CTB the SAO
346 /// stage is driven at, or if either is zero.
347 #[must_use]
348 pub fn new(width: usize, height: usize) -> Self {
349 assert!(width > 0 && height > 0, "the frame has to have samples");
350 let luma = synthetic_luma(width, height, 0x5EED_1234);
351 let intra_blocks = build_intra_blocks();
352 let intra_samples = intra_blocks
353 .iter()
354 .map(|(p, _)| (p.n_tbs() * p.n_tbs()) as u64)
355 .sum();
356 let tx_blocks = build_tx_blocks();
357 let tx_samples = tx_blocks.iter().map(|(l, _)| l.len() as u64).sum();
358 let (picture, sao_ctbs) = build_sao_inputs(width, height, &luma);
359 let (sao_boundaries, sao_filtered_samples) = build_sao_boundaries(width, height, &sao_ctbs);
360 let inter_pus = schedule_inter_pus(width, height);
361 let inter_luma_samples = inter_pus.iter().map(|pu| (pu.n * pu.n) as u64).sum();
362 Self {
363 width,
364 height,
365 luma,
366 intra_blocks,
367 intra_samples,
368 tx_blocks,
369 tx_samples,
370 picture,
371 sao_ctbs,
372 sao_boundaries,
373 sao_filtered_samples,
374 inter_pus,
375 inter_luma_samples,
376 convert_config: convert_config(width, height),
377 cabac_bytes: build_cabac_bitstream(),
378 }
379 }
380
381 /// Luma samples the inter-prediction stage writes per run.
382 ///
383 /// Luma only, so the figure stays comparable with the one this group
384 /// reported before it predicted chroma; each of these samples brings half
385 /// a chroma sample of §8.5.3.3.3.3 work with it at 4:2:0.
386 #[must_use]
387 pub fn inter_pred_samples(&self) -> u64 {
388 self.inter_luma_samples
389 }
390
391 /// Samples the intra-prediction stage writes per run.
392 #[must_use]
393 pub fn intra_pred_samples(&self) -> u64 {
394 self.intra_samples
395 }
396
397 /// Luma samples the deblocking stage rewrites per run.
398 #[must_use]
399 pub fn deblock_samples(&self) -> u64 {
400 (self.luma_edges().len() * 4 * 8) as u64
401 }
402
403 /// Samples the SAO stage classifies per run, luma plus chroma.
404 ///
405 /// Counts only the CTBs `SAO_CTB_MIX` leaves switched on, since a CTB
406 /// with `SaoTypeIdx == 0` is returned from before it reads a sample. On the
407 /// measured mix that is a small minority of the picture, which is the
408 /// point: the throughput this divides is the classifiers', not the frame's.
409 #[must_use]
410 pub fn sao_samples(&self) -> u64 {
411 self.sao_filtered_samples
412 }
413
414 /// Residual samples the inverse-transform stage produces per run.
415 #[must_use]
416 pub fn inverse_transform_samples(&self) -> u64 {
417 self.tx_samples
418 }
419
420 /// Samples the output-conversion stage writes per run, one RGBA pixel each.
421 #[must_use]
422 pub fn color_convert_samples(&self) -> u64 {
423 (self.width * self.height) as u64
424 }
425
426 /// Bins the entropy stage decodes per run.
427 #[must_use]
428 pub fn cabac_bins(&self) -> u64 {
429 CABAC_BINS as u64
430 }
431
432 /// §8.5.3.3 — fractional-sample interpolation and the weighted combine,
433 /// over the prediction-unit mix a real decode runs.
434 ///
435 /// Each scheduled unit interpolates its luma through the §8.5.3.3.3.2
436 /// 8-tap filter and, at 4:2:0, its Cb and Cr through the §8.5.3.3.3.3
437 /// 4-tap filter, once per used reference list, and combines the result
438 /// through the §8.5.3.3.4.2 default weighted prediction. Every one of
439 /// those is a vectorized `filter_taps` / `combine_weighted` primitive.
440 ///
441 /// The unit sizes, the uni/bi split and the presence of chroma all come
442 /// from `INTER_PU_MIX`, which is measured rather than chosen; see that
443 /// constant for what the uniform 16x16 bi-predicted luma-only grid this
444 /// replaced was getting wrong.
445 ///
446 /// # Panics
447 /// Panics if the prepared planes no longer match their dimensions.
448 #[must_use]
449 pub fn run_inter_pred(&self) -> Vec<u8> {
450 let luma = RefPlane::new(&self.luma, self.width, self.height)
451 .expect("the prepared plane matches its dimensions");
452 let (cw, ch) = (self.width / 2, self.height / 2);
453 let cb = RefPlane::new(self.picture.plane(Plane::Cb), cw, ch)
454 .expect("the prepared picture's Cb plane matches its dimensions");
455 let cr = RefPlane::new(self.picture.plane(Plane::Cr), cw, ch)
456 .expect("the prepared picture's Cr plane matches its dimensions");
457 let mut digest = Digest::new();
458 for pu in &self.inter_pus {
459 let n = pu.n;
460 // Sweep the fractional phases so both the one-dimensional and the
461 // two-dimensional filter paths run, and so a unit that lands on a
462 // full-sample position (the copy path) is reached too.
463 let (fx0, fy0) = (pu.phase % 4, (pu.phase / 4) % 4);
464 let (fx1, fy1) = ((pu.phase + 1) % 4, (pu.phase + 3) % 4);
465 let l0 = interp_luma_block(&luma, pu.x, pu.y, fx0, fy0, n, n, BIT_DEPTH)
466 .expect("the unit lies inside the plane");
467 let l1 = if pu.bi {
468 interp_luma_block(&luma, pu.x + 1, pu.y + 1, fx1, fy1, n, n, BIT_DEPTH)
469 .expect("the unit lies inside the plane")
470 } else {
471 Vec::new()
472 };
473 let combined = default_weighted_pred(&l0, &l1, true, pu.bi, n, n, BIT_DEPTH)
474 .expect("both lists are the unit's size");
475 digest.push_all(&combined);
476
477 // §8.5.3.3.3.3: the chroma units are half the luma unit's side at
478 // 4:2:0, at eighth-sample phases rather than quarter-sample ones.
479 let (cn, cx, cy) = (n / 2, pu.x / 2, pu.y / 2);
480 let (cfx0, cfy0) = (pu.phase % 8, (pu.phase / 8) % 8);
481 let (cfx1, cfy1) = ((pu.phase + 3) % 8, (pu.phase + 5) % 8);
482 for plane in [&cb, &cr] {
483 let c0 = interp_chroma_block(plane, cx, cy, cfx0, cfy0, cn, cn, BIT_DEPTH)
484 .expect("the unit lies inside the plane");
485 let c1 = if pu.bi {
486 interp_chroma_block(plane, cx + 1, cy + 1, cfx1, cfy1, cn, cn, BIT_DEPTH)
487 .expect("the unit lies inside the plane")
488 } else {
489 Vec::new()
490 };
491 let combined = default_weighted_pred(&c0, &c1, true, pu.bi, cn, cn, BIT_DEPTH)
492 .expect("both lists are the unit's size");
493 digest.push_all(&combined);
494 }
495 }
496 digest.finish()
497 }
498
499 /// §8.4.4.2 — reference-sample smoothing and the planar / DC / angular
500 /// predictors, over every mode at every transform-block size.
501 #[must_use]
502 pub fn run_intra_pred(&self) -> Vec<u8> {
503 let mut digest = Digest::new();
504 for (samples, params) in &self.intra_blocks {
505 let pred = intra_predict(samples, params).expect("the prepared block is well-formed");
506 digest.push_all(&pred);
507 }
508 digest.finish()
509 }
510
511 /// §8.7.2.5.4 — the luma block-edge deblocking filter over a whole frame's
512 /// worth of vertical edge segments.
513 ///
514 /// `bS == 2` and a high QP put the §8.7.2.5.3 thresholds where the strong
515 /// filter is reachable; the plane's flat bands are what actually reach it.
516 #[must_use]
517 pub fn run_deblock(&self) -> Vec<u8> {
518 let mut samples = self.luma.clone();
519 let mut plane = SamplePlane {
520 samples: &mut samples,
521 width: self.width,
522 stride: self.width,
523 };
524 let qp = EdgeQp {
525 qp_q: 37,
526 qp_p: 37,
527 beta_offset_div2: 0,
528 tc_offset_div2: 0,
529 bit_depth: BIT_DEPTH,
530 };
531 for pos in self.luma_edges() {
532 filter_luma_block_edge(&mut plane, pos, 2, qp);
533 }
534 let mut digest = Digest::new();
535 digest.push_all(&samples);
536 digest.finish()
537 }
538
539 /// §8.7.3 — sample adaptive offset over every CTB of a full picture, with
540 /// both the band-offset and the edge-offset classifiers in the grid.
541 #[must_use]
542 pub fn run_sao(&self) -> Vec<u8> {
543 let filtered = apply_sao_picture_with_boundaries(
544 self.picture.clone(),
545 &self.sao_ctbs,
546 SAO_CTB_LOG2,
547 1,
548 true,
549 true,
550 Some(&self.sao_boundaries),
551 );
552 // Fold only the CTBs the mix switches on, which is what the stage
553 // produced: a `SaoTypeIdx == 0` CTB is returned from before it reads a
554 // sample, so its samples are the input's and folding them would be
555 // folding the picture rather than the stage. It would also cost several
556 // times what the stage does — [`SAO_CTB_MIX`] leaves most of a picture
557 // switched off — and criterion would be timing this fold.
558 let ctb = 1usize << SAO_CTB_LOG2;
559 let ctbs_x = self.width.div_ceil(ctb);
560 let mut digest = Digest::new();
561 for (i, resolved) in self.sao_ctbs.iter().enumerate() {
562 let (rx, ry) = (i % ctbs_x, i / ctbs_x);
563 for (cidx, plane) in [Plane::Luma, Plane::Cb, Plane::Cr].into_iter().enumerate() {
564 if resolved.components[cidx].sao_type_idx == 0 {
565 continue;
566 }
567 let step = if cidx == 0 { ctb } else { ctb / 2 };
568 let (pw, ph) = filtered.plane_dims(plane);
569 let (x0, y0) = (rx * step, ry * step);
570 let (w, h) = (step.min(pw - x0), step.min(ph - y0));
571 let samples = filtered.plane(plane);
572 for j in 0..h {
573 let o = (y0 + j) * pw + x0;
574 digest.push_samples(&samples[o..o + w]);
575 }
576 }
577 }
578 digest.finish()
579 }
580
581 /// §8.6.2 / §8.6.3 / §8.6.4 — dequantization and the inverse transform, at
582 /// every transform-block size and for both the DCT and the 4x4 intra DST.
583 #[must_use]
584 pub fn run_inverse_transform(&self) -> Vec<u8> {
585 let mut digest = Digest::new();
586 for (levels, params) in &self.tx_blocks {
587 let residual =
588 residual_block(levels, None, *params).expect("the prepared block is well-formed");
589 digest.push_all(&residual);
590 }
591 digest.finish()
592 }
593
594 /// The YUV420-to-RGBA output conversion every whole-frame decode ends in.
595 ///
596 /// Issue #220: this stage is not decoding, but it was a third of what the
597 /// whole-frame groups measure (see `benches/README.md`), so it is timed
598 /// directly rather than inferred by subtracting one whole-frame group from
599 /// another. Issue #219 gave it the [`color_convert`] kernel family, so its
600 /// arms now separate by instruction set like the decoding stages do, and
601 /// this group is what reports that.
602 ///
603 /// # Panics
604 /// Panics if the prepared picture no longer converts.
605 ///
606 /// [`color_convert`]: super::color_convert
607 #[must_use]
608 pub fn run_color_convert(&self) -> Vec<u8> {
609 let frame = picture_to_rgba(&self.picture, &self.convert_config, &Limits::default())
610 .expect("the prepared picture is Main-profile 8-bit 4:2:0");
611 let mut digest = Digest::new();
612 for plane in &frame.planes {
613 digest.push_bytes(&plane.data);
614 }
615 digest.finish()
616 }
617
618 /// §9.3.4 — the CABAC arithmetic decoder's bin loop.
619 ///
620 /// This stage has no vector path and is not expected to grow one: the
621 /// arithmetic decoder is inherently serial, each bin's range update
622 /// depending on the previous one's. It is measured because it is the serial
623 /// fraction that bounds what vectorizing everything else can buy — the
624 /// residual parser in `engine::residual` spends essentially all of its time
625 /// in these two calls, so their throughput is the Amdahl ceiling for the
626 /// whole-frame arms.
627 ///
628 /// The mixture is roughly the context-coded / bypass split of residual
629 /// parsing: three context-coded decisions per bypass bin.
630 #[must_use]
631 pub fn run_cabac(&self) -> Vec<u8> {
632 let mut contexts: Vec<ContextModel> = (0..64)
633 .map(|i| ContextModel::init(90 + (i as u8 % 60), 30))
634 .collect();
635 let mut engine =
636 CabacEngine::new(BitReader::new(&self.cabac_bytes)).expect("the buffer has bits");
637 let mut digest = Digest::new();
638 let mut acc = 0i32;
639 let context_count = contexts.len();
640 for i in 0..CABAC_BINS {
641 let bin = if i % 4 == 3 {
642 engine.decode_bypass()
643 } else {
644 engine.decode_decision(&mut contexts[i % context_count])
645 };
646 match bin {
647 Ok(bin) => acc = acc.wrapping_mul(3).wrapping_add(i32::from(bin)),
648 // A fixed buffer decoded to exhaustion stops here; the bin count
649 // reached is the same on every arm because the input is fixed.
650 Err(_) => break,
651 }
652 digest.push_i32(acc);
653 }
654 digest.finish()
655 }
656
657 /// Inter-prediction block grid, leaving a block of margin on each axis so
658 /// the `+1` L1 offset stays addressable.
659 /// Every vertical luma edge segment of the frame, on the §8.7.2.4 grid:
660 /// 8 samples apart across the edge, 4 rows apart along it.
661 fn luma_edges(&self) -> Vec<EdgePos> {
662 let mut edges = Vec::new();
663 let mut ey = 0;
664 while ey + 4 <= self.height {
665 let mut ex = 8;
666 while ex + 8 <= self.width {
667 edges.push(EdgePos {
668 ex,
669 ey,
670 edge: EdgeType::Vertical,
671 });
672 ex += 8;
673 }
674 ey += 4;
675 }
676 edges
677 }
678}
679
680/// One reference array and parameter set per (mode, size) pair, repeated so a
681/// run covers a frame-scale number of samples rather than a handful of blocks.
682fn build_intra_blocks() -> Vec<(ReferenceSamples, IntraPredParams)> {
683 const REPEATS: usize = 24;
684 let mut rng = Lcg::new(0x17AB_5EED);
685 let mut blocks = Vec::new();
686 for _ in 0..REPEATS {
687 for n_tbs in TX_SIZES {
688 let want = 2 * n_tbs;
689 let left: Vec<i32> = (0..want).map(|_| rng.below(256) as i32).collect();
690 let top: Vec<i32> = (0..want).map(|_| rng.below(256) as i32).collect();
691 let corner = rng.below(256) as i32;
692 let samples = ReferenceSamples::new(n_tbs, corner, left, top)
693 .expect("the arrays are 2*nTbS long and nTbS is legal");
694 for mode in 0..=34u8 {
695 blocks.push((
696 samples.clone(),
697 IntraPredParams {
698 pred_mode_intra: mode,
699 cidx: IntraComponent::Luma,
700 bit_depth: BIT_DEPTH,
701 bit_depth_luma: BIT_DEPTH,
702 intra_smoothing_disabled: false,
703 strong_intra_smoothing_enabled: true,
704 chroma_array_type_3: false,
705 disable_boundary_filter: false,
706 },
707 ));
708 }
709 }
710 }
711 blocks
712}
713
714/// Coefficient blocks at every transform size, sparse the way real residual is:
715/// energy concentrated in the low-frequency corner, most positions zero.
716fn build_tx_blocks() -> Vec<(Vec<i32>, BlockParams)> {
717 const BLOCKS_PER_SIZE: usize = 512;
718 let mut rng = Lcg::new(0x7A_5F0);
719 let mut blocks = Vec::new();
720 for n_tbs in TX_SIZES {
721 for i in 0..BLOCKS_PER_SIZE {
722 let mut levels = vec![0i32; n_tbs * n_tbs];
723 // A low-frequency sub-block of non-zero levels, as a coded
724 // sub-block group would leave.
725 let coded = (n_tbs / 2).max(4);
726 for y in 0..coded {
727 for x in 0..coded {
728 let magnitude = (rng.below(64) as i32) - 32;
729 levels[y * n_tbs + x] = magnitude;
730 }
731 }
732 let pred_mode = if i % 2 == 0 {
733 PredMode::Intra
734 } else {
735 PredMode::Inter
736 };
737 blocks.push((
738 levels,
739 BlockParams {
740 n_tbs,
741 q_p: 26 + (i as u32 % 12),
742 component: TxComponent::Luma,
743 pred_mode,
744 bit_depth: BIT_DEPTH,
745 extended_precision: false,
746 transquant_bypass: false,
747 transform_skip: false,
748 transform_skip_rotation_enabled: false,
749 },
750 ));
751 }
752 }
753 blocks
754}
755
756/// Pick the class furthest below its target share of the CTBs emitted so far.
757///
758/// Greedy on the sample deficit, as [`schedule_inter_pus`] is, so the emitted
759/// mix converges on the measured one exactly rather than approximately and does
760/// so deterministically — which is what the benchmark's bit-exactness guard
761/// needs.
762fn sao_class_for(shares: &[f64; 6], emitted: &[u64; 6], total: u64) -> usize {
763 (0..6)
764 .max_by(|&a, &b| {
765 let deficit = |i: usize| shares[i].mul_add(total as f64, -(emitted[i] as f64));
766 deficit(a)
767 .partial_cmp(&deficit(b))
768 .expect("the shares are finite")
769 })
770 .expect("the mix is not empty")
771}
772
773/// The resolved SAO parameters of one [`SAO_CTB_MIX`] class.
774fn sao_component_for(class: usize, i: usize, cidx: usize) -> ResolvedSaoComponent {
775 match class {
776 0 => ResolvedSaoComponent::off(),
777 1 => ResolvedSaoComponent {
778 sao_type_idx: 1,
779 offset_val: [0, 3, -2, 1, -3],
780 band_position: ((i * 7 + cidx) % 28) as u8,
781 eo_class: 0,
782 },
783 _ => ResolvedSaoComponent {
784 sao_type_idx: 2,
785 offset_val: [0, 2, 1, -1, -2],
786 band_position: 0,
787 eo_class: (class - 2) as u8,
788 },
789 }
790}
791
792/// A full 4:2:0 picture of synthetic content plus a per-CTB SAO parameter grid
793/// built to the measured [`SAO_CTB_MIX`].
794fn build_sao_inputs(width: usize, height: usize, luma: &[i32]) -> (Picture, Vec<ResolvedSao>) {
795 let mut picture = Picture::new(width, height, 1, BIT_DEPTH, BIT_DEPTH);
796 let (y_plane, _) = picture.plane_mut(Plane::Luma);
797 y_plane.copy_from_slice(luma);
798 let chroma = synthetic_luma(width / 2, height / 2, 0xC4B0);
799 for plane in [Plane::Cb, Plane::Cr] {
800 let (samples, _) = picture.plane_mut(plane);
801 samples.copy_from_slice(&chroma);
802 }
803
804 let ctb = 1usize << SAO_CTB_LOG2;
805 let ctbs_x = width.div_ceil(ctb);
806 let ctbs_y = height.div_ceil(ctb);
807 let mut grid = Vec::with_capacity(ctbs_x * ctbs_y);
808 // One deficit accumulator per row of the mix: luma is scheduled on its own
809 // shares, and Cb / Cr share the chroma row's — and take the same class as
810 // each other, as §7.3.8.3 signals them as one.
811 let mut emitted = [[0u64; 6]; 2];
812 for i in 0..ctbs_x * ctbs_y {
813 let total = i as u64;
814 let luma_class = sao_class_for(&SAO_CTB_MIX[0], &emitted[0], total);
815 let chroma_class = sao_class_for(&SAO_CTB_MIX[1], &emitted[1], total);
816 emitted[0][luma_class] += 1;
817 emitted[1][chroma_class] += 1;
818 grid.push(ResolvedSao {
819 components: [
820 sao_component_for(luma_class, i, 0),
821 sao_component_for(chroma_class, i, 1),
822 sao_component_for(chroma_class, i, 2),
823 ],
824 });
825 }
826 (picture, grid)
827}
828
829/// The §8.7.3.2 boundary grids for a single-slice, single-tile picture, plus
830/// the number of samples the grid's switched-on CTBs classify.
831///
832/// Every CTB is in slice 0 and tile 0, so nothing here denies a neighbour read
833/// — but the decoder still carries the grids, and the per-CTB
834/// [`SaoBoundaries::ctb_neighbourhood_unconstrained`] test that clears the
835/// vector path has to run over them. Passing them is what makes this group time
836/// the dispatch a decode takes.
837fn build_sao_boundaries(width: usize, height: usize, grid: &[ResolvedSao]) -> (SaoBoundaries, u64) {
838 let ctb = 1usize << SAO_CTB_LOG2;
839 let ctbs_x = width.div_ceil(ctb);
840 let ctbs_y = height.div_ceil(ctb);
841 let mut samples = 0u64;
842 for (i, resolved) in grid.iter().enumerate() {
843 let (rx, ry) = (i % ctbs_x, i / ctbs_x);
844 let w = ctb.min(width.saturating_sub(rx * ctb));
845 let h = ctb.min(height.saturating_sub(ry * ctb));
846 if resolved.components[0].sao_type_idx != 0 {
847 samples += (w * h) as u64;
848 }
849 for cidx in 1..3 {
850 if resolved.components[cidx].sao_type_idx != 0 {
851 samples += ((w / 2) * (h / 2)) as u64;
852 }
853 }
854 }
855 let n = ctbs_x * ctbs_y;
856 let boundaries = SaoBoundaries {
857 slice_addr_of_ctb: vec![0; n],
858 tile_id_of_ctb: vec![0; n],
859 pic_w_ctbs: ctbs_x,
860 ctb_log2_size_y: SAO_CTB_LOG2,
861 across_slices: true,
862 across_tiles: true,
863 filter_across_of_ctb: Some(vec![true; n]),
864 ctb_ts_of_rs: Some((0..n as u32).collect()),
865 };
866 (boundaries, samples)
867}
868
869/// A fixed pseudo-random byte buffer for the CABAC stage.
870///
871/// The arithmetic decoder's cost is in its renormalization and range updates,
872/// which depend on the bin values it produces, not on those bins being a valid
873/// slice segment. A fixed buffer makes the bin sequence reproducible, which is
874/// what the cross-instruction-set comparison needs.
875fn build_cabac_bitstream() -> Vec<u8> {
876 let mut rng = Lcg::new(0xCABA_C0DE);
877 (0..CABAC_BINS).map(|_| rng.below(256) as u8).collect()
878}
879
880/// The decoder configuration [`HevcStageInputs::run_color_convert`] converts
881/// under: the same RGBA8 limited-range output every whole-frame group asks for,
882/// at the prepared picture's dimensions.
883fn convert_config(width: usize, height: usize) -> VideoDecoderConfig {
884 let dimensions = VideoDimensions::new(width as u32, height as u32, &Limits::default())
885 .expect("the benchmark frame is within the default limits");
886 VideoDecoderConfig {
887 codec: Codec::Hevc,
888 profile: CodecProfile::HevcMain,
889 coded_dimensions: dimensions,
890 output_format: PixelFormat::Rgba8,
891 color_range: ColorRange::Limited,
892 hardware: HardwarePreference::Avoid,
893 configuration: Vec::new(),
894 }
895}
896
897/// Decodes `frames` frames of `samples` and stops at the decoded `Picture`.
898///
899/// Issue #220: the public decoder's `submit` returns RGBA, so a whole-frame
900/// benchmark through it measures decoding *plus* the colour conversion of
901/// [`HevcStageInputs::run_color_convert`] — a third of the interval, which
902/// until issue #219 gave it a kernel had no vector path at all and diluted
903/// every scalar-versus-SIMD ratio taken off those groups. This is the same
904/// decode without that tail: it drives the same
905/// `HevcDecoder` over the same access units and collects its pictures instead
906/// of converting them, so the difference between this and the end-to-end group
907/// is the conversion and nothing else about how the bitstream is handled.
908///
909/// Returns a fold over every decoded sample, for the harness's bit-exactness
910/// guard — the same fold [`decode_frames`] applies to the converted bytes, so
911/// neither group pays for identifying its output more than the other does. Pins
912/// no instruction set; the caller selects the arm through
913/// [`crate::simd::set_override`] exactly as for an end-to-end decode.
914///
915/// # Panics
916/// Panics if the configuration is not decodable or if `samples` does not yield
917/// `frames` frames — a benchmark measuring less work than it reports would be
918/// worse than a failure.
919/// Decodes `frames` frames of `samples` all the way out to RGBA.
920///
921/// The end-to-end half of the issue #220 split, and the counterpart to
922/// [`decode_pictures`]: same decoder, same access units, same frame count, same
923/// output fold — the only difference is the `picture_to_rgba` pass on each
924/// decoded picture, which is what makes the gap between the two groups the
925/// conversion rather than an artefact of how each arm identifies its output.
926///
927/// # Panics
928/// Panics under the same conditions as [`decode_pictures`].
929#[must_use]
930pub fn decode_frames(
931 configuration: &VideoDecoderConfig,
932 samples: &[EncodedVideoSample],
933 limits: &Limits,
934 frames: u64,
935) -> Vec<u8> {
936 let mut decoder = bench_decoder(configuration, limits);
937 let cancellation = CancellationToken::new();
938 let mut digest = Digest::new();
939 let mut decoded = 0_u64;
940 for sample in samples {
941 for frame in decoder
942 .submit(sample, &cancellation)
943 .expect("the sample decodes")
944 {
945 for plane in &frame.frame.planes {
946 digest.push_bytes(&plane.data);
947 }
948 decoded += 1;
949 }
950 if decoded >= frames {
951 break;
952 }
953 }
954 assert_frame_count(decoded, frames);
955 digest.finish()
956}
957
958/// The software decoder the whole-frame benchmark surface drives.
959///
960/// # Panics
961/// Panics if the configuration is not decodable.
962fn bench_decoder(configuration: &VideoDecoderConfig, limits: &Limits) -> HevcDecoder {
963 let parsed = ParsedConfiguration::parse(configuration, limits)
964 .expect("the sample's configuration record parses");
965 HevcDecoder::new(configuration.clone(), *limits, parsed)
966 .expect("the software HEVC decoder is constructible")
967}
968
969/// A benchmark measuring less work than it reports would be worse than a
970/// failure.
971fn assert_frame_count(decoded: u64, frames: u64) {
972 assert!(
973 decoded >= frames,
974 "the sample yielded {decoded} decoded pictures, not the {frames} the group reports"
975 );
976}
977
978#[must_use]
979pub fn decode_pictures(
980 configuration: &VideoDecoderConfig,
981 samples: &[EncodedVideoSample],
982 limits: &Limits,
983 frames: u64,
984) -> Vec<u8> {
985 let mut decoder = bench_decoder(configuration, limits);
986 let cancellation = CancellationToken::new();
987 let mut digest = Digest::new();
988 let mut decoded = 0_u64;
989 for sample in samples {
990 for picture in decoder
991 .submit_pictures(sample, &cancellation)
992 .expect("the sample decodes")
993 {
994 for plane in [Plane::Luma, Plane::Cb, Plane::Cr] {
995 digest.push_samples(picture.plane(plane));
996 }
997 decoded += 1;
998 }
999 if decoded >= frames {
1000 break;
1001 }
1002 }
1003 assert_frame_count(decoded, frames);
1004 digest.finish()
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009 use super::*;
1010 use crate::simd::{self, SimdIsa};
1011
1012 /// A frame small enough to build and run quickly, but still a whole number
1013 /// of the 64-sample CTBs the SAO stage is driven at.
1014 fn small_inputs() -> HevcStageInputs {
1015 HevcStageInputs::new(256, 128)
1016 }
1017
1018 /// The §8.5.3.3 workload has to run the prediction-unit mix issue #280
1019 /// measured, not a mix that happens to be close to it.
1020 ///
1021 /// The uniform 16x16 bi-predicted luma-only grid this replaced would fail
1022 /// every assertion here: one size instead of four, no uni-predicted units,
1023 /// and no chroma. A schedule that drifted back towards one of those — say
1024 /// by rounding every cell to the largest class — would look like a faster
1025 /// kernel rather than a narrower workload, which is the failure this
1026 /// benchmark exists to avoid.
1027 #[test]
1028 fn the_inter_pred_schedule_runs_the_measured_prediction_unit_mix() {
1029 // A frame large enough that each class gets whole cells to land in;
1030 // the greedy schedule converges on the shares as cells accumulate.
1031 let pus = schedule_inter_pus(1920, 1088);
1032 let total: u64 = pus.iter().map(|pu| (pu.n * pu.n) as u64).sum();
1033 assert!(total > 0, "the schedule emitted no prediction units");
1034
1035 for &(n, bi, share) in &INTER_PU_MIX {
1036 let got: u64 = pus
1037 .iter()
1038 .filter(|pu| pu.n == n && pu.bi == bi)
1039 .map(|pu| (pu.n * pu.n) as u64)
1040 .sum();
1041 let got = got as f64 / total as f64;
1042 assert!(
1043 (got - share).abs() < 0.01,
1044 "{n}x{n} {} is {:.1}% of luma samples, measured {:.1}%",
1045 if bi { "bi" } else { "uni" },
1046 got * 100.0,
1047 share * 100.0,
1048 );
1049 }
1050
1051 // Bi-prediction is the majority but not the whole of it, and every
1052 // measured size is present — the three things the old grid collapsed.
1053 let bi: u64 = pus
1054 .iter()
1055 .filter(|pu| pu.bi)
1056 .map(|pu| (pu.n * pu.n) as u64)
1057 .sum();
1058 let bi = bi as f64 / total as f64;
1059 assert!((0.55..0.70).contains(&bi), "bi-predicted share is {bi}");
1060 for side in [8usize, 16, 32, 64] {
1061 assert!(
1062 pus.iter().any(|pu| pu.n == side),
1063 "no {side}x{side} prediction unit was scheduled"
1064 );
1065 }
1066
1067 // And the workload predicts chroma: every unit is at least 8x8 luma,
1068 // so every unit has a 4:2:0 chroma unit of at least 4x4 to filter.
1069 assert!(
1070 pus.iter()
1071 .all(|pu| pu.n >= 8 && pu.x % 2 == 0 && pu.y % 2 == 0),
1072 "a unit has no whole 4:2:0 chroma unit"
1073 );
1074 }
1075
1076 /// Every stage must produce identical output on every instruction set the
1077 /// host can run.
1078 ///
1079 /// This is the same guarantee the benchmark's own guard checks, asserted
1080 /// here so it fails in `cargo test` rather than only when someone runs the
1081 /// benchmarks: a stage whose vector path diverged would otherwise report a
1082 /// speedup that is really just a different answer.
1083 #[test]
1084 fn every_stage_is_bit_exact_across_instruction_sets() {
1085 let _guard = simd::test_lock();
1086 let inputs = small_inputs();
1087 /// One stage's name and the entry point that runs it.
1088 type Stage = (&'static str, fn(&HevcStageInputs) -> Vec<u8>);
1089 let stages: [Stage; 7] = [
1090 ("inter_pred", HevcStageInputs::run_inter_pred),
1091 ("intra_pred", HevcStageInputs::run_intra_pred),
1092 ("deblock", HevcStageInputs::run_deblock),
1093 ("sao", HevcStageInputs::run_sao),
1094 ("inverse_transform", HevcStageInputs::run_inverse_transform),
1095 ("color_convert", HevcStageInputs::run_color_convert),
1096 ("cabac", HevcStageInputs::run_cabac),
1097 ];
1098
1099 for (name, run) in stages {
1100 simd::set_override(Some(SimdIsa::Scalar));
1101 let reference = run(&inputs);
1102 for isa in simd::available() {
1103 simd::set_override(Some(isa));
1104 assert_eq!(
1105 run(&inputs),
1106 reference,
1107 "{name} on {} diverged from the scalar reference",
1108 isa.name()
1109 );
1110 }
1111 }
1112 simd::set_override(None);
1113 }
1114
1115 /// The SAO grid has to run the §7.3.8.3 parameter mix issue #310 measured,
1116 /// not a saturated picture.
1117 ///
1118 /// The share that matters most is the one that used to be zero: on real
1119 /// content SAO is switched *off* on most CTBs, and an off CTB is returned
1120 /// from before it reads a sample. A grid drifting back towards filtering
1121 /// everything would read as a faster kernel when it is really a wider
1122 /// workload, so the emitted mix is pinned rather than trusted.
1123 #[test]
1124 fn the_sao_grid_runs_the_measured_parameter_mix() {
1125 let inputs = HevcStageInputs::new(1920, 1088);
1126 let total = inputs.sao_ctbs.len() as f64;
1127 assert!(total > 0.0, "the grid has CTBs");
1128 for (row, cidx) in [(0usize, 0usize), (1, 1), (1, 2)] {
1129 for (class, &want) in SAO_CTB_MIX[row].iter().enumerate() {
1130 let got = inputs
1131 .sao_ctbs
1132 .iter()
1133 .filter(|r| {
1134 let c = &r.components[cidx];
1135 let seen = match c.sao_type_idx {
1136 0 => 0,
1137 1 => 1,
1138 _ => 2 + usize::from(c.eo_class),
1139 };
1140 seen == class
1141 })
1142 .count() as f64
1143 / total;
1144 assert!(
1145 (got - want).abs() < 0.01,
1146 "component {cidx} class {class}: {got:.4} against the measured {want:.4}"
1147 );
1148 }
1149 }
1150 // Cb and Cr are signalled as one in §7.3.8.3, so they resolve alike.
1151 assert!(
1152 inputs.sao_ctbs.iter().all(|r| r.components[1].sao_type_idx
1153 == r.components[2].sao_type_idx
1154 && r.components[1].eo_class == r.components[2].eo_class),
1155 "Cb and Cr take the same SAO type and class"
1156 );
1157 // Most of the picture is switched off, so the classifiers run over a
1158 // fraction of it rather than all of it.
1159 let all = (1920u64 * 1088) * 3 / 2;
1160 assert!(
1161 inputs.sao_samples() < all / 4,
1162 "{} filtered samples against {all} in the picture",
1163 inputs.sao_samples()
1164 );
1165 }
1166
1167 /// The digests have to depend on the stages' samples.
1168 ///
1169 /// A workload that returned a constant would pass the bit-exactness guard
1170 /// above vacuously, so check that different stages of the same inputs
1171 /// produce different digests and that none of them is the empty fold.
1172 #[test]
1173 fn stage_digests_depend_on_the_stage_output() {
1174 let _guard = simd::test_lock();
1175 simd::set_override(Some(SimdIsa::Scalar));
1176 let inputs = small_inputs();
1177 let empty = Digest::new().finish();
1178 let digests = [
1179 inputs.run_inter_pred(),
1180 inputs.run_intra_pred(),
1181 inputs.run_deblock(),
1182 inputs.run_sao(),
1183 inputs.run_inverse_transform(),
1184 inputs.run_color_convert(),
1185 inputs.run_cabac(),
1186 ];
1187 simd::set_override(None);
1188
1189 for digest in &digests {
1190 assert_eq!(digest.len(), 8, "a digest is eight bytes");
1191 assert_ne!(*digest, empty, "a stage folded nothing");
1192 }
1193 for (i, a) in digests.iter().enumerate() {
1194 for b in &digests[i + 1..] {
1195 assert_ne!(a, b, "two stages produced the same digest");
1196 }
1197 }
1198 }
1199
1200 /// The deblocking input has to actually reach the wide filter.
1201 ///
1202 /// The §8.7.2.5.3 decision takes the strong filter only where the samples
1203 /// on both sides of an edge are flat enough. Content that never satisfies
1204 /// it would leave the wide path untimed while still producing a plausible
1205 /// number, so assert the plane contains flat runs across its edges rather
1206 /// than trusting the generator.
1207 #[test]
1208 fn the_deblocking_content_contains_flat_edges() {
1209 let inputs = small_inputs();
1210 let flat_edges = inputs
1211 .luma_edges()
1212 .iter()
1213 .filter(|pos| {
1214 // The §8.7.2.5.3 dEp/dEq flatness test reads p3..p0 / q0..q3 of
1215 // the segment's first row; approximate it by requiring both
1216 // sides to span no more than a couple of levels.
1217 let row = pos.ey * inputs.width;
1218 let span = |from: usize| {
1219 let window = &inputs.luma[row + from..row + from + 4];
1220 window.iter().max().unwrap() - window.iter().min().unwrap()
1221 };
1222 span(pos.ex - 4) <= 2 && span(pos.ex) <= 2
1223 })
1224 .count();
1225 assert!(
1226 flat_edges > inputs.luma_edges().len() / 10,
1227 "only {flat_edges} of {} edges are flat; the wide filter would go untimed",
1228 inputs.luma_edges().len()
1229 );
1230 }
1231
1232 /// The stages' reported work has to match what they actually touch, since
1233 /// it is the denominator of every throughput number the benchmark prints.
1234 #[test]
1235 fn reported_sample_counts_match_the_workloads() {
1236 let inputs = small_inputs();
1237 // Three 64x64 cells fit a 256x128 frame once the one-cell margin the
1238 // second reference list needs is taken off each edge, and every cell
1239 // is filled by prediction units of one class whatever that class is.
1240 assert_eq!(inputs.inter_pred_samples(), 3 * 64 * 64);
1241 // Only the CTBs the measured mix leaves switched on are counted, so
1242 // this is a fraction of the 256x128 4:2:0 picture rather than all of it.
1243 assert!(
1244 inputs.sao_samples() > 0 && inputs.sao_samples() < 256 * 128 * 3 / 2,
1245 "sao_samples was {}",
1246 inputs.sao_samples()
1247 );
1248 assert_eq!(
1249 inputs.deblock_samples(),
1250 inputs.luma_edges().len() as u64 * 32
1251 );
1252 assert_eq!(inputs.cabac_bins(), CABAC_BINS as u64);
1253 assert!(inputs.intra_pred_samples() > 0);
1254 assert!(inputs.inverse_transform_samples() > 0);
1255 }
1256
1257 /// The picture-only path exists so a whole-frame benchmark can leave the
1258 /// output conversion out of the measured interval — which is only sound if
1259 /// it is otherwise the same decode. Converting what it collects has to
1260 /// reproduce, frame for frame and byte for byte, what the public
1261 /// `submit` returns.
1262 #[test]
1263 fn the_picture_only_path_decodes_what_the_rgba_path_decodes() {
1264 let (configuration, samples) = pcm_stream();
1265 let limits = Limits::default();
1266 let cancellation = CancellationToken::new();
1267
1268 let mut rgba_decoder = new_decoder(&configuration, &limits);
1269 let mut expected = Vec::new();
1270 for sample in &samples {
1271 for frame in rgba_decoder.submit(sample, &cancellation).unwrap() {
1272 expected.push(frame.frame);
1273 }
1274 }
1275 assert!(
1276 !expected.is_empty(),
1277 "the generated stream has to produce output frames for this to test anything"
1278 );
1279
1280 let mut picture_decoder = new_decoder(&configuration, &limits);
1281 let mut actual = Vec::new();
1282 for sample in &samples {
1283 for picture in picture_decoder
1284 .submit_pictures(sample, &cancellation)
1285 .unwrap()
1286 {
1287 actual.push(picture_to_rgba(&picture, &configuration, &limits).unwrap());
1288 }
1289 }
1290 assert_eq!(actual, expected);
1291 }
1292
1293 /// The digest the picture-only whole-frame group returns is what the
1294 /// harness compares across instruction sets, so it has to depend on the
1295 /// decoded samples and be reproducible.
1296 #[test]
1297 fn decode_pictures_folds_the_decoded_samples_reproducibly() {
1298 let (configuration, samples) = pcm_stream();
1299 let limits = Limits::default();
1300 let one = decode_pictures(&configuration, &samples, &limits, 1);
1301 assert_eq!(one, decode_pictures(&configuration, &samples, &limits, 1));
1302 assert_ne!(one, decode_pictures(&configuration, &samples, &limits, 2));
1303 assert_ne!(one, Digest::new().finish());
1304 }
1305
1306 /// The two whole-frame surfaces have to decode the same frames, so the
1307 /// interval between them is the conversion and not a different workload.
1308 #[test]
1309 fn the_two_whole_frame_paths_fold_the_same_number_of_frames() {
1310 let (configuration, samples) = pcm_stream();
1311 let limits = Limits::default();
1312 let frames = decode_frames(&configuration, &samples, &limits, 2);
1313 assert_eq!(frames, decode_frames(&configuration, &samples, &limits, 2));
1314 assert_ne!(
1315 frames,
1316 decode_pictures(&configuration, &samples, &limits, 2)
1317 );
1318 assert_ne!(frames, Digest::new().finish());
1319 }
1320
1321 /// The conversion stage folds a real converted frame, and folds a
1322 /// different one for different content.
1323 #[test]
1324 fn color_convert_folds_the_converted_frame() {
1325 let inputs = small_inputs();
1326 let converted = inputs.run_color_convert();
1327 assert_eq!(converted, inputs.run_color_convert());
1328 assert_ne!(converted, HevcStageInputs::new(128, 64).run_color_convert());
1329 assert_eq!(inputs.color_convert_samples(), 256 * 128);
1330 }
1331
1332 /// A short PCM-coded stream, built with the crate's own HEVC encoder so
1333 /// the decode-side tests run on a real bitstream without a fixture.
1334 fn pcm_stream() -> (VideoDecoderConfig, Vec<EncodedVideoSample>) {
1335 use crate::FrameIndex;
1336 use zvidlib_hevc_encoder::encoder::{hvcc_box, length_prefixed_vcl};
1337 use zvidlib_hevc_encoder::engine::encoder::pcm::encode_idr_pcm_au;
1338
1339 const SIDE: usize = 16;
1340 const FRAMES: u64 = 4;
1341 let mut rng = Lcg::new(0x8EEF_0220);
1342 let y: Vec<u8> = (0..SIDE * SIDE).map(|_| rng.below(256) as u8).collect();
1343 let chroma: Vec<u8> = (0..SIDE * SIDE / 4).map(|_| rng.below(256) as u8).collect();
1344 let au = encode_idr_pcm_au(&y, &chroma, &chroma, SIDE, SIDE).unwrap();
1345 let data = length_prefixed_vcl(&au).unwrap();
1346 let mut configuration = convert_config(SIDE, SIDE);
1347 configuration.configuration = hvcc_box(&au).unwrap();
1348 let samples = (0..FRAMES)
1349 .map(|index| EncodedVideoSample {
1350 presentation_index: FrameIndex(index),
1351 random_access: true,
1352 data: data.clone(),
1353 })
1354 .collect();
1355 (configuration, samples)
1356 }
1357
1358 fn new_decoder(configuration: &VideoDecoderConfig, limits: &Limits) -> HevcDecoder {
1359 let parsed = ParsedConfiguration::parse(configuration, limits).unwrap();
1360 HevcDecoder::new(configuration.clone(), *limits, parsed).unwrap()
1361 }
1362}