diff --git a/crates/coreav-kernels-hevc/src/inter.rs b/crates/coreav-kernels-hevc/src/inter.rs index 5a00b74e..ed14a80b 100644 --- a/crates/coreav-kernels-hevc/src/inter.rs +++ b/crates/coreav-kernels-hevc/src/inter.rs @@ -196,8 +196,13 @@ fn interpolate( rows: [[0; 64]; 8], }; let source_width = output.width + x_filter.len().saturating_sub(1); - for (x, column) in scratch.columns.iter_mut().take(source_width).enumerate() { - *column = (x_start + x as i64).clamp(0, reference.width as i64 - 1) as usize; + let consecutive = config.depth.bits() == 8 + && x_start >= 0 + && x_start + source_width as i64 <= reference.width as i64; + if !consecutive { + for (x, column) in scratch.columns.iter_mut().take(source_width).enumerate() { + *column = (x_start + x as i64).clamp(0, reference.width as i64 - 1) as usize; + } } let row_count = y_filter.len().max(1); let mut next_row = 0; @@ -210,13 +215,20 @@ fn interpolate( .data .get(begin..begin + reference.width) .ok_or(Error::Buffer)?; - let input = scratch - .source - .get_mut(..source_width) - .ok_or(Error::Buffer)?; - for (value, &column) in input.iter_mut().zip(&scratch.columns) { - *value = *source.get(column).ok_or(Error::Buffer)?; - } + let input = if consecutive { + source + .get(x_start as usize..x_start as usize + source_width) + .ok_or(Error::Buffer)? + } else { + let input = scratch + .source + .get_mut(..source_width) + .ok_or(Error::Buffer)?; + for (value, &column) in input.iter_mut().zip(&scratch.columns) { + *value = *source.get(column).ok_or(Error::Buffer)?; + } + &*input + }; let row = scratch.rows.get_mut(next_row % 8).ok_or(Error::Buffer)?; let row = row.get_mut(..output.width).ok_or(Error::Buffer)?; if x_filter.is_empty() { diff --git a/crates/coreav-kernels-hevc/src/inter_tests.rs b/crates/coreav-kernels-hevc/src/inter_tests.rs index df0aa970..f2b6fcf8 100644 --- a/crates/coreav-kernels-hevc/src/inter_tests.rs +++ b/crates/coreav-kernels-hevc/src/inter_tests.rs @@ -156,6 +156,88 @@ fn inter_scratch_fits_existing_reconstruction_envelope() { assert_eq!(SCRATCH_BYTES, 9_224); } +#[test] +fn interpolation_support_at_exact_row_extents_matches_equations_before_writes() { + for bits in [8, 10] { + for component in [Component::Luma, Component::Chroma] { + let (taps, first, limit) = if component == Component::Luma { + (8, 3, 64) + } else { + (4, 1, 32) + }; + for width in [1, 7, 8, 9, 31, 32, 63, 64] { + if width > limit { + continue; + } + let height = 2; + let reference_width = width + taps - 1; + let reference_height = height + taps - 1; + let source_stride = reference_width + 3; + let source_length = source_stride * (reference_height - 1) + reference_width; + let mut source = vec![u16::MAX; 1 + source_length]; + for y in 0..reference_height { + for x in 0..reference_width { + source[1 + y * source_stride + x] = + ((47 * x + 91 * y + 13 * x * y) % (1 << bits)) as u16; + } + } + let stride = width + 3; + let length = stride * (height - 1) + width; + for origin in [first - 1, first, first + 1] { + let request = config(bits, component, origin, first, 1, 1).unwrap(); + let mut output = vec![i32::MAX; 1 + length]; + let mut expected = output.clone(); + for y in 0..height { + for x in 0..width { + expected[1 + y * stride + x] = equation_sample( + request, + ( + &source[1..], + source_stride, + reference_width, + reference_height, + ), + (x, y), + ) + .unwrap(); + } + } + interpolate_inter( + request, + RefPlane::new( + &source[1..], + source_stride, + reference_width, + reference_height, + ) + .unwrap(), + RectMut::new(&mut output[1..], stride, width, height).unwrap(), + ) + .unwrap(); + assert_eq!(output, expected, "{bits}/{component:?}/{width}/{origin}"); + } + source[source_length] = 1 << bits; + let mut output = vec![i32::MAX; 1 + length]; + assert_eq!( + interpolate_inter( + config(bits, component, first, first, 1, 1).unwrap(), + RefPlane::new( + &source[1..], + source_stride, + reference_width, + reference_height, + ) + .unwrap(), + RectMut::new(&mut output[1..], stride, width, height).unwrap(), + ), + Err(Error::Sample) + ); + assert_eq!(output, vec![i32::MAX; 1 + length]); + } + } + } +} + #[cfg(all(not(feature = "scalar"), target_arch = "x86_64"))] #[test] fn avx2_capability_matches_runtime_detection() { diff --git a/docs/design/hevc-reference-row-borrows.md b/docs/design/hevc-reference-row-borrows.md new file mode 100644 index 00000000..74446216 --- /dev/null +++ b/docs/design/hevc-reference-row-borrows.md @@ -0,0 +1,11 @@ +# HEVC eight-bit reference-row borrows + +The interpolator checks its entire clipped reference support before writing output. It then gathers each source row through a clamped column map into a temporary buffer, even when the horizontal support lies entirely inside the coded picture. + +For eight-bit input with an interior horizontal support, borrow the consecutive source samples directly. Construct the column map and gather only for the existing boundary-replication path. Ten-bit reconstruction keeps that path unchanged. The AVX2 kernels, scalar filter arithmetic, horizontal and vertical shifts, intermediate row ring, output writes and scratch accounting do not change. + +H.265 clause 8.5.3.3.3 and Tables 8-8 and 8-9 define consecutive taps within a row. If `x_start >= 0` and `x_start + source_width <= reference.width`, every previously gathered index is exactly `x_start + x`. A checked source slice therefore supplies the identical samples in the identical order. The existing full-support depth check still precedes both paths. Taps with zero coefficients remain part of the checked support. + +The support calculation remains shared with scheduling. This change only consumes the resulting start coordinate and filter extent inside the kernel. Picture-edge replication, extreme signed motion vectors, invalid samples and output padding preserve their existing behavior. No new unsafe site or scratch allocation is needed. + +The preceding diagnostic compares only removal of the interior column-map and gather copies, without deleting filter arithmetic. It checks both exact decoder arms against the qualified oracle before whole-process instruction counts. Those counts motivate this bounded change but are not a wall-time speed claim. Main10 remains a separate follow-up.