Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/hevc-conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ jobs:
kernel-differential:
if: github.event_name == 'workflow_dispatch' && !inputs.scalar
runs-on: ${{ matrix.os }}
timeout-minutes: 60
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
Expand All @@ -299,7 +299,7 @@ jobs:
- name: Prepare bounded kernel differential targets
run: |
scripts/fuzz_run.sh setup
printf '%s\n' 'crates/coreav-kernels-hevc/fuzz horizontal' 'crates/coreav-kernels-hevc/fuzz vertical' 'crates/coreav-kernels-hevc/fuzz sao' 'crates/coreav-kernels-hevc/fuzz intra' 'crates/coreav-kernels-hevc/fuzz deblock' > "$RUNNER_TEMP/interpolation-plan.txt"
printf '%s\n' 'crates/coreav-kernels-hevc/fuzz horizontal' 'crates/coreav-kernels-hevc/fuzz vertical' 'crates/coreav-kernels-hevc/fuzz sao' 'crates/coreav-kernels-hevc/fuzz intra' 'crates/coreav-kernels-hevc/fuzz deblock' 'crates/coreav-kernels-hevc/fuzz combine' > "$RUNNER_TEMP/interpolation-plan.txt"
scripts/fuzz_run.sh build "$RUNNER_TEMP/interpolation-plan.txt"
- id: differential
name: Compare automatic and scalar kernels for ten minutes per target
Expand Down
7 changes: 7 additions & 0 deletions crates/coreav-kernels-hevc/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,10 @@ path = "fuzz_targets/deblock.rs"
test = false
doc = false
bench = false

[[bin]]
name = "combine"
path = "fuzz_targets/combine.rs"
test = false
doc = false
bench = false
113 changes: 113 additions & 0 deletions crates/coreav-kernels-hevc/fuzz/fuzz_targets/combine.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#![no_main]
use coreav_kernels_hevc::{BitDepth, PredictionWeight, Rect, RectMut};

libfuzzer_sys::fuzz_target!(|data: &[u8]| {
if data.len() < 24 {
return;
}
let bits = if data[0] & 1 == 0 { 8 } else { 10 };
let depth = BitDepth::new(bits).unwrap();
let (minimum, maximum) = if bits == 8 {
(-16830, 33150)
} else {
(-16880, 33247)
};
let width = usize::from(data[2] % 64) + 1;
let height = usize::from(data[3] % 64) + 1;
let first_stride = width + usize::from(data[4] & 7);
let second_stride = width + usize::from(data[5] & 7);
let output_stride = width + usize::from(data[6] & 7);
let first_offset = usize::from(data[7] & 7);
let second_offset = usize::from(data[8] & 7);
let output_offset = usize::from(data[13] & 7);
let mut first = vec![i32::MIN; first_offset + first_stride * (height - 1) + width];
let mut second = vec![i32::MAX; second_offset + second_stride * (height - 1) + width];
for (side, buffer, stride, offset) in [
(0, &mut first, first_stride, first_offset),
(1, &mut second, second_stride, second_offset),
] {
for y in 0..height {
for x in 0..width {
let index = side * width * height + y * width + x;
let position = 20 + (4 * index) % (data.len() - 20);
let raw =
u32::from_le_bytes(core::array::from_fn(|i| data[(position + i) % data.len()]));
buffer[offset + y * stride + x] =
minimum + (raw % (maximum - minimum + 1) as u32) as i32;
}
}
if data[8] & 0xc0 == 0xc0 && side == usize::from(data[0] & 2 != 0) {
*buffer.last_mut().unwrap() = i32::MAX;
}
}
let denom = data[1] % 8;
let second_denom = if data[13] & 0xc0 == 0x40 {
(denom + 1) % 8
} else {
denom
};
let weight = PredictionWeight::new(
denom,
(1 << denom) + i16::from(data[9] as i8),
i16::from(data[10] as i8),
)
.unwrap();
let second_weight = PredictionWeight::new(
second_denom,
(1 << second_denom) + i16::from(data[11] as i8),
i16::from(data[12] as i8),
)
.unwrap();
let first = Rect::new(&first[first_offset..], first_stride, width, height).unwrap();
let second = Rect::new(&second[second_offset..], second_stride, width, height).unwrap();
let extent = output_stride * (height - 1) + width;
let mut actual = vec![u16::MAX; output_offset + extent + 5];
let mut expected = actual.clone();
let result = if data[0] & 2 != 0 {
coreav_kernels_hevc::weight_bi(
depth,
weight,
second_weight,
first,
second,
RectMut::new(
&mut actual[output_offset..output_offset + extent],
output_stride,
width,
height,
)
.unwrap(),
)
} else {
coreav_kernels_hevc::weight_uni(
depth,
weight,
first,
RectMut::new(
&mut actual[output_offset..output_offset + extent],
output_stride,
width,
height,
)
.unwrap(),
)
};
let reference = coreav_kernels_hevc::combine_reference(
depth,
weight,
first,
(data[0] & 2 != 0).then_some((second_weight, second)),
RectMut::new(
&mut expected[output_offset..output_offset + extent],
output_stride,
width,
height,
)
.unwrap(),
);
assert_eq!(result, reference);
assert_eq!(actual, expected);
if result.is_err() {
assert!(actual.iter().all(|&value| value == u16::MAX));
}
});
2 changes: 2 additions & 0 deletions crates/coreav-kernels-hevc/src/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ impl<'a, T> Rect<'a, T> {
}
}

#[cfg(any(test, feature = "test-support"))]
impl<T: Copy> Rect<'_, T> {
pub(crate) fn at(&self, x: usize, y: usize) -> Result<T, Error> {
self.data
Expand Down Expand Up @@ -261,6 +262,7 @@ impl<'a, T> RectMut<'a, T> {
self.height
}

#[cfg(any(test, feature = "test-support"))]
pub(crate) fn put(&mut self, x: usize, y: usize, value: T) -> Result<(), Error> {
*self
.data
Expand Down
96 changes: 96 additions & 0 deletions crates/coreav-kernels-hevc/src/inter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,102 @@ pub fn weight_bi(
}

fn combine(
depth: BitDepth,
weight: PredictionWeight,
first: Rect<'_, i32>,
second: Option<(PredictionWeight, Rect<'_, i32>)>,
output: RectMut<'_, u16>,
) -> Result<(), Error> {
if first.width != output.width || first.height != output.height {
return Err(Error::Rectangle);
}
if let Some((other_weight, other)) = second {
if other.width != output.width || other.height != output.height {
return Err(Error::Rectangle);
}
if other_weight.denom != weight.denom {
return Err(Error::Weight);
}
}
let max_sample = i32::from(depth.max_sample());
let lower = (-24 * max_sample) >> (depth.bits() - 8);
let upper = (88 * max_sample) >> (depth.bits() - 8);
let minimum = (88 * lower - 24 * upper) >> 6;
let maximum = (88 * upper - 24 * lower) >> 6;
for row in first.data.chunks(first.stride).take(first.height) {
if row
.get(..first.width)
.ok_or(Error::Buffer)?
.iter()
.any(|value| !(minimum..=maximum).contains(value))
{
return Err(Error::Sample);
}
}
if let Some((_, other)) = second {
for row in other.data.chunks(other.stride).take(other.height) {
if row
.get(..other.width)
.ok_or(Error::Buffer)?
.iter()
.any(|value| !(minimum..=maximum).contains(value))
{
return Err(Error::Sample);
}
}
}
let width = output.width;
let height = output.height;
let shift = weight.denom + 14 - depth.bits();
let offset = i32::from(weight.offset) << (depth.bits() - 8);
let first_weight = i32::from(weight.weight);
if let Some((other_weight, other)) = second {
let second_weight = i32::from(other_weight.weight);
let other_offset = i32::from(other_weight.offset) << (depth.bits() - 8);
let bias = (offset + other_offset + 1) << shift;
for ((first, other), output) in first
.data
.chunks(first.stride)
.zip(other.data.chunks(other.stride))
.zip(output.data.chunks_mut(output.stride))
.take(height)
{
for ((&first, &other), output) in first
.get(..width)
.ok_or(Error::Buffer)?
.iter()
.zip(other.get(..width).ok_or(Error::Buffer)?)
.zip(output.get_mut(..width).ok_or(Error::Buffer)?)
{
let sample = (first * first_weight + other * second_weight + bias) >> (shift + 1);
*output = sample.clamp(0, max_sample) as u16;
}
}
} else {
let rounding = 1 << (shift - 1);
for (first, output) in first
.data
.chunks(first.stride)
.zip(output.data.chunks_mut(output.stride))
.take(height)
{
for (&first, output) in first
.get(..width)
.ok_or(Error::Buffer)?
.iter()
.zip(output.get_mut(..width).ok_or(Error::Buffer)?)
{
let sample = ((first * first_weight + rounding) >> shift) + offset;
*output = sample.clamp(0, max_sample) as u16;
}
}
}
Ok(())
}

#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn combine_reference(
depth: BitDepth,
weight: PredictionWeight,
first: Rect<'_, i32>,
Expand Down
85 changes: 85 additions & 0 deletions crates/coreav-kernels-hevc/src/inter_tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,91 @@
use crate::*;
use std::{vec, vec::Vec};

#[test]
fn combination_rows_match_literal_full_domain_and_exact_extents() {
let mut seed = 0x943a_1807_u32;
let deltas = [-128, -127, -1, 0, 1, 126, 127];
let offsets = [-128, -1, 0, 1, 127];
for (bits, minimum, maximum) in [(8, -16830, 33150), (10, -16880, 33247)] {
let depth = BitDepth::new(bits).unwrap();
for width in 1..=64 {
let height = 3;
let first_stride = width + 3;
let second_stride = width + 5;
let output_stride = width + 1;
let mut first = vec![i32::MIN; 1 + first_stride * (height - 1) + width];
let mut second = vec![i32::MAX; 1 + second_stride * (height - 1) + width];
for y in 0..height {
for x in 0..width {
for (data, stride) in [(&mut first, first_stride), (&mut second, second_stride)]
{
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed << 5;
data[1 + y * stride + x] = match x % 4 {
0 => minimum,
1 => maximum,
2 => 0,
_ => minimum + (seed % (maximum - minimum + 1) as u32) as i32,
};
}
}
}
let first = Rect::new(&first[1..], first_stride, width, height).unwrap();
let second = Rect::new(&second[1..], second_stride, width, height).unwrap();
for denom in 0..=7 {
for case in 0..49 {
let weight = PredictionWeight::new(
denom,
(1 << denom) + deltas[case % 7],
offsets[case % 5],
)
.unwrap();
let other_weight = PredictionWeight::new(
denom,
(1 << denom) + deltas[case / 7],
offsets[(case / 5) % 5],
)
.unwrap();
for bi in [false, true] {
let mut actual = vec![999; 1 + output_stride * (height - 1) + width];
let mut expected = actual.clone();
let other = bi.then_some((other_weight, second));
let result = if bi {
weight_bi(
depth,
weight,
other_weight,
first,
second,
RectMut::new(&mut actual[1..], output_stride, width, height)
.unwrap(),
)
} else {
weight_uni(
depth,
weight,
first,
RectMut::new(&mut actual[1..], output_stride, width, height)
.unwrap(),
)
};
let reference = inter::combine_reference(
depth,
weight,
first,
other,
RectMut::new(&mut expected[1..], output_stride, width, height).unwrap(),
);
assert_eq!(result, reference, "{bits}/{width}/{denom}/{case}/{bi}");
assert_eq!(actual, expected, "{bits}/{width}/{denom}/{case}/{bi}");
}
}
}
}
}
}

#[test]
fn automatic_and_scalar_match_all_widths_phases_and_tails() {
let mut seed = 0x18a5_92c3_u32;
Expand Down
6 changes: 3 additions & 3 deletions crates/coreav-kernels-hevc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@ pub use filter::{
#[cfg(all(feature = "native-hevc", feature = "test-support"))]
#[doc(hidden)]
pub use filter::{deblock_chroma_scalar, deblock_luma_scalar};
#[cfg(all(feature = "native-hevc", feature = "test-support"))]
#[doc(hidden)]
pub use inter::interpolate_inter_scalar;
#[cfg(feature = "native-hevc")]
pub use inter::{
InterPrediction, MotionVector, PredictionWeight, RefPlane, interpolate_inter, predict_bi,
predict_uni, weight_bi, weight_uni,
};
#[cfg(all(feature = "native-hevc", feature = "test-support"))]
#[doc(hidden)]
pub use inter::{combine_reference, interpolate_inter_scalar};
#[cfg(all(feature = "native-hevc", feature = "test-support"))]
#[doc(hidden)]
pub use intra::predict_intra_reference;
#[cfg(feature = "native-hevc")]
pub use intra::{IntraPrediction, predict_intra};
Expand Down
19 changes: 19 additions & 0 deletions docs/design/hevc-inter-combine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# HEVC inter prediction combination

This family changes only the opt-in HEVC kernel crate's uni, bi and weighted prediction combination under H.265 V11 (01/2026), clause 8.5.3.3.4. Interpolation, reference-picture selection, parsing, default features and decoder isolation stay unchanged.

## Safe arithmetic first

Validate shapes, matching denominators and every active predictor before writing any output. Borrow active rows with their independent strides, so padding is neither validated nor modified. Select uni or bi once per block rather than inside every sample. Keep the original checked, signed `i64` equations as a test-support reference.

Every admitted internal predictor has magnitude at most 33,247. Effective weights have magnitude at most 255, scaled offsets at most 512, and the initial shift is in 4..=13. Even the conservative bi bound `2*33247*255 + (2*512+1)*8192 = 25352770` is below `2^25`, so products, offset sums and rounding fit signed `i32`. The bound deliberately combines maxima from different depths. Signed right shifts and final clipping remain in their normative order; in particular, bi offsets are not rounded separately.

## Dispatch boundary

Both depths use the safe row implementation. This family adds no runtime capability check, target-feature function or unsafe code. The `scalar` feature continues to exclude existing SIMD in other kernel families without changing these combination equations.

## Verification

Compare automatic and scalar-forced results with the literal reference over both depths, all denominators, effective-weight and offset extremes, uni and bi, widths 1..=64, independent strides, unaligned starts and exact final extents. Preserve rejection-before-write checks. A bounded differential fuzz target exercises the same safe interfaces. Both full conformance modes and the census remain required before shipping.

Reference comparisons bind every arm to an exact commit, verify pixels with the qualified pinned oracle before timing, and check decoded work on every call. Baseline and safe rows run on the same pinned Actions CPU with alternating repeated calls. Instruction attribution is not a CPU-time share or a promised speedup; private reports retain the host, raw calls and CoreAV/FFmpeg ratios.
Loading
Loading