Browse Source

Implement SIMD-first HEVC intra prediction

pull/2633/head
James Jackson-South 1 week ago
parent
commit
a38137830d
  1. 8
      HEIF_IMPLEMENTATION_PLAN.md
  2. 423
      src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.Operations.cs
  3. 280
      src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.OperatorImplementations.cs
  4. 412
      src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.cs
  5. 116
      tests/ImageSharp.Benchmarks/Codecs/Heif/HevcIntraPredictionBenchmarks.cs
  6. 396
      tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcIntraPredictorTests.cs

8
HEIF_IMPLEMENTATION_PLAN.md

@ -29,7 +29,7 @@ Checkboxes may be marked complete only when the implementation and the verificat
## Active and queued work
- [ ] **Active:** complete the SIMD-first shared HEIF color pipeline, including ICC color management after codec color conversion and image composition.
- [ ] **Active:** complete HEVC still-picture reconstruction and connect its native planes to the verified shared SIMD-first HEIF color pipeline.
- [x] Correct AV1 clipped-edge partition entropy handling and verify every block-mode cell from a real libavif AVIF against libaom 3.14.1.
- [x] Preserve the exact ICC payload from an independently encoded AVIF primary item.
- [x] Prove that a genuine non-sRGB AVIF profile changes decoded pixels and matches the independently converted source image within the documented AV1 tolerance.
@ -42,6 +42,8 @@ Checkboxes may be marked complete only when the implementation and the verificat
- [x] Implement SIMD 4:2:0 and 4:2:2 encoder downsampling with odd-width and odd-height tails, and verify the stored 8/12-bit chroma samples against independently encoded full-resolution planes.
- [x] Move the H.273 parameter resolver, closed color operators, transfer functions, sample loading/storage, chroma filtering, and RGB packing into one shared HEIF color pipeline used by both AV1 and HEVC rather than maintaining codec-specific arithmetic copies.
- [x] Implement sequential pooled HEVC conversion in both directions for monochrome, 4:2:0, 4:2:2, and 4:4:4; independent luma/chroma precision; 8/10/12-bit full/limited ranges; and all six progressive 4:2:0 chroma sample locations. Eight-bit paths use JPEG's optimized RGB plane contracts, while high-bit-depth paths retain 16-bit packed pixels.
- [x] Implement allocation-free SIMD-first HEVC intra prediction for all 35 modes, 4/8/16/32 blocks, and 8/10/12-bit samples, including three-tap and strong-bilinear reference filtering, negative-angle reference extension, luma boundary filters, and SIMD horizontal transposition. Verify the normal and forced-scalar paths against one scalar oracle.
- [ ] Connect prediction to coding-tree and transform-unit traversal, reconstructed-plane reference availability, and the bounded item decoder lifecycle.
- [ ] Complete the SIMD YUV/CICP paths for every supported AV1 bit depth, chroma format, range, matrix, transfer function, color primary, and chroma position, with scalar fallback only when hardware vectorization is unavailable or the operation is inherently non-vectorizable.
- [x] Apply ICC conversion only after the SIMD YUV/CICP stage, alpha composition, grid assembly, and presentation transforms have produced the presented RGB image; retain ImageSharp's shared ICC converter and optimize reusable bulk kernels rather than creating a HEIF-specific color-management implementation.
- [ ] Verify ICC preservation, conversion, compaction, and metadata skipping for grids, alpha-composited images, every presented sequence frame, and the completed HEVC path.
@ -243,6 +245,7 @@ This snapshot pins or classifies the available references and failures; it does
| `HevcPlane` and `HevcPictureBuffer` | HEVC sections 6.2 and 6.3 source and decoded picture sample-array dimensions for monochrome, 4:2:0, 4:2:2, 4:4:4, and separate-color-plane coding | HM `source/Lib/TLibCommon/TComPicYuv.cpp` at `9c1f298659ab0cee9dc13d23d0304221575410b9`; Android `libhevc` decoded-picture buffer layout paths at `c83a76b084498d55f252f48b2e3786804cdf24b7`; ImageSharp `MemoryAllocator` and `Buffer2D<T>` | Allocate one allocator-owned native `ushort` plane set for the selected still picture so the same reconstruction path preserves every supported 8-through-16-bit sample without byte-backed reinterpretation. Derive chroma dimensions by ceiling division for each sampling layout and treat separate color planes as full-resolution independently coded arrays. The buffer contains one picture only and introduces no decoded-picture buffer, reference lifetime, frame queue, or playback state. |
| `HevcCodingTreeState` | HEVC sections 6.4 coding-tree block and coding-block availability plus section 9.3 split-flag context derivation | HM `source/Lib/TLibCommon/TComDataCU.cpp` function `getCtxSplitFlag` and `source/Lib/TLibDecoder/TDecCu.cpp` coding-tree traversal at `9c1f298659ab0cee9dc13d23d0304221575410b9`; Android `libhevc` `decoder/ihevcd_parse_slice.c` coding-tree paths at `c83a76b084498d55f252f48b2e3786804cdf24b7`; ImageSharp `MemoryAllocator` and `Buffer2D<T>` | Store only leaf depth, effective luma QP, transquant-bypass, and PCM state at minimum-coding-block resolution for the selected picture. Derive split contexts from caller-approved left and above availability so slice and tile boundaries remain owned by traversal rather than hidden in a general block graph. Clip edge writes to the coded still-picture state and add no prediction units, motion fields, references, decoded-picture queue, or sequence lifetime. |
| `HevcIntraPredictionState` | HEVC sections 8.4.2 and 9.3 intra luma/chroma prediction-mode derivation and binarization | HM `source/Lib/TLibCommon/TComDataCU.cpp` functions `getIntraDirPredictor` and `getAllowedChromaDir`, plus `source/Lib/TLibDecoder/TDecSbac.cpp` functions `parseIntraDirLumaAng` and `parseIntraDirChroma`, at `9c1f298659ab0cee9dc13d23d0304221575410b9`; Android `libhevc` intra-mode parsing and neighbor derivation paths at `c83a76b084498d55f252f48b2e3786804cdf24b7`; ImageSharp `MemoryAllocator` and `Buffer2D<T>` | Preserve the normative two-pass luma flag/suffix order, spatial most-probable-mode derivation, omitted-mode reinsertion, four-way minimum-CU partition order, explicit chroma candidate substitution, and derived-chroma mode at 4x4 luma resolution. Accept slice/tile availability from traversal, fill only the selected still-picture map, and add no inter prediction unit, motion, reference, or sequence state. |
| `HevcIntraPredictor` | HEVC section 8.4.4.2 intra sample prediction and section 8.4.4.2.3 reference-sample substitution and filtering | HM `source/Lib/TLibCommon/TComPrediction.cpp` functions `predIntraGetPredValDC`, `xPredIntraAng`, `xPredIntraPlanar`, and `xDCPredFiltering`, plus `source/Lib/TLibCommon/TComPattern.cpp` function `fillReferenceSamples` and its reference-filter selection at `9c1f298659ab0cee9dc13d23d0304221575410b9`; Android `libhevc` intra-prediction kernels at `c83a76b084498d55f252f48b2e3786804cdf24b7` | Predict from caller-prepared references with planar, DC, and all 33 angular modes; negative-angle extension; three-tap and strong-bilinear smoothing; and the normative luma boundary filters. Traverse `Vector512`, `Vector256`, and `Vector128` widths before one scalar tail, use caller-owned reusable scratch for horizontal transposition, and retain no generic video, inter-picture, or reference-picture state. |
| `HeifContentColorVolume`, `HeifItem.ContentColorVolume`, and `HeifDecoderCore` content color-volume parsing and presentation | HEIF content color-volume item property; AVIF 1.2 content color-volume requirements; ITU-T H.274 (V4) content colour volume syntax and semantics | libavif `src/read.c` function `avifSkipContentColourVolume` at `092276ce89098ead06db80975173191e5fee1826`; official ITU-T H.274 (V4), January 2026 | Decode only the bounded per-image `cclv` property: require zero cancellation, persistence, and reserved bits; preserve optional signed G/B/R primary coordinates and normalized minimum, maximum, and average luminance values; and validate their registered ranges and ordering. Expose the effective grid-or-tile still-image value through `HeifMetadata`. Do not add SEI persistence, retained video state, tracks, samples, timing, or a generic ISO BMFF color-volume box model. |
| `HeifAmbientViewingEnvironment`, `HeifReferenceViewingEnvironment`, `HeifNominalDiffuseWhite`, and their per-item presentation metadata | ISOBMFF ambient viewing environment; ITU-T H.274 (V4) section 8.13; HEIF Amendment 1 sections 6.5.44 and 6.5.45; AVIF 1.2 image-item box requirements | libavif `src/read.c` functions `avifSkipAmbientViewingEnvironment`, `avifSkipReferenceViewingEnvironment`, and `avifSkipNominalDiffuseWhite` at `092276ce89098ead06db80975173191e5fee1826`; official ITU-T H.274 (V4), January 2026 | Decode the fixed-size `amve`, version-zero `reve`, and version-zero `ndwt` properties only when associated with the presented still-image item. Preserve physical illuminance and luminance units, distinct surround/periphery chromaticities, and the coded zero that requests the standard nominal diffuse-white default. Validate registered coordinate ranges and duplicate associations. Retain no video-SEI persistence, visual sample entry, display pipeline, track, timing, or generic viewing-environment box model. |
| `GridHeifItemDecoder` and `HeifDecoderCore` grid/thumbnail selection | ISO/IEC 23008-12 section 6.6.2.3 image-grid syntax and MIAF grid-cell constraints | libavif `src/read.c` functions `avifParseImageGridBox`, `avifDecoderDataAllocateImagePlanes`, and `avifDecoderDataCopyTileToImage` at `092276ce89098ead06db80975173191e5fee1826` | Parse version-zero 16-bit and 32-bit grid descriptors, preserve row-major `dimg` order, require the declared tile count and one coding format, validate canvas coverage and edge overlap, and crop only the rightmost column and bottom row while copying through ImageSharp pixel buffers. A primary grid whose tile codec is unavailable may use only a decodable thumbnail that explicitly references that grid. |
@ -458,6 +461,8 @@ Implement and verify in dependency order:
- [ ] CABAC arithmetic decoding and every required context transition.
- [ ] Coding-tree, coding-unit, prediction-unit, and transform-unit traversal across all permitted sizes and partition modes.
- [ ] Intra prediction for every luma and chroma mode, including strong intra smoothing and constrained prediction rules.
- [x] Implement and verify the allocation-free SIMD-first predictor primitive for all 35 modes, 4/8/16/32 blocks, and 8/10/12-bit samples.
- [ ] Build reference availability from reconstructed-plane and coding state, and connect the predictor to transform-unit traversal.
- [ ] Scaling lists, inverse quantization, transform skip, every required inverse transform, range-extension precision, and lossless reconstruction.
- [ ] Deblocking and sample-adaptive offset for every signaled luma/chroma and bit-depth path.
- [ ] Tiles, wavefront entry points, dependent slices, and all other parallelization syntax permitted by the exposed still-image profiles.
@ -552,6 +557,7 @@ Tasks:
- [ ] Remove known avoidable allocations first: per-transform arrays, the intermediate RGB image, repeated block scratch arrays, and file-sized buffering.
- [x] Complete the active AV1 forward and inverse transform-family checklist above.
- [ ] Benchmark codec-specific costs for CABAC/range decode, inverse transforms, still-image prediction, deblocking, SAO, CDEF, restoration, chroma upsampling, color conversion, alpha packing, and grid copies.
- [x] Add a permanent frame-wide HEVC intra-prediction benchmark. On .NET 10, SIMD planar, vertical-angular, and horizontal-angular prediction measured 155.2, 150.7, and 255.0 microseconds per padded 1920x1088 frame, compared with forced-scalar timings of 1.890, 1.081, and 0.979 milliseconds: 12.2, 7.2, and 3.8 times faster with zero managed allocations.
- [ ] Implement vector paths only for confirmed hot loops, using existing `Vector128`, `Vector256`, and `Vector512` helper and dispatch patterns where supported.
- [ ] Prioritize shared color conversion and pixel packing, chroma upsampling, inverse-transform add-and-clip, intra predictors, HEVC deblock/SAO, AV1 loop filter/CDEF/restoration, and contiguous grid copies.
- [ ] Benchmark the complete decode color pipeline on representative 8/10/12-bit AVIF and HEIC images with and without embedded ICC profiles. Report absolute end-to-end timings and allocations in addition to the isolated YUV/CICP and ICC stage costs.

423
src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.Operations.cs

@ -0,0 +1,423 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Provides shared SIMD operations used by the closed prediction operators.
/// </content>
internal static partial class HevcIntraPredictor
{
/// <summary>
/// Calculates one 512-bit half of a planar prediction row.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="indices">The zero-based X coordinates.</param>
/// <param name="left">The left reference sample for the row.</param>
/// <param name="topRight">The top-right reference sample.</param>
/// <param name="bottomLeft">The bottom-left reference sample.</param>
/// <param name="topWeight">The top-reference weight.</param>
/// <param name="bottomWeight">The bottom-left-reference weight.</param>
/// <param name="size">The square block side.</param>
/// <param name="rounding">The division rounding constant.</param>
/// <param name="shift">The division shift.</param>
/// <returns>The predicted samples as widened lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<uint> CalculatePlanarVector(
Vector512<uint> top,
Vector512<uint> indices,
uint left,
uint topRight,
uint bottomLeft,
uint topWeight,
uint bottomWeight,
uint size,
uint rounding,
int shift)
{
Vector512<uint> horizontal = ((Vector512.Create(size - 1) - indices) * left) + ((indices + Vector512<uint>.One) * topRight);
Vector512<uint> vertical = (top * topWeight) + Vector512.Create(bottomLeft * bottomWeight);
return (horizontal + vertical + Vector512.Create(rounding)) >> shift;
}
/// <summary>
/// Calculates one 256-bit half of a planar prediction row.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="indices">The zero-based X coordinates.</param>
/// <param name="left">The left reference sample for the row.</param>
/// <param name="topRight">The top-right reference sample.</param>
/// <param name="bottomLeft">The bottom-left reference sample.</param>
/// <param name="topWeight">The top-reference weight.</param>
/// <param name="bottomWeight">The bottom-left-reference weight.</param>
/// <param name="size">The square block side.</param>
/// <param name="rounding">The division rounding constant.</param>
/// <param name="shift">The division shift.</param>
/// <returns>The predicted samples as widened lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<uint> CalculatePlanarVector(
Vector256<uint> top,
Vector256<uint> indices,
uint left,
uint topRight,
uint bottomLeft,
uint topWeight,
uint bottomWeight,
uint size,
uint rounding,
int shift)
{
Vector256<uint> horizontal = ((Vector256.Create(size - 1) - indices) * left) + ((indices + Vector256<uint>.One) * topRight);
Vector256<uint> vertical = (top * topWeight) + Vector256.Create(bottomLeft * bottomWeight);
return (horizontal + vertical + Vector256.Create(rounding)) >> shift;
}
/// <summary>
/// Calculates one 128-bit half of a planar prediction row.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="indices">The zero-based X coordinates.</param>
/// <param name="left">The left reference sample for the row.</param>
/// <param name="topRight">The top-right reference sample.</param>
/// <param name="bottomLeft">The bottom-left reference sample.</param>
/// <param name="topWeight">The top-reference weight.</param>
/// <param name="bottomWeight">The bottom-left-reference weight.</param>
/// <param name="size">The square block side.</param>
/// <param name="rounding">The division rounding constant.</param>
/// <param name="shift">The division shift.</param>
/// <returns>The predicted samples as widened lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<uint> CalculatePlanarVector(
Vector128<uint> top,
Vector128<uint> indices,
uint left,
uint topRight,
uint bottomLeft,
uint topWeight,
uint bottomWeight,
uint size,
uint rounding,
int shift)
{
Vector128<uint> horizontal = ((Vector128.Create(size - 1) - indices) * left) + ((indices + Vector128<uint>.One) * topRight);
Vector128<uint> vertical = (top * topWeight) + Vector128.Create(bottomLeft * bottomWeight);
return (horizontal + vertical + Vector128.Create(rounding)) >> shift;
}
/// <summary>
/// Sums reconstructed reference samples without overflowing their 16-bit storage.
/// </summary>
/// <param name="samples">The samples to sum.</param>
/// <returns>The exact unsigned sum.</returns>
private static uint SumSamples(ReadOnlySpan<ushort> samples)
{
ref ushort samplesBase = ref MemoryMarshal.GetReference(samples);
uint sum = 0;
int i = 0;
if (Vector512.IsHardwareAccelerated)
{
int oneVectorFromEnd = samples.Length - Vector512<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<ushort>.Count)
{
(Vector512<uint> low, Vector512<uint> high) = Vector512.Widen(Vector512.LoadUnsafe(ref samplesBase, (nuint)i));
sum += Vector512.Sum(low) + Vector512.Sum(high);
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = samples.Length - Vector256<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<ushort>.Count)
{
(Vector256<uint> low, Vector256<uint> high) = Vector256.Widen(Vector256.LoadUnsafe(ref samplesBase, (nuint)i));
sum += Vector256.Sum(low) + Vector256.Sum(high);
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = samples.Length - Vector128<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<ushort>.Count)
{
(Vector128<uint> low, Vector128<uint> high) = Vector128.Widen(Vector128.LoadUnsafe(ref samplesBase, (nuint)i));
sum += Vector128.Sum(low) + Vector128.Sum(high);
}
}
for (; i < samples.Length; i++)
{
sum += Unsafe.Add(ref samplesBase, i);
}
return sum;
}
/// <summary>
/// Copies the top reference into every row and optionally filters the first column.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference samples.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="size">The square block side.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="filterPredictionEdges">Whether the vertical luma edge filter applies.</param>
private static void PredictVertical(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int bitDepth,
bool filterPredictionEdges)
{
ReadOnlySpan<ushort> row = top.Slice(1, size);
int maximum = (1 << bitDepth) - 1;
for (int y = 0; y < size; y++)
{
row.CopyTo(destination.Slice(y * destinationStride, size));
if (filterPredictionEdges)
{
int sample = destination[y * destinationStride] + ((left[y + 1] - left[0]) >> 1);
destination[y * destinationStride] = (ushort)Math.Clamp(sample, 0, maximum);
}
}
}
/// <summary>
/// Fills each row from its left reference and optionally filters the first row.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference samples.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="size">The square block side.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="filterPredictionEdges">Whether the horizontal luma edge filter applies.</param>
private static void PredictHorizontal(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int bitDepth,
bool filterPredictionEdges)
{
for (int y = 0; y < size; y++)
{
destination.Slice(y * destinationStride, size).Fill(left[y + 1]);
}
if (!filterPredictionEdges)
{
return;
}
int maximum = (1 << bitDepth) - 1;
for (int x = 0; x < size; x++)
{
int sample = destination[x] + ((top[x + 1] - top[0]) >> 1);
destination[x] = (ushort)Math.Clamp(sample, 0, maximum);
}
}
/// <summary>
/// Generates a vertical-oriented angular block using contiguous SIMD interpolation within each row.
/// </summary>
/// <param name="main">The main reference beginning at logical index zero.</param>
/// <param name="mainOrigin">The span index corresponding to logical reference index zero.</param>
/// <param name="destination">The contiguous destination or transposition scratch block.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="size">The square block side.</param>
/// <param name="angle">The signed prediction displacement in thirty-second-sample units.</param>
private static void PredictAngularRows(
ReadOnlySpan<ushort> main,
int mainOrigin,
Span<ushort> destination,
int destinationStride,
int size,
int angle)
{
for (int y = 0, deltaPosition = angle; y < size; y++, deltaPosition += angle)
{
int deltaInteger = deltaPosition >> 5;
int deltaFraction = deltaPosition & 31;
int sourceOffset = mainOrigin + deltaInteger + 1;
Span<ushort> row = destination.Slice(y * destinationStride, size);
if (deltaFraction == 0)
{
main.Slice(sourceOffset, size).CopyTo(row);
}
else
{
InterpolateAngularRow(main[sourceOffset..], row, deltaFraction);
}
}
}
/// <summary>
/// Interpolates one angular prediction row between consecutive main-reference samples.
/// </summary>
/// <param name="source">The first main-reference sample for the row.</param>
/// <param name="destination">The destination prediction row.</param>
/// <param name="fraction">The right-hand weight with a denominator of thirty-two.</param>
private static void InterpolateAngularRow(ReadOnlySpan<ushort> source, Span<ushort> destination, int fraction)
{
ref ushort sourceBase = ref MemoryMarshal.GetReference(source);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
uint leftWeight = (uint)(32 - fraction);
uint rightWeight = (uint)fraction;
int i = 0;
if (Vector512.IsHardwareAccelerated)
{
int oneVectorFromEnd = destination.Length - Vector512<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<ushort>.Count)
{
Vector512<ushort> left = Vector512.LoadUnsafe(ref sourceBase, (nuint)i);
Vector512<ushort> right = Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + 1));
(Vector512<uint> leftLow, Vector512<uint> leftHigh) = Vector512.Widen(left);
(Vector512<uint> rightLow, Vector512<uint> rightHigh) = Vector512.Widen(right);
Vector512<uint> low = ((leftLow * leftWeight) + (rightLow * rightWeight) + Vector512.Create(16U)) >> 5;
Vector512<uint> high = ((leftHigh * leftWeight) + (rightHigh * rightWeight) + Vector512.Create(16U)) >> 5;
Vector512.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = destination.Length - Vector256<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<ushort>.Count)
{
Vector256<ushort> left = Vector256.LoadUnsafe(ref sourceBase, (nuint)i);
Vector256<ushort> right = Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + 1));
(Vector256<uint> leftLow, Vector256<uint> leftHigh) = Vector256.Widen(left);
(Vector256<uint> rightLow, Vector256<uint> rightHigh) = Vector256.Widen(right);
Vector256<uint> low = ((leftLow * leftWeight) + (rightLow * rightWeight) + Vector256.Create(16U)) >> 5;
Vector256<uint> high = ((leftHigh * leftWeight) + (rightHigh * rightWeight) + Vector256.Create(16U)) >> 5;
Vector256.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = destination.Length - Vector128<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<ushort>.Count)
{
Vector128<ushort> left = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
Vector128<ushort> right = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + 1));
(Vector128<uint> leftLow, Vector128<uint> leftHigh) = Vector128.Widen(left);
(Vector128<uint> rightLow, Vector128<uint> rightHigh) = Vector128.Widen(right);
Vector128<uint> low = ((leftLow * leftWeight) + (rightLow * rightWeight) + Vector128.Create(16U)) >> 5;
Vector128<uint> high = ((leftHigh * leftWeight) + (rightHigh * rightWeight) + Vector128.Create(16U)) >> 5;
Vector128.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
for (; i < destination.Length; i++)
{
Unsafe.Add(ref destinationBase, i) = (ushort)(((source[i] * leftWeight) + (source[i + 1] * rightWeight) + 16) >> 5);
}
}
/// <summary>
/// Transposes a square horizontal prediction block into the reconstructed destination.
/// </summary>
/// <param name="source">The contiguous transposed prediction block.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="size">The square block side.</param>
private static void TransposeBlock(ReadOnlySpan<ushort> source, Span<ushort> destination, int destinationStride, int size)
{
if (Vector128.IsHardwareAccelerated && size >= Vector128<ushort>.Count)
{
for (int y = 0; y < size; y += Vector128<ushort>.Count)
{
for (int x = 0; x < size; x += Vector128<ushort>.Count)
{
Transpose8x8(source, destination, destinationStride, size, x, y);
}
}
return;
}
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
destination[(x * destinationStride) + y] = source[(y * size) + x];
}
}
}
/// <summary>
/// Transposes one eight-by-eight tile of 16-bit prediction samples.
/// </summary>
/// <param name="source">The contiguous source block.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="sourceStride">The contiguous source row stride.</param>
/// <param name="x">The tile X coordinate in the source block.</param>
/// <param name="y">The tile Y coordinate in the source block.</param>
private static void Transpose8x8(
ReadOnlySpan<ushort> source,
Span<ushort> destination,
int destinationStride,
int sourceStride,
int x,
int y)
{
ref ushort sourceBase = ref MemoryMarshal.GetReference(source);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
Vector128<short> row0 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 0) * sourceStride) + x)).AsInt16();
Vector128<short> row1 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 1) * sourceStride) + x)).AsInt16();
Vector128<short> row2 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 2) * sourceStride) + x)).AsInt16();
Vector128<short> row3 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 3) * sourceStride) + x)).AsInt16();
Vector128<short> row4 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 4) * sourceStride) + x)).AsInt16();
Vector128<short> row5 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 5) * sourceStride) + x)).AsInt16();
Vector128<short> row6 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 6) * sourceStride) + x)).AsInt16();
Vector128<short> row7 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 7) * sourceStride) + x)).AsInt16();
// Three zip stages exchange one, two, then four 16-bit coordinates. The resulting vectors are the eight
// source columns in row order, so each can be stored contiguously into one destination row.
Vector128<short> pair0 = Vector128_.UnpackLow(row0, row1);
Vector128<short> pair1 = Vector128_.UnpackHigh(row0, row1);
Vector128<short> pair2 = Vector128_.UnpackLow(row2, row3);
Vector128<short> pair3 = Vector128_.UnpackHigh(row2, row3);
Vector128<short> pair4 = Vector128_.UnpackLow(row4, row5);
Vector128<short> pair5 = Vector128_.UnpackHigh(row4, row5);
Vector128<short> pair6 = Vector128_.UnpackLow(row6, row7);
Vector128<short> pair7 = Vector128_.UnpackHigh(row6, row7);
Vector128<int> quad0 = Vector128_.UnpackLow(pair0.AsInt32(), pair2.AsInt32());
Vector128<int> quad1 = Vector128_.UnpackHigh(pair0.AsInt32(), pair2.AsInt32());
Vector128<int> quad2 = Vector128_.UnpackLow(pair1.AsInt32(), pair3.AsInt32());
Vector128<int> quad3 = Vector128_.UnpackHigh(pair1.AsInt32(), pair3.AsInt32());
Vector128<int> quad4 = Vector128_.UnpackLow(pair4.AsInt32(), pair6.AsInt32());
Vector128<int> quad5 = Vector128_.UnpackHigh(pair4.AsInt32(), pair6.AsInt32());
Vector128<int> quad6 = Vector128_.UnpackLow(pair5.AsInt32(), pair7.AsInt32());
Vector128<int> quad7 = Vector128_.UnpackHigh(pair5.AsInt32(), pair7.AsInt32());
Vector128<ushort> column0 = Vector128_.UnpackLow(quad0.AsInt64(), quad4.AsInt64()).AsUInt16();
Vector128<ushort> column1 = Vector128_.UnpackHigh(quad0.AsInt64(), quad4.AsInt64()).AsUInt16();
Vector128<ushort> column2 = Vector128_.UnpackLow(quad1.AsInt64(), quad5.AsInt64()).AsUInt16();
Vector128<ushort> column3 = Vector128_.UnpackHigh(quad1.AsInt64(), quad5.AsInt64()).AsUInt16();
Vector128<ushort> column4 = Vector128_.UnpackLow(quad2.AsInt64(), quad6.AsInt64()).AsUInt16();
Vector128<ushort> column5 = Vector128_.UnpackHigh(quad2.AsInt64(), quad6.AsInt64()).AsUInt16();
Vector128<ushort> column6 = Vector128_.UnpackLow(quad3.AsInt64(), quad7.AsInt64()).AsUInt16();
Vector128<ushort> column7 = Vector128_.UnpackHigh(quad3.AsInt64(), quad7.AsInt64()).AsUInt16();
column0.StoreUnsafe(ref destinationBase, (nuint)(((x + 0) * destinationStride) + y));
column1.StoreUnsafe(ref destinationBase, (nuint)(((x + 1) * destinationStride) + y));
column2.StoreUnsafe(ref destinationBase, (nuint)(((x + 2) * destinationStride) + y));
column3.StoreUnsafe(ref destinationBase, (nuint)(((x + 3) * destinationStride) + y));
column4.StoreUnsafe(ref destinationBase, (nuint)(((x + 4) * destinationStride) + y));
column5.StoreUnsafe(ref destinationBase, (nuint)(((x + 5) * destinationStride) + y));
column6.StoreUnsafe(ref destinationBase, (nuint)(((x + 6) * destinationStride) + y));
column7.StoreUnsafe(ref destinationBase, (nuint)(((x + 7) * destinationStride) + y));
}
}

280
src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.OperatorImplementations.cs

@ -0,0 +1,280 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Provides the closed planar, DC, and angular prediction operators.
/// </content>
internal static partial class HevcIntraPredictor
{
/// <summary>
/// Implements planar interpolation between the top, left, bottom-left, and top-right references.
/// </summary>
private readonly struct PlanarPredictionOperator : IHevcIntraPredictionOperator<PlanarPredictionOperator>
{
/// <inheritdoc/>
public static void Predict(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int mode,
int bitDepth,
bool filterPredictionEdges,
Span<ushort> scratch)
{
ref ushort topBase = ref MemoryMarshal.GetReference(top);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
uint bottomLeft = left[size];
uint topRight = top[size];
int shift = BitOperations.Log2((uint)size) + 1;
uint rounding = (uint)size;
for (int y = 0; y < size; y++)
{
uint leftSample = left[y + 1];
uint topWeight = (uint)(size - y - 1);
uint bottomWeight = (uint)(y + 1);
ref ushort rowBase = ref Unsafe.Add(ref destinationBase, y * destinationStride);
int x = 0;
// The two widened halves carry consecutive X coordinates. Each lane evaluates the normative
// horizontal and vertical ramps, then narrows after the common rounded power-of-two division.
if (Vector512.IsHardwareAccelerated)
{
Vector512<uint> indices = CreateIndicesVector512();
int oneVectorFromEnd = size - Vector512<ushort>.Count;
for (; x <= oneVectorFromEnd; x += Vector512<ushort>.Count)
{
Vector512<ushort> topSamples = Vector512.LoadUnsafe(ref topBase, (nuint)(x + 1));
(Vector512<uint> topLow, Vector512<uint> topHigh) = Vector512.Widen(topSamples);
Vector512<uint> lowIndices = indices + Vector512.Create((uint)x);
Vector512<uint> highIndices = lowIndices + Vector512.Create((uint)Vector512<uint>.Count);
Vector512<uint> low = CalculatePlanarVector(
topLow,
lowIndices,
leftSample,
topRight,
bottomLeft,
topWeight,
bottomWeight,
(uint)size,
rounding,
shift);
Vector512<uint> high = CalculatePlanarVector(
topHigh,
highIndices,
leftSample,
topRight,
bottomLeft,
topWeight,
bottomWeight,
(uint)size,
rounding,
shift);
Vector512.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref rowBase, x));
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<uint> indices = CreateIndicesVector256();
int oneVectorFromEnd = size - Vector256<ushort>.Count;
for (; x <= oneVectorFromEnd; x += Vector256<ushort>.Count)
{
Vector256<ushort> topSamples = Vector256.LoadUnsafe(ref topBase, (nuint)(x + 1));
(Vector256<uint> topLow, Vector256<uint> topHigh) = Vector256.Widen(topSamples);
Vector256<uint> lowIndices = indices + Vector256.Create((uint)x);
Vector256<uint> highIndices = lowIndices + Vector256.Create((uint)Vector256<uint>.Count);
Vector256<uint> low = CalculatePlanarVector(
topLow,
lowIndices,
leftSample,
topRight,
bottomLeft,
topWeight,
bottomWeight,
(uint)size,
rounding,
shift);
Vector256<uint> high = CalculatePlanarVector(
topHigh,
highIndices,
leftSample,
topRight,
bottomLeft,
topWeight,
bottomWeight,
(uint)size,
rounding,
shift);
Vector256.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref rowBase, x));
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<uint> indices = CreateIndicesVector128();
int oneVectorFromEnd = size - Vector128<ushort>.Count;
for (; x <= oneVectorFromEnd; x += Vector128<ushort>.Count)
{
Vector128<ushort> topSamples = Vector128.LoadUnsafe(ref topBase, (nuint)(x + 1));
(Vector128<uint> topLow, Vector128<uint> topHigh) = Vector128.Widen(topSamples);
Vector128<uint> lowIndices = indices + Vector128.Create((uint)x);
Vector128<uint> highIndices = lowIndices + Vector128.Create((uint)Vector128<uint>.Count);
Vector128<uint> low = CalculatePlanarVector(
topLow,
lowIndices,
leftSample,
topRight,
bottomLeft,
topWeight,
bottomWeight,
(uint)size,
rounding,
shift);
Vector128<uint> high = CalculatePlanarVector(
topHigh,
highIndices,
leftSample,
topRight,
bottomLeft,
topWeight,
bottomWeight,
(uint)size,
rounding,
shift);
Vector128.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref rowBase, x));
}
}
for (; x < size; x++)
{
uint horizontal = ((uint)(size - x - 1) * leftSample) + ((uint)(x + 1) * topRight);
uint vertical = ((uint)(size - y - 1) * top[x + 1]) + ((uint)(y + 1) * bottomLeft);
Unsafe.Add(ref rowBase, x) = (ushort)((horizontal + vertical + (uint)size) >> shift);
}
}
}
}
/// <summary>
/// Implements DC prediction and its optional luma boundary filter.
/// </summary>
private readonly struct DcPredictionOperator : IHevcIntraPredictionOperator<DcPredictionOperator>
{
/// <inheritdoc/>
public static void Predict(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int mode,
int bitDepth,
bool filterPredictionEdges,
Span<ushort> scratch)
{
uint sum = SumSamples(top.Slice(1, size)) + SumSamples(left.Slice(1, size));
ushort dc = (ushort)((sum + (uint)size) >> (BitOperations.Log2((uint)size) + 1));
for (int y = 0; y < size; y++)
{
destination.Slice(y * destinationStride, size).Fill(dc);
}
if (!filterPredictionEdges)
{
return;
}
destination[0] = (ushort)((top[1] + left[1] + (2 * dc) + 2) >> 2);
for (int x = 1; x < size; x++)
{
destination[x] = (ushort)((top[x + 1] + (3 * dc) + 2) >> 2);
}
for (int y = 1; y < size; y++)
{
destination[y * destinationStride] = (ushort)((left[y + 1] + (3 * dc) + 2) >> 2);
}
}
}
/// <summary>
/// Implements the thirty-three directional intra-prediction modes.
/// </summary>
private readonly struct AngularPredictionOperator : IHevcIntraPredictionOperator<AngularPredictionOperator>
{
/// <inheritdoc/>
public static void Predict(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int mode,
int bitDepth,
bool filterPredictionEdges,
Span<ushort> scratch)
{
if (mode == VerticalMode)
{
PredictVertical(top, left, destination, destinationStride, size, bitDepth, filterPredictionEdges);
return;
}
if (mode == HorizontalMode)
{
PredictHorizontal(top, left, destination, destinationStride, size, bitDepth, filterPredictionEdges);
return;
}
bool vertical = mode >= FirstVerticalMode;
int angleMode = vertical ? mode - VerticalMode : HorizontalMode - mode;
int absoluteAngleMode = Math.Abs(angleMode);
int angle = PredictionAngles[absoluteAngleMode] * Math.Sign(angleMode);
ReadOnlySpan<ushort> main = vertical ? top : left;
ReadOnlySpan<ushort> side = vertical ? left : top;
Span<ushort> temporaryBlock = scratch[..(size * size)];
Span<ushort> extendedReference = scratch.Slice(size * size, (4 * size) + 1);
int mainOrigin = 0;
if (angle < 0)
{
mainOrigin = size * 2;
main[..(size + 1)].CopyTo(extendedReference[mainOrigin..]);
int inverseAngle = InversePredictionAngles[absoluteAngleMode];
int inverseAngleSum = 128;
int minimumIndex = (size * angle) >> 5;
for (int index = -1; index > minimumIndex; index--)
{
inverseAngleSum += inverseAngle;
extendedReference[mainOrigin + index] = side[inverseAngleSum >> 8];
}
main = extendedReference;
}
Span<ushort> prediction = vertical ? destination : temporaryBlock;
int predictionStride = vertical ? destinationStride : size;
PredictAngularRows(main, mainOrigin, prediction, predictionStride, size, angle);
if (!vertical)
{
TransposeBlock(temporaryBlock, destination, destinationStride, size);
}
}
}
}

412
src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.cs

@ -0,0 +1,412 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Reconstructs HEVC intra-prediction blocks from prepared neighboring samples.
/// </summary>
internal static partial class HevcIntraPredictor
{
/// <summary>
/// The HEVC planar prediction mode.
/// </summary>
private const int PlanarMode = 0;
/// <summary>
/// The HEVC DC prediction mode.
/// </summary>
private const int DcMode = 1;
/// <summary>
/// The HEVC horizontal prediction mode.
/// </summary>
private const int HorizontalMode = 10;
/// <summary>
/// The first prediction mode whose main reference is the top row.
/// </summary>
private const int FirstVerticalMode = 18;
/// <summary>
/// The HEVC vertical prediction mode.
/// </summary>
private const int VerticalMode = 26;
/// <summary>
/// The largest transform-block side supported by HEVC intra prediction.
/// </summary>
private const int MaximumBlockSize = 32;
/// <summary>
/// Defines one closed intra-prediction operation selected by the decoded mode.
/// </summary>
/// <typeparam name="TOperator">The implementing operator type.</typeparam>
private interface IHevcIntraPredictionOperator<TOperator>
where TOperator : struct, IHevcIntraPredictionOperator<TOperator>
{
/// <summary>
/// Reconstructs one square prediction block.
/// </summary>
/// <param name="top">The top-left, top, and top-right reference samples.</param>
/// <param name="left">The top-left, left, and below-left reference samples.</param>
/// <param name="destination">The destination buffer beginning at the block origin.</param>
/// <param name="destinationStride">The destination row stride in samples.</param>
/// <param name="size">The square block side in samples.</param>
/// <param name="mode">The decoded prediction mode.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="filterPredictionEdges">Whether the luma edge filter applies to the selected block.</param>
/// <param name="scratch">The caller-owned block and extended-reference scratch space.</param>
public static abstract void Predict(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int mode,
int bitDepth,
bool filterPredictionEdges,
Span<ushort> scratch);
}
/// <summary>
/// Gets the angle selected by each absolute angular-mode displacement.
/// </summary>
private static ReadOnlySpan<int> PredictionAngles => [0, 2, 5, 9, 13, 17, 21, 26, 32];
/// <summary>
/// Gets the reciprocal angle used to extend the main reference for negative directions.
/// </summary>
private static ReadOnlySpan<int> InversePredictionAngles => [0, 4096, 1638, 910, 630, 482, 390, 315, 256];
/// <summary>
/// Gets the scratch length required to predict a block of the specified size.
/// </summary>
/// <param name="log2Size">The base-two logarithm of the square block side.</param>
/// <returns>The required number of <see cref="ushort"/> elements.</returns>
public static int GetScratchLength(int log2Size)
{
DebugGuard.MustBeBetweenOrEqualTo(log2Size, 2, 5, nameof(log2Size));
int size = 1 << log2Size;
return (size * size) + (4 * size) + 1;
}
/// <summary>
/// Reconstructs one square intra-prediction block using a closed operator selected by the decoded mode.
/// </summary>
/// <param name="top">The top-left, top, and top-right reference samples.</param>
/// <param name="left">The top-left, left, and below-left reference samples.</param>
/// <param name="destination">The destination buffer beginning at the block origin.</param>
/// <param name="destinationStride">The destination row stride in samples.</param>
/// <param name="log2Size">The base-two logarithm of the square block side.</param>
/// <param name="mode">The decoded prediction mode in the inclusive range zero through thirty-four.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="filterPredictionEdges">Whether the luma edge filter applies to the selected block.</param>
/// <param name="scratch">The caller-owned scratch returned by <see cref="GetScratchLength(int)"/>.</param>
public static void Predict(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int log2Size,
int mode,
int bitDepth,
bool filterPredictionEdges,
Span<ushort> scratch)
{
DebugGuard.MustBeBetweenOrEqualTo(log2Size, 2, 5, nameof(log2Size));
DebugGuard.MustBeBetweenOrEqualTo(mode, PlanarMode, 34, nameof(mode));
int size = 1 << log2Size;
switch (mode)
{
case PlanarMode:
Predict<PlanarPredictionOperator>(
top,
left,
destination,
destinationStride,
size,
mode,
bitDepth,
filterPredictionEdges,
scratch);
break;
case DcMode:
Predict<DcPredictionOperator>(
top,
left,
destination,
destinationStride,
size,
mode,
bitDepth,
filterPredictionEdges,
scratch);
break;
default:
Predict<AngularPredictionOperator>(
top,
left,
destination,
destinationStride,
size,
mode,
bitDepth,
filterPredictionEdges,
scratch);
break;
}
}
/// <summary>
/// Filters prepared reference samples using the normative three-tap or strong bilinear filter.
/// </summary>
/// <param name="top">The unfiltered top-left, top, and top-right samples.</param>
/// <param name="left">The unfiltered top-left, left, and below-left samples.</param>
/// <param name="filteredTop">The destination top reference.</param>
/// <param name="filteredLeft">The destination left reference.</param>
/// <param name="log2Size">The base-two logarithm of the square prediction-block side.</param>
/// <param name="bitDepth">The reconstructed luma precision.</param>
/// <param name="strongIntraSmoothingEnabled">Whether the sequence permits strong intra smoothing.</param>
public static void FilterReferenceSamples(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> filteredTop,
Span<ushort> filteredLeft,
int log2Size,
int bitDepth,
bool strongIntraSmoothingEnabled)
{
DebugGuard.MustBeBetweenOrEqualTo(log2Size, 2, 5, nameof(log2Size));
int size = 1 << log2Size;
int referenceLength = (size * 2) + 1;
bool useStrongSmoothing = strongIntraSmoothingEnabled && size == MaximumBlockSize;
if (useStrongSmoothing)
{
int threshold = 1 << (bitDepth - 5);
int last = referenceLength - 1;
bool leftIsBilinear = Math.Abs((left[last] + left[0]) - (2 * left[size])) < threshold;
bool topIsBilinear = Math.Abs((top[0] + top[last]) - (2 * top[size])) < threshold;
useStrongSmoothing = leftIsBilinear && topIsBilinear;
}
if (useStrongSmoothing)
{
FilterReferenceBilinear(top[..referenceLength], filteredTop, size);
FilterReferenceBilinear(left[..referenceLength], filteredLeft, size);
return;
}
// The corner belongs to both references. Filtering it once from the first samples on both sides keeps the
// two logical arrays identical at index zero before their independent one-dimensional filters continue.
ushort filteredCorner = (ushort)((left[1] + (2 * top[0]) + top[1] + 2) >> 2);
filteredTop[0] = filteredCorner;
filteredLeft[0] = filteredCorner;
FilterReferenceThreeTap(top[..referenceLength], filteredTop);
FilterReferenceThreeTap(left[..referenceLength], filteredLeft);
}
/// <summary>
/// Invokes one statically selected prediction operator without interface dispatch in the block loop.
/// </summary>
/// <typeparam name="TOperator">The selected prediction operator.</typeparam>
/// <param name="top">The prepared top reference.</param>
/// <param name="left">The prepared left reference.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride in samples.</param>
/// <param name="size">The square block side in samples.</param>
/// <param name="mode">The decoded prediction mode.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="filterPredictionEdges">Whether the luma edge filter applies.</param>
/// <param name="scratch">The caller-owned prediction scratch.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Predict<TOperator>(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int mode,
int bitDepth,
bool filterPredictionEdges,
Span<ushort> scratch)
where TOperator : struct, IHevcIntraPredictionOperator<TOperator>
=> TOperator.Predict(
top,
left,
destination,
destinationStride,
size,
mode,
bitDepth,
filterPredictionEdges,
scratch);
/// <summary>
/// Applies the strong bilinear filter between the reference endpoints.
/// </summary>
/// <param name="source">The complete unfiltered reference.</param>
/// <param name="destination">The complete filtered reference.</param>
/// <param name="size">The prediction-block side in samples.</param>
private static void FilterReferenceBilinear(ReadOnlySpan<ushort> source, Span<ushort> destination, int size)
{
int last = source.Length - 1;
destination[0] = source[0];
destination[last] = source[last];
ref ushort sourceBase = ref MemoryMarshal.GetReference(source);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
uint first = source[0];
uint final = source[last];
int shift = BitOperations.Log2((uint)(size * 2));
uint rounding = (uint)size;
int i = 1;
// Each widened lane represents one reference coordinate. The weights sum to 2N, so narrowing is exact
// after the rounded shift for every supported 8, 10, and 12-bit sample.
if (Vector512.IsHardwareAccelerated)
{
Vector512<uint> indices = CreateIndicesVector512();
int oneVectorFromEnd = last - Vector512<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<ushort>.Count)
{
Vector512<uint> lowerIndices = indices + Vector512.Create((uint)i);
Vector512<uint> upperIndices = lowerIndices + Vector512.Create((uint)Vector512<uint>.Count);
Vector512<uint> lower = (((Vector512.Create((uint)last) - lowerIndices) * first) + (lowerIndices * final) + Vector512.Create(rounding)) >> shift;
Vector512<uint> upper = (((Vector512.Create((uint)last) - upperIndices) * first) + (upperIndices * final) + Vector512.Create(rounding)) >> shift;
Vector512.Narrow(lower, upper).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<uint> indices = CreateIndicesVector256();
int oneVectorFromEnd = last - Vector256<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<ushort>.Count)
{
Vector256<uint> lowerIndices = indices + Vector256.Create((uint)i);
Vector256<uint> upperIndices = lowerIndices + Vector256.Create((uint)Vector256<uint>.Count);
Vector256<uint> lower = (((Vector256.Create((uint)last) - lowerIndices) * first) + (lowerIndices * final) + Vector256.Create(rounding)) >> shift;
Vector256<uint> upper = (((Vector256.Create((uint)last) - upperIndices) * first) + (upperIndices * final) + Vector256.Create(rounding)) >> shift;
Vector256.Narrow(lower, upper).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<uint> indices = CreateIndicesVector128();
int oneVectorFromEnd = last - Vector128<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<ushort>.Count)
{
Vector128<uint> lowerIndices = indices + Vector128.Create((uint)i);
Vector128<uint> upperIndices = lowerIndices + Vector128.Create((uint)Vector128<uint>.Count);
Vector128<uint> lower = (((Vector128.Create((uint)last) - lowerIndices) * first) + (lowerIndices * final) + Vector128.Create(rounding)) >> shift;
Vector128<uint> upper = (((Vector128.Create((uint)last) - upperIndices) * first) + (upperIndices * final) + Vector128.Create(rounding)) >> shift;
Vector128.Narrow(lower, upper).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
for (; i < last; i++)
{
Unsafe.Add(ref destinationBase, i) = (ushort)((((last - i) * first) + (i * final) + rounding) >> shift);
}
}
/// <summary>
/// Applies the normal three-tap reference filter to every non-endpoint sample.
/// </summary>
/// <param name="source">The complete unfiltered reference.</param>
/// <param name="destination">The complete filtered reference with its corner already initialized.</param>
private static void FilterReferenceThreeTap(ReadOnlySpan<ushort> source, Span<ushort> destination)
{
int last = source.Length - 1;
destination[last] = source[last];
ref ushort sourceBase = ref MemoryMarshal.GetReference(source);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
int i = 1;
if (Vector512.IsHardwareAccelerated)
{
int oneVectorFromEnd = last - Vector512<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<ushort>.Count)
{
Vector512<ushort> previous = Vector512.LoadUnsafe(ref sourceBase, (nuint)(i - 1));
Vector512<ushort> current = Vector512.LoadUnsafe(ref sourceBase, (nuint)i);
Vector512<ushort> next = Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + 1));
(Vector512<uint> previousLow, Vector512<uint> previousHigh) = Vector512.Widen(previous);
(Vector512<uint> currentLow, Vector512<uint> currentHigh) = Vector512.Widen(current);
(Vector512<uint> nextLow, Vector512<uint> nextHigh) = Vector512.Widen(next);
Vector512<uint> low = (previousLow + (currentLow << 1) + nextLow + Vector512.Create(2U)) >> 2;
Vector512<uint> high = (previousHigh + (currentHigh << 1) + nextHigh + Vector512.Create(2U)) >> 2;
Vector512.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = last - Vector256<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<ushort>.Count)
{
Vector256<ushort> previous = Vector256.LoadUnsafe(ref sourceBase, (nuint)(i - 1));
Vector256<ushort> current = Vector256.LoadUnsafe(ref sourceBase, (nuint)i);
Vector256<ushort> next = Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + 1));
(Vector256<uint> previousLow, Vector256<uint> previousHigh) = Vector256.Widen(previous);
(Vector256<uint> currentLow, Vector256<uint> currentHigh) = Vector256.Widen(current);
(Vector256<uint> nextLow, Vector256<uint> nextHigh) = Vector256.Widen(next);
Vector256<uint> low = (previousLow + (currentLow << 1) + nextLow + Vector256.Create(2U)) >> 2;
Vector256<uint> high = (previousHigh + (currentHigh << 1) + nextHigh + Vector256.Create(2U)) >> 2;
Vector256.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = last - Vector128<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<ushort>.Count)
{
Vector128<ushort> previous = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i - 1));
Vector128<ushort> current = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
Vector128<ushort> next = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + 1));
(Vector128<uint> previousLow, Vector128<uint> previousHigh) = Vector128.Widen(previous);
(Vector128<uint> currentLow, Vector128<uint> currentHigh) = Vector128.Widen(current);
(Vector128<uint> nextLow, Vector128<uint> nextHigh) = Vector128.Widen(next);
Vector128<uint> low = (previousLow + (currentLow << 1) + nextLow + Vector128.Create(2U)) >> 2;
Vector128<uint> high = (previousHigh + (currentHigh << 1) + nextHigh + Vector128.Create(2U)) >> 2;
Vector128.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
for (; i < last; i++)
{
Unsafe.Add(ref destinationBase, i) = (ushort)((source[i - 1] + (2 * source[i]) + source[i + 1] + 2) >> 2);
}
}
/// <summary>
/// Creates the zero-through-fifteen lane indices used by 512-bit weighted interpolation.
/// </summary>
/// <returns>The ordered lane indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<uint> CreateIndicesVector512()
=> Vector512.Create(0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 11U, 12U, 13U, 14U, 15U);
/// <summary>
/// Creates the zero-through-seven lane indices used by 256-bit weighted interpolation.
/// </summary>
/// <returns>The ordered lane indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<uint> CreateIndicesVector256() => Vector256.Create(0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U);
/// <summary>
/// Creates the zero-through-three lane indices used by 128-bit weighted interpolation.
/// </summary>
/// <returns>The ordered lane indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<uint> CreateIndicesVector128() => Vector128.Create(0U, 1U, 2U, 3U);
}

116
tests/ImageSharp.Benchmarks/Codecs/Heif/HevcIntraPredictionBenchmarks.cs

@ -0,0 +1,116 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Heif.Hevc;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif;
/// <summary>
/// Measures complete coded-frame traversal through representative HEVC intra-prediction modes.
/// </summary>
[MemoryDiagnoser(displayGenColumns: false)]
public class HevcIntraPredictionBenchmarks
{
/// <summary>
/// The coded frame width, which is an exact multiple of the maximum transform-block side.
/// </summary>
private const int Width = 1920;
/// <summary>
/// The coded frame height including the final padded coding-tree row for a 1080-line presentation.
/// </summary>
private const int Height = 1088;
/// <summary>
/// The base-two logarithm of the benchmark prediction-block side.
/// </summary>
private const int BlockLog2 = 5;
/// <summary>
/// The prediction-block side in samples.
/// </summary>
private const int BlockSize = 1 << BlockLog2;
/// <summary>
/// The prepared top reference shared by deterministic benchmark blocks.
/// </summary>
private readonly ushort[] top = new ushort[(BlockSize * 2) + 1];
/// <summary>
/// The prepared left reference shared by deterministic benchmark blocks.
/// </summary>
private readonly ushort[] left = new ushort[(BlockSize * 2) + 1];
/// <summary>
/// The frame-wide reconstructed prediction samples.
/// </summary>
private readonly ushort[] destination = new ushort[Width * Height];
/// <summary>
/// The maximum-block scratch reused throughout each coded frame.
/// </summary>
private readonly ushort[] scratch = new ushort[HevcIntraPredictor.GetScratchLength(BlockLog2)];
/// <summary>
/// Populates deterministic twelve-bit reference samples outside the measured frame traversal.
/// </summary>
[GlobalSetup]
public void Setup()
{
this.top[0] = this.left[0] = 1365;
for (int i = 1; i < this.top.Length; i++)
{
this.top[i] = (ushort)((1365 + (37 * i)) & 4095);
this.left[i] = (ushort)((1365 + (53 * i)) & 4095);
}
}
/// <summary>
/// Measures frame-wide planar prediction with 512-, 256-, and 128-bit row dispatch where available.
/// </summary>
/// <returns>The final reconstructed sample, keeping the frame output observable.</returns>
[Benchmark(Baseline = true)]
public ushort PredictPlanarFrame() => this.PredictFrame(0);
/// <summary>
/// Measures frame-wide fractional vertical prediction using contiguous SIMD interpolation.
/// </summary>
/// <returns>The final reconstructed sample, keeping the frame output observable.</returns>
[Benchmark]
public ushort PredictVerticalAngularFrame() => this.PredictFrame(30);
/// <summary>
/// Measures frame-wide horizontal prediction including the SIMD block transposition stage.
/// </summary>
/// <returns>The final reconstructed sample, keeping the frame output observable.</returns>
[Benchmark]
public ushort PredictHorizontalAngularFrame() => this.PredictFrame(2);
/// <summary>
/// Reconstructs every maximum-size prediction block in the coded benchmark frame.
/// </summary>
/// <param name="mode">The HEVC intra-prediction mode.</param>
/// <returns>The final reconstructed sample.</returns>
private ushort PredictFrame(int mode)
{
for (int y = 0; y < Height; y += BlockSize)
{
for (int x = 0; x < Width; x += BlockSize)
{
HevcIntraPredictor.Predict(
this.top,
this.left,
this.destination.AsSpan((y * Width) + x),
Width,
BlockLog2,
mode,
12,
true,
this.scratch);
}
}
return this.destination[^1];
}
}

396
tests/ImageSharp.Tests/Formats/Heif/Hevc/HevcIntraPredictorTests.cs

@ -0,0 +1,396 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Heif.Hevc;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Hevc;
/// <summary>
/// Verifies HEVC planar, DC, angular, reference-filter, and SIMD prediction behavior.
/// </summary>
[Trait("Format", "Heic")]
public class HevcIntraPredictorTests
{
/// <summary>
/// Verifies fixed four-by-four prediction results derived from the HEVC intra-prediction equations.
/// </summary>
[Fact]
public void PredictsKnownFourByFourBlocks()
{
ushort[] top = [64, 80, 96, 112, 128, 144, 160, 176, 192];
ushort[] left = [64, 70, 76, 82, 88, 94, 100, 106, 112];
int[] modes = [0, 1, 2, 9, 18, 30, 34];
ushort[][] expected =
[
[83, 97, 110, 123, 87, 97, 108, 118, 90, 98, 105, 113, 93, 98, 103, 108],
[84, 93, 97, 101, 88, 92, 92, 92, 90, 92, 92, 92, 91, 92, 92, 92],
[76, 82, 88, 94, 82, 88, 94, 100, 88, 94, 100, 106, 94, 100, 106, 112],
[70, 71, 71, 72, 76, 77, 77, 78, 82, 83, 83, 84, 88, 89, 89, 90],
[64, 80, 96, 112, 70, 64, 80, 96, 76, 70, 64, 80, 82, 76, 70, 64],
[87, 103, 119, 135, 93, 109, 125, 141, 100, 116, 132, 148, 106, 122, 138, 154],
[96, 112, 128, 144, 112, 128, 144, 160, 128, 144, 160, 176, 144, 160, 176, 192]
];
const int size = 4;
const int stride = 7;
ushort[] destination = new ushort[stride * size];
ushort[] scratch = new ushort[HevcIntraPredictor.GetScratchLength(2)];
for (int caseIndex = 0; caseIndex < modes.Length; caseIndex++)
{
destination.AsSpan().Fill(ushort.MaxValue);
int mode = modes[caseIndex];
HevcIntraPredictor.Predict(top, left, destination, stride, 2, mode, 8, mode == 1, scratch);
for (int y = 0; y < size; y++)
{
ReadOnlySpan<ushort> expectedRow = expected[caseIndex].AsSpan(y * size, size);
ReadOnlySpan<ushort> actualRow = destination.AsSpan(y * stride, size);
Assert.True(expectedRow.SequenceEqual(actualRow), $"Mode {mode}, row {y} did not match the fixed HEVC result.");
}
}
}
/// <summary>
/// Verifies the optional luma boundary filter for the pure horizontal and vertical modes.
/// </summary>
[Fact]
public void FiltersPureDirectionPredictionEdges()
{
ushort[] top = [64, 80, 96, 112, 128, 144, 160, 176, 192];
ushort[] left = [64, 70, 76, 82, 88, 94, 100, 106, 112];
ushort[] scratch = new ushort[HevcIntraPredictor.GetScratchLength(2)];
ushort[] horizontal = new ushort[16];
ushort[] vertical = new ushort[16];
HevcIntraPredictor.Predict(top, left, horizontal, 4, 2, 10, 8, true, scratch);
HevcIntraPredictor.Predict(top, left, vertical, 4, 2, 26, 8, true, scratch);
ushort[] expectedHorizontal = [78, 86, 94, 102, 76, 76, 76, 76, 82, 82, 82, 82, 88, 88, 88, 88];
ushort[] expectedVertical = [83, 96, 112, 128, 86, 96, 112, 128, 89, 96, 112, 128, 92, 96, 112, 128];
Assert.True(expectedHorizontal.AsSpan().SequenceEqual(horizontal));
Assert.True(expectedVertical.AsSpan().SequenceEqual(vertical));
}
/// <summary>
/// Verifies exact three-tap filtering, including the shared top-left sample.
/// </summary>
[Fact]
public void FiltersReferenceSamplesWithThreeTapKernel()
{
ushort[] top = [64, 80, 96, 112, 128, 144, 160, 176, 192];
ushort[] left = [64, 70, 76, 82, 88, 94, 100, 106, 112];
ushort[] filteredTop = new ushort[top.Length];
ushort[] filteredLeft = new ushort[left.Length];
HevcIntraPredictor.FilterReferenceSamples(top, left, filteredTop, filteredLeft, 2, 8, true);
ushort[] expectedTop = [70, 80, 96, 112, 128, 144, 160, 176, 192];
ushort[] expectedLeft = [70, 70, 76, 82, 88, 94, 100, 106, 112];
Assert.True(expectedTop.AsSpan().SequenceEqual(filteredTop));
Assert.True(expectedLeft.AsSpan().SequenceEqual(filteredLeft));
}
/// <summary>
/// Verifies that eligible thirty-two-sample references use strong bilinear smoothing rather than local three-tap filtering.
/// </summary>
[Fact]
public void StrongSmoothingReplacesEligibleNonlinearReferences()
{
const int size = 32;
ushort[] top = new ushort[(size * 2) + 1];
ushort[] left = new ushort[top.Length];
ushort[] filteredTop = new ushort[top.Length];
ushort[] filteredLeft = new ushort[left.Length];
for (int i = 0; i < top.Length; i++)
{
top[i] = (ushort)(100 + i + (i % 3));
left[i] = (ushort)(100 + (2 * i) + (i % 5));
}
// Strong smoothing is selected from the endpoint/midpoint test, so keep those six values exactly bilinear
// while the remaining samples deliberately differ from the expected straight lines.
top[0] = left[0] = 100;
top[size] = 132;
top[size * 2] = 164;
left[size] = 164;
left[size * 2] = 228;
HevcIntraPredictor.FilterReferenceSamples(top, left, filteredTop, filteredLeft, 5, 10, true);
for (int i = 0; i < top.Length; i++)
{
Assert.Equal((ushort)(100 + i), filteredTop[i]);
Assert.Equal((ushort)(100 + (2 * i)), filteredLeft[i]);
}
}
/// <summary>
/// Compares every prediction mode and block width with a specification-shaped scalar oracle.
/// </summary>
/// <param name="log2Size">The base-two logarithm of the tested block side.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
[Theory]
[InlineData(2, 8)]
[InlineData(3, 10)]
[InlineData(4, 12)]
[InlineData(5, 12)]
public void EveryModeMatchesScalarOracle(int log2Size, int bitDepth)
{
int size = 1 << log2Size;
int maximum = (1 << bitDepth) - 1;
int referenceLength = (size * 2) + 1;
ushort[] top = new ushort[referenceLength];
ushort[] left = new ushort[referenceLength];
top[0] = left[0] = (ushort)(maximum / 3);
for (int i = 1; i < referenceLength; i++)
{
top[i] = (ushort)((top[0] + (37 * i) + (3 * size)) & maximum);
left[i] = (ushort)((left[0] + (53 * i) + (5 * size)) & maximum);
}
int stride = size + 3;
ushort[] expected = new ushort[stride * size];
ushort[] actual = new ushort[stride * size];
ushort[] scratch = new ushort[HevcIntraPredictor.GetScratchLength(log2Size)];
for (int mode = 0; mode <= 34; mode++)
{
expected.AsSpan().Clear();
actual.AsSpan().Clear();
PredictScalar(top, left, expected, stride, size, mode, bitDepth, true);
HevcIntraPredictor.Predict(top, left, actual, stride, log2Size, mode, bitDepth, true, scratch);
Assert.True(expected.AsSpan().SequenceEqual(actual), $"Mode {mode}, size {size}, and bit depth {bitDepth} did not match the scalar oracle.");
}
}
/// <summary>
/// Reconstructs one block directly from the HEVC planar, DC, and angular prediction equations.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference samples.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="size">The square block side.</param>
/// <param name="mode">The prediction mode.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="filterPredictionEdges">Whether the luma edge filter applies.</param>
private static void PredictScalar(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int mode,
int bitDepth,
bool filterPredictionEdges)
{
if (mode == 0)
{
int shift = BitOperations.Log2((uint)size) + 1;
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
int horizontal = ((size - x - 1) * left[y + 1]) + ((x + 1) * top[size]);
int vertical = ((size - y - 1) * top[x + 1]) + ((y + 1) * left[size]);
destination[(y * destinationStride) + x] = (ushort)((horizontal + vertical + size) >> shift);
}
}
return;
}
if (mode == 1)
{
PredictDcScalar(top, left, destination, destinationStride, size, filterPredictionEdges);
return;
}
if (mode == 10)
{
PredictHorizontalScalar(top, left, destination, destinationStride, size, bitDepth, filterPredictionEdges);
return;
}
if (mode == 26)
{
PredictVerticalScalar(top, left, destination, destinationStride, size, bitDepth, filterPredictionEdges);
return;
}
PredictAngularScalar(top, left, destination, destinationStride, size, mode);
}
/// <summary>
/// Reconstructs a scalar DC block and its optional boundary filter.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference samples.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="size">The square block side.</param>
/// <param name="filterPredictionEdges">Whether the luma edge filter applies.</param>
private static void PredictDcScalar(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
bool filterPredictionEdges)
{
int sum = 0;
for (int i = 1; i <= size; i++)
{
sum += top[i] + left[i];
}
ushort dc = (ushort)((sum + size) >> (BitOperations.Log2((uint)size) + 1));
for (int y = 0; y < size; y++)
{
destination.Slice(y * destinationStride, size).Fill(dc);
}
if (!filterPredictionEdges)
{
return;
}
destination[0] = (ushort)((top[1] + left[1] + (2 * dc) + 2) >> 2);
for (int i = 1; i < size; i++)
{
destination[i] = (ushort)((top[i + 1] + (3 * dc) + 2) >> 2);
destination[i * destinationStride] = (ushort)((left[i + 1] + (3 * dc) + 2) >> 2);
}
}
/// <summary>
/// Reconstructs scalar horizontal prediction and its optional boundary filter.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference samples.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="size">The square block side.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="filterPredictionEdges">Whether the luma edge filter applies.</param>
private static void PredictHorizontalScalar(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int bitDepth,
bool filterPredictionEdges)
{
for (int y = 0; y < size; y++)
{
destination.Slice(y * destinationStride, size).Fill(left[y + 1]);
}
if (filterPredictionEdges)
{
int maximum = (1 << bitDepth) - 1;
for (int x = 0; x < size; x++)
{
destination[x] = (ushort)Math.Clamp(destination[x] + ((top[x + 1] - top[0]) >> 1), 0, maximum);
}
}
}
/// <summary>
/// Reconstructs scalar vertical prediction and its optional boundary filter.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference samples.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="size">The square block side.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="filterPredictionEdges">Whether the luma edge filter applies.</param>
private static void PredictVerticalScalar(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int bitDepth,
bool filterPredictionEdges)
{
int maximum = (1 << bitDepth) - 1;
for (int y = 0; y < size; y++)
{
top.Slice(1, size).CopyTo(destination[(y * destinationStride)..]);
if (filterPredictionEdges)
{
int offset = y * destinationStride;
destination[offset] = (ushort)Math.Clamp(destination[offset] + ((left[y + 1] - left[0]) >> 1), 0, maximum);
}
}
}
/// <summary>
/// Reconstructs a scalar angular block, including negative-reference extension and horizontal transposition.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference samples.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="size">The square block side.</param>
/// <param name="mode">The angular prediction mode.</param>
private static void PredictAngularScalar(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int mode)
{
ReadOnlySpan<int> angles = [0, 2, 5, 9, 13, 17, 21, 26, 32];
ReadOnlySpan<int> inverseAngles = [0, 4096, 1638, 910, 630, 482, 390, 315, 256];
bool vertical = mode >= 18;
int angleMode = vertical ? mode - 26 : 10 - mode;
int absoluteAngleMode = Math.Abs(angleMode);
int angle = angles[absoluteAngleMode] * Math.Sign(angleMode);
ReadOnlySpan<ushort> main = vertical ? top : left;
ReadOnlySpan<ushort> side = vertical ? left : top;
int mainOrigin = size * 2;
int[] extendedMain = new int[(4 * size) + 1];
for (int i = 0; i < main.Length; i++)
{
extendedMain[mainOrigin + i] = main[i];
}
if (angle < 0)
{
int inverseAngleSum = 128;
for (int index = -1; index > ((size * angle) >> 5); index--)
{
inverseAngleSum += inverseAngles[absoluteAngleMode];
extendedMain[mainOrigin + index] = side[inverseAngleSum >> 8];
}
}
ushort[] temporary = new ushort[size * size];
for (int y = 0, deltaPosition = angle; y < size; y++, deltaPosition += angle)
{
int deltaInteger = deltaPosition >> 5;
int deltaFraction = deltaPosition & 31;
for (int x = 0; x < size; x++)
{
int index = mainOrigin + x + deltaInteger + 1;
temporary[(y * size) + x] = deltaFraction == 0
? (ushort)extendedMain[index]
: (ushort)(((extendedMain[index] * (32 - deltaFraction)) + (extendedMain[index + 1] * deltaFraction) + 16) >> 5);
}
}
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
int sourceIndex = vertical ? (y * size) + x : (x * size) + y;
destination[(y * destinationStride) + x] = temporary[sourceIndex];
}
}
}
}
Loading…
Cancel
Save