diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md
index 2bcc74685f..56a7cc7e9e 100644
--- a/HEIF_IMPLEMENTATION_PLAN.md
+++ b/HEIF_IMPLEMENTATION_PLAN.md
@@ -58,7 +58,7 @@ This snapshot pins or classifies the available references and failures; it does
| --- | --- | --- | --- |
| `Av1YuvConverter.ConvertToRgb`, `ConvertFromRgb`, scalar row conversion, and chroma reconstruction | H.273 formulas 20-31 and the identity, YCgCo, and non-constant-luminance matrix formulas; AV1 section 6.4.2 chroma sample positions | libavif `src/reformat.c` and `src/colr.c` at `092276ce89098ead06db80975173191e5fee1826`; libaom `aom/aom_image.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar behavioral oracle for 8-bit full/limited-range conversion. Decode covers monochrome, YUV 4:2:0, 4:2:2, and 4:4:4 with AV1 chroma sample positioning; encode remains YUV 4:4:4 at this snapshot. Later high-bit-depth and SIMD paths must match it. |
| `Av1FrameBuffer` high-bit-depth sample layout and `Av1YuvConverter` 10/12-bit output conversion | AV1 section 6.4.1 bit depth and H.273 sample-range scaling | libaom `aom_scale/yv12config.h`, `av1/common/idct.c`, and `av1/common/reconintra.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f`; libavif `src/avif.c` and `src/reformat.c` at `092276ce89098ead06db80975173191e5fee1826` | Establish two-byte native sample storage with sample-unit strides for 10/12-bit reconstruction and use the same scalar color model at every supported bit depth. |
-| `Av1PredictionDecoder` and the scalar DC, directional, Paeth, smooth, and filter-intra predictors | AV1 sections 7.11.2 and 7.11.2.3 intra prediction | libaom `aom_dsp/intrapred.c` and `av1/common/reconintra.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Behavioral oracle for neighbor addressing, directional upsampling, Paeth selection, smooth normalization, filter-intra taps, and chroma-from-luma row strides. Existing managed scalar predictors remain the implementation base. The WIP rectangular smooth digest expectations encode width/height-swapped weights and must be replaced only from an independently generated oracle, not regenerated from this implementation. |
+| `Av1PredictionDecoder`, `Av1HighBitDepthPredictor`, and the scalar DC, directional, Paeth, smooth, and filter-intra predictors | AV1 sections 7.11.2 and 7.11.2.3 intra prediction | libaom `aom_dsp/intrapred.c` and `av1/common/reconintra.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Behavioral oracle for neighbor addressing, directional upsampling, Paeth selection, smooth normalization, filter-intra taps, high-bit-depth clipping, and chroma-from-luma row strides. Existing managed scalar tables and predictors remain the implementation base. The WIP rectangular byte-pipeline smooth digest expectations encode width/height-swapped weights and must be replaced only from an independently generated oracle, not regenerated from this implementation. |
| `Av1Inverse2dTransformer` and `Av1InverseTransformerFactory` | AV1 section 7.11.2 inverse transform and reconstruction | libaom `av1/common/av1_inv_txfm1d.c`, `av1/common/av1_inv_txfm2d.c`, and `av1/common/idct.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar transform oracle for coefficient-row traversal, intermediate layout, stage ranges, clipping, and high-bit-depth sample addition. The managed 16-bit overload is also used as a parity oracle for the byte overload. |
This table is intentionally incomplete. Add a row before each additional AV1 or HEVC algorithm is ported or materially reshaped.
diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.cs
index 243d0e822c..c07d823acb 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.cs
@@ -18,7 +18,7 @@ internal static class Av1FilterIntraPredictor
// AV1 7.11.2.3 defines five sets of eight filters over the same seven
// already-reconstructed neighbors. The omitted eighth libaom tap is zero.
- private static readonly sbyte[] Taps =
+ internal static readonly sbyte[] Taps =
[
// DC
diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1HighBitDepthPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1HighBitDepthPredictor.cs
new file mode 100644
index 0000000000..e2364d040f
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1HighBitDepthPredictor.cs
@@ -0,0 +1,423 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
+
+///
+/// Implements AV1 intra prediction for 10-bit and 12-bit sample buffers.
+///
+internal static class Av1HighBitDepthPredictor
+{
+ private const int FilterBufferStride = 33;
+ private const int FilterBufferLength = FilterBufferStride * FilterBufferStride;
+ private const int FilterTapsPerPixel = 7;
+ private const int FilterPixelsPerGroup = 8;
+ private const int FilterTapsPerMode = FilterTapsPerPixel * FilterPixelsPerGroup;
+
+ internal static void DcPredictor(
+ bool hasLeft,
+ bool hasAbove,
+ Av1TransformSize transformSize,
+ Span destination,
+ nuint destinationStride,
+ Span above,
+ Span left,
+ int bitDepth)
+ {
+ int width = transformSize.GetWidth();
+ int height = transformSize.GetHeight();
+ int sum = 0;
+ int count = 0;
+
+ if (hasAbove)
+ {
+ for (int column = 0; column < width; column++)
+ {
+ sum += above[column];
+ }
+
+ count += width;
+ }
+
+ if (hasLeft)
+ {
+ for (int row = 0; row < height; row++)
+ {
+ sum += left[row];
+ }
+
+ count += height;
+ }
+
+ short prediction = count == 0
+ ? (short)(1 << (bitDepth - 1))
+ : (short)((sum + (count >> 1)) / count);
+
+ Fill(destination, destinationStride, width, height, prediction);
+ }
+
+ internal static void GeneralPredictor(
+ Av1PredictionMode mode,
+ Av1TransformSize transformSize,
+ Span destination,
+ nuint destinationStride,
+ Span above,
+ Span left)
+ {
+ int width = transformSize.GetWidth();
+ int height = transformSize.GetHeight();
+
+ switch (mode)
+ {
+ case Av1PredictionMode.Horizontal:
+ PredictHorizontal(destination, destinationStride, left, width, height);
+ break;
+ case Av1PredictionMode.Vertical:
+ PredictVertical(destination, destinationStride, above, width, height);
+ break;
+ case Av1PredictionMode.Paeth:
+ PredictPaeth(destination, destinationStride, above, left, width, height);
+ break;
+ case Av1PredictionMode.Smooth:
+ PredictSmooth(destination, destinationStride, above, left, width, height);
+ break;
+ case Av1PredictionMode.SmoothHorizontal:
+ PredictSmoothHorizontal(destination, destinationStride, above, left, width, height);
+ break;
+ case Av1PredictionMode.SmoothVertical:
+ PredictSmoothVertical(destination, destinationStride, above, left, width, height);
+ break;
+ }
+ }
+
+ internal static void DirectionalPredictor(
+ Span destination,
+ nuint destinationStride,
+ Av1TransformSize transformSize,
+ Span above,
+ Span left,
+ bool upsampleAbove,
+ bool upsampleLeft,
+ int angle,
+ int bitDepth)
+ {
+ Guard.MustBeBetweenOrEqualTo(angle, 1, 269, nameof(angle));
+ int dx = Av1PredictorFactory.GetDeltaX(angle);
+ int dy = Av1PredictorFactory.GetDeltaY(angle);
+ int width = transformSize.GetWidth();
+ int height = transformSize.GetHeight();
+
+ if (angle is > 0 and < 90)
+ {
+ PredictDirectionalZone1(destination, destinationStride, above, upsampleAbove, dx, width, height, bitDepth);
+ }
+ else if (angle is > 90 and < 180)
+ {
+ PredictDirectionalZone2(destination, destinationStride, above, left, upsampleAbove, upsampleLeft, dx, dy, width, height, bitDepth);
+ }
+ else if (angle is > 180 and < 270)
+ {
+ PredictDirectionalZone3(destination, destinationStride, left, upsampleLeft, dx, dy, width, height, bitDepth);
+ }
+ else if (angle == 90)
+ {
+ PredictVertical(destination, destinationStride, above, width, height);
+ }
+ else if (angle == 180)
+ {
+ PredictHorizontal(destination, destinationStride, left, width, height);
+ }
+ }
+
+ internal static void FilterIntraPredictor(
+ Span destination,
+ nuint destinationStride,
+ Av1TransformSize transformSize,
+ Span above,
+ Span left,
+ Av1FilterIntraMode mode,
+ int bitDepth)
+ {
+ int width = transformSize.GetWidth();
+ int height = transformSize.GetHeight();
+ int maximum = (1 << bitDepth) - 1;
+ DebugGuard.MustBeLessThanOrEqualTo(width, 32, nameof(width));
+ DebugGuard.MustBeLessThanOrEqualTo(height, 32, nameof(height));
+ Guard.MustBeGreaterThanOrEqualTo(destinationStride, (nuint)width, nameof(destinationStride));
+ Guard.MustBeSizedAtLeast(destination, (int)destinationStride * height, nameof(destination));
+ Guard.MustBeSizedAtLeast(above, width, nameof(above));
+ Guard.MustBeSizedAtLeast(left, height, nameof(left));
+
+ Span buffer = stackalloc short[FilterBufferLength];
+ ref short bufferRef = ref buffer[0];
+ ref short aboveRef = ref above[0];
+ ref short leftRef = ref left[0];
+
+ // Row zero includes the top-left sample followed by the top neighbors.
+ bufferRef = Unsafe.Subtract(ref aboveRef, 1);
+ above[..width].CopyTo(buffer[1..]);
+ for (int row = 0; row < height; row++)
+ {
+ Unsafe.Add(ref bufferRef, (row + 1) * FilterBufferStride) = Unsafe.Add(ref leftRef, row);
+ }
+
+ int modeOffset = (int)mode * FilterTapsPerMode;
+ ref sbyte tapsRef = ref Av1FilterIntraPredictor.Taps[modeOffset];
+ for (int row = 1; row <= height; row += 2)
+ {
+ for (int column = 1; column <= width; column += 4)
+ {
+ int sourceOffset = ((row - 1) * FilterBufferStride) + column - 1;
+ int p0 = Unsafe.Add(ref bufferRef, sourceOffset);
+ int p1 = Unsafe.Add(ref bufferRef, sourceOffset + 1);
+ int p2 = Unsafe.Add(ref bufferRef, sourceOffset + 2);
+ int p3 = Unsafe.Add(ref bufferRef, sourceOffset + 3);
+ int p4 = Unsafe.Add(ref bufferRef, sourceOffset + 4);
+ int p5 = Unsafe.Add(ref bufferRef, sourceOffset + FilterBufferStride);
+ int p6 = Unsafe.Add(ref bufferRef, sourceOffset + (2 * FilterBufferStride));
+
+ for (int pixel = 0; pixel < FilterPixelsPerGroup; pixel++)
+ {
+ int tapOffset = pixel * FilterTapsPerPixel;
+ int prediction =
+ (Unsafe.Add(ref tapsRef, tapOffset) * p0)
+ + (Unsafe.Add(ref tapsRef, tapOffset + 1) * p1)
+ + (Unsafe.Add(ref tapsRef, tapOffset + 2) * p2)
+ + (Unsafe.Add(ref tapsRef, tapOffset + 3) * p3)
+ + (Unsafe.Add(ref tapsRef, tapOffset + 4) * p4)
+ + (Unsafe.Add(ref tapsRef, tapOffset + 5) * p5)
+ + (Unsafe.Add(ref tapsRef, tapOffset + 6) * p6);
+
+ int rowOffset = pixel >> 2;
+ int columnOffset = pixel & 3;
+ int destinationOffset = ((row + rowOffset) * FilterBufferStride) + column + columnOffset;
+ Unsafe.Add(ref bufferRef, destinationOffset) =
+ (short)Av1Math.Clamp(Av1Math.RoundPowerOf2(prediction, 4), 0, maximum);
+ }
+ }
+ }
+
+ for (int row = 0; row < height; row++)
+ {
+ buffer.Slice(((row + 1) * FilterBufferStride) + 1, width).CopyTo(
+ destination.Slice(row * (int)destinationStride, width));
+ }
+ }
+
+ private static void PredictHorizontal(Span destination, nuint stride, Span left, int width, int height)
+ {
+ for (int row = 0; row < height; row++)
+ {
+ destination.Slice(row * (int)stride, width).Fill(left[row]);
+ }
+ }
+
+ private static void PredictVertical(Span destination, nuint stride, Span above, int width, int height)
+ {
+ for (int row = 0; row < height; row++)
+ {
+ above[..width].CopyTo(destination.Slice(row * (int)stride, width));
+ }
+ }
+
+ private static void PredictPaeth(Span destination, nuint stride, Span above, Span left, int width, int height)
+ {
+ int topLeft = Unsafe.Subtract(ref above[0], 1);
+ for (int row = 0; row < height; row++)
+ {
+ Span destinationRow = destination.Slice(row * (int)stride, width);
+ for (int column = 0; column < width; column++)
+ {
+ short leftValue = left[row];
+ short topValue = above[column];
+ int basis = topValue + leftValue - topLeft;
+ int leftDistance = Av1Math.AbsoluteDifference(basis, leftValue);
+ int topDistance = Av1Math.AbsoluteDifference(basis, topValue);
+ int topLeftDistance = Av1Math.AbsoluteDifference(basis, topLeft);
+ destinationRow[column] = leftDistance <= topDistance && leftDistance <= topLeftDistance
+ ? leftValue
+ : topDistance <= topLeftDistance ? topValue : (short)topLeft;
+ }
+ }
+ }
+
+ private static void PredictSmooth(Span destination, nuint stride, Span above, Span left, int width, int height)
+ {
+ int below = left[height - 1];
+ int right = above[width - 1];
+ Span widthWeights = Av1SmoothPredictor.Weights.AsSpan(width, width);
+ Span heightWeights = Av1SmoothPredictor.Weights.AsSpan(height, height);
+ int scale = 1 << Av1SmoothPredictor.WeightLog2Scale;
+ int log2Scale = Av1SmoothPredictor.WeightLog2Scale + 1;
+
+ for (int row = 0; row < height; row++)
+ {
+ int rowWeight = heightWeights[row];
+ Span destinationRow = destination.Slice(row * (int)stride, width);
+ for (int column = 0; column < width; column++)
+ {
+ int columnWeight = widthWeights[column];
+ int prediction = (above[column] * rowWeight) + (below * (scale - rowWeight));
+ prediction += (left[row] * columnWeight) + (right * (scale - columnWeight));
+ destinationRow[column] = (short)Av1Math.DivideRound(prediction, log2Scale);
+ }
+ }
+ }
+
+ private static void PredictSmoothHorizontal(Span destination, nuint stride, Span above, Span left, int width, int height)
+ {
+ int right = above[width - 1];
+ Span weights = Av1SmoothPredictor.Weights.AsSpan(width, width);
+ int scale = 1 << Av1SmoothPredictor.WeightLog2Scale;
+
+ for (int row = 0; row < height; row++)
+ {
+ Span destinationRow = destination.Slice(row * (int)stride, width);
+ for (int column = 0; column < width; column++)
+ {
+ int weight = weights[column];
+ int prediction = (left[row] * weight) + (right * (scale - weight));
+ destinationRow[column] = (short)Av1Math.DivideRound(prediction, Av1SmoothPredictor.WeightLog2Scale);
+ }
+ }
+ }
+
+ private static void PredictSmoothVertical(Span destination, nuint stride, Span above, Span left, int width, int height)
+ {
+ int below = left[height - 1];
+ Span weights = Av1SmoothPredictor.Weights.AsSpan(height, height);
+ int scale = 1 << Av1SmoothPredictor.WeightLog2Scale;
+
+ for (int row = 0; row < height; row++)
+ {
+ int weight = weights[row];
+ Span destinationRow = destination.Slice(row * (int)stride, width);
+ for (int column = 0; column < width; column++)
+ {
+ int prediction = (above[column] * weight) + (below * (scale - weight));
+ destinationRow[column] = (short)Av1Math.DivideRound(prediction, Av1SmoothPredictor.WeightLog2Scale);
+ }
+ }
+ }
+
+ private static void PredictDirectionalZone1(Span destination, nuint stride, Span above, bool upsample, int dx, int width, int height, int bitDepth)
+ {
+ int upsampleAbove = upsample ? 1 : 0;
+ int maxBasisX = (width + height - 1) << upsampleAbove;
+ int fractionBitCount = 6 - upsampleAbove;
+ int basisIncrement = 1 << upsampleAbove;
+ int maximum = (1 << bitDepth) - 1;
+ int x = dx;
+ ref short aboveRef = ref above[0];
+
+ for (int row = 0; row < height; row++)
+ {
+ Span destinationRow = destination.Slice(row * (int)stride, width);
+ int basis = x >> fractionBitCount;
+ int shift = ((x << upsampleAbove) & 0x3F) >> 1;
+ for (int column = 0; column < width; column++)
+ {
+ if (basis < maxBasisX)
+ {
+ int prediction = (Unsafe.Add(ref aboveRef, basis) * (32 - shift)) + (Unsafe.Add(ref aboveRef, basis + 1) * shift);
+ destinationRow[column] = (short)Av1Math.Clamp(Av1Math.RoundPowerOf2(prediction, 5), 0, maximum);
+ }
+ else
+ {
+ destinationRow[column] = Unsafe.Add(ref aboveRef, maxBasisX);
+ }
+
+ basis += basisIncrement;
+ }
+
+ x += dx;
+ }
+ }
+
+ private static void PredictDirectionalZone2(Span destination, nuint stride, Span above, Span left, bool doUpsampleAbove, bool doUpsampleLeft, int dx, int dy, int width, int height, int bitDepth)
+ {
+ int upsampleAbove = doUpsampleAbove ? 1 : 0;
+ int upsampleLeft = doUpsampleLeft ? 1 : 0;
+ int minBasisX = -(1 << upsampleAbove);
+ int fractionBitCountX = 6 - upsampleAbove;
+ int fractionBitCountY = 6 - upsampleLeft;
+ int basisIncrementX = 1 << upsampleAbove;
+ int maximum = (1 << bitDepth) - 1;
+ int x = -dx;
+ ref short aboveRef = ref above[0];
+ ref short leftRef = ref left[0];
+
+ for (int row = 0; row < height; row++)
+ {
+ Span destinationRow = destination.Slice(row * (int)stride, width);
+ int basisX = x >> fractionBitCountX;
+ int y = (row << 6) - dy;
+ for (int column = 0; column < width; column++, basisX += basisIncrementX, y -= dy)
+ {
+ int prediction;
+ if (basisX >= minBasisX)
+ {
+ int shift = ((x * (1 << upsampleAbove)) & 0x3F) >> 1;
+ prediction = (Unsafe.Add(ref aboveRef, basisX) * (32 - shift)) + (Unsafe.Add(ref aboveRef, basisX + 1) * shift);
+ }
+ else
+ {
+ int basisY = y >> fractionBitCountY;
+ int shift = ((y * (1 << upsampleLeft)) & 0x3F) >> 1;
+ prediction = (Unsafe.Add(ref leftRef, basisY) * (32 - shift)) + (Unsafe.Add(ref leftRef, basisY + 1) * shift);
+ }
+
+ destinationRow[column] = (short)Av1Math.Clamp(Av1Math.RoundPowerOf2(prediction, 5), 0, maximum);
+ }
+
+ x -= dx;
+ }
+ }
+
+ private static void PredictDirectionalZone3(Span destination, nuint stride, Span left, bool upsample, int dx, int dy, int width, int height, int bitDepth)
+ {
+ int upsampleLeft = upsample ? 1 : 0;
+ int maxBasisY = (width + height - 1) << upsampleLeft;
+ int fractionBitCount = 6 - upsampleLeft;
+ int basisIncrement = 1 << upsampleLeft;
+ int maximum = (1 << bitDepth) - 1;
+ int y = dy;
+ ref short leftRef = ref left[0];
+ Guard.IsTrue(dx == 1, nameof(dx), "Dx expected to be always equal to 1 for directional Zone 3 prediction.");
+
+ for (int column = 0; column < width; column++)
+ {
+ int basis = y >> fractionBitCount;
+ int shift = ((y << upsampleLeft) & 0x3F) >> 1;
+ for (int row = 0; row < height; row++)
+ {
+ int destinationOffset = (row * (int)stride) + column;
+ if (basis < maxBasisY)
+ {
+ int prediction = (Unsafe.Add(ref leftRef, basis) * (32 - shift)) + (Unsafe.Add(ref leftRef, basis + 1) * shift);
+ destination[destinationOffset] = (short)Av1Math.Clamp(Av1Math.RoundPowerOf2(prediction, 5), 0, maximum);
+ }
+ else
+ {
+ destination[destinationOffset] = Unsafe.Add(ref leftRef, maxBasisY);
+ }
+
+ basis += basisIncrement;
+ }
+
+ y += dy;
+ }
+ }
+
+ private static void Fill(Span destination, nuint stride, int width, int height, short value)
+ {
+ for (int row = 0; row < height; row++)
+ {
+ destination.Slice(row * (int)stride, width).Fill(value);
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictorFactory.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictorFactory.cs
index c4c0d97d9c..496a0dbb95 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictorFactory.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictorFactory.cs
@@ -69,6 +69,9 @@ internal class Av1PredictorFactory
}
}
+ internal static void DcPredictor(bool hasLeft, bool hasAbove, Av1TransformSize transformSize, Span destination, nuint destinationStride, Span aboveRow, Span leftColumn, int bitDepth)
+ => Av1HighBitDepthPredictor.DcPredictor(hasLeft, hasAbove, transformSize, destination, destinationStride, aboveRow, leftColumn, bitDepth);
+
///
/// SVT: svt_aom_highbd_dr_predictor
///
@@ -102,9 +105,15 @@ internal class Av1PredictorFactory
}
}
+ internal static void DirectionalPredictor(Span destination, nuint stride, Av1TransformSize transformSize, Span aboveRow, Span leftColumn, bool upsampleAbove, bool upsampleLeft, int angle, int bitDepth)
+ => Av1HighBitDepthPredictor.DirectionalPredictor(destination, stride, transformSize, aboveRow, leftColumn, upsampleAbove, upsampleLeft, angle, bitDepth);
+
internal static void FilterIntraPredictor(Span destination, nuint destinationStride, Av1TransformSize transformSize, Span aboveRow, Span leftColumn, Av1FilterIntraMode filterIntraMode)
=> Av1FilterIntraPredictor.Predict(destination, destinationStride, transformSize, aboveRow, leftColumn, filterIntraMode);
+ internal static void FilterIntraPredictor(Span destination, nuint destinationStride, Av1TransformSize transformSize, Span aboveRow, Span leftColumn, Av1FilterIntraMode filterIntraMode, int bitDepth)
+ => Av1HighBitDepthPredictor.FilterIntraPredictor(destination, destinationStride, transformSize, aboveRow, leftColumn, filterIntraMode, bitDepth);
+
internal static void GeneralPredictor(Av1PredictionMode mode, Av1TransformSize transformSize, Span destination, nuint destinationStride, Span aboveRow, Span leftColumn)
{
switch (mode)
@@ -130,11 +139,14 @@ internal class Av1PredictorFactory
}
}
+ internal static void GeneralPredictor(Av1PredictionMode mode, Av1TransformSize transformSize, Span destination, nuint destinationStride, Span aboveRow, Span leftColumn)
+ => Av1HighBitDepthPredictor.GeneralPredictor(mode, transformSize, destination, destinationStride, aboveRow, leftColumn);
+
// Get the shift (up-scaled by 256) in Y w.r.t a unit change in X.
// If angle > 0 && angle < 90, dy = 1;
// If angle > 90 && angle < 180, dy = (int32_t)(256 * t);
// If angle > 180 && angle < 270, dy = -((int32_t)(256 * t));
- private static int GetDeltaY(int angle)
+ internal static int GetDeltaY(int angle)
{
if (angle is > 90 and < 180)
{
@@ -155,7 +167,7 @@ internal class Av1PredictorFactory
// If angle > 0 && angle < 90, dx = -((int32_t)(256 / t));
// If angle > 90 && angle < 180, dx = (int32_t)(256 / t);
// If angle > 180 && angle < 270, dx = 1;
- private static int GetDeltaX(int angle)
+ internal static int GetDeltaX(int angle)
{
if (angle is > 0 and < 90)
{
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs
index 186b47981a..f69656654e 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs
@@ -413,6 +413,137 @@ public class Av1PredictorTests
Assert.All(destination, value => Assert.Equal(73, value));
}
+ [Theory]
+ [InlineData(true, true, 800)]
+ [InlineData(true, false, 700)]
+ [InlineData(false, true, 900)]
+ [InlineData(false, false, 512)]
+ public void HighBitDepthDcPredictionUsesAvailableEdges(bool hasLeft, bool hasAbove, short expected)
+ {
+ short[] destination = new short[16];
+ short[] above = [900, 900, 900, 900];
+ short[] left = [700, 700, 700, 700];
+
+ Av1PredictorFactory.DcPredictor(
+ hasLeft,
+ hasAbove,
+ Av1TransformSize.Size4x4,
+ destination,
+ 4,
+ above,
+ left,
+ 10);
+
+ Assert.All(destination, value => Assert.Equal(expected, value));
+ }
+
+ [Theory]
+ [InlineData((int)Av1PredictionMode.Horizontal)]
+ [InlineData((int)Av1PredictionMode.Vertical)]
+ [InlineData((int)Av1PredictionMode.Paeth)]
+ [InlineData((int)Av1PredictionMode.Smooth)]
+ [InlineData((int)Av1PredictionMode.SmoothHorizontal)]
+ [InlineData((int)Av1PredictionMode.SmoothVertical)]
+ public void HighBitDepthGeneralPredictionMatchesTranslatedEightBitOracle(int modeIndex)
+ {
+ const int offset = 512;
+ Av1PredictionMode mode = (Av1PredictionMode)modeIndex;
+ byte[] aboveData = [50, 60, 10, 90, 40];
+ byte[] left = [20, 80, 30, 100];
+ short[] highAboveData = aboveData.Select(value => (short)(value + offset)).ToArray();
+ short[] highLeft = left.Select(value => (short)(value + offset)).ToArray();
+ byte[] expected = new byte[16];
+ short[] actual = new short[16];
+
+ Av1PredictorFactory.GeneralPredictor(
+ mode,
+ Av1TransformSize.Size4x4,
+ expected,
+ 4,
+ aboveData.AsSpan(1),
+ left);
+
+ Av1PredictorFactory.GeneralPredictor(
+ mode,
+ Av1TransformSize.Size4x4,
+ actual,
+ 4,
+ highAboveData.AsSpan(1),
+ highLeft);
+
+ Assert.Equal(expected.Select(value => (short)(value + offset)), actual);
+ }
+
+ [Theory]
+ [InlineData(45)]
+ [InlineData(67)]
+ [InlineData(90)]
+ [InlineData(113)]
+ [InlineData(135)]
+ [InlineData(157)]
+ [InlineData(180)]
+ [InlineData(203)]
+ public void HighBitDepthDirectionalPredictionMatchesTranslatedEightBitOracle(int angle)
+ {
+ const int offset = 512;
+ byte[] aboveData = new byte[256];
+ byte[] leftData = new byte[256];
+ for (int i = 0; i < aboveData.Length; i++)
+ {
+ aboveData[i] = (byte)(20 + (i % 180));
+ leftData[i] = (byte)(30 + (i % 170));
+ }
+
+ short[] highAboveData = aboveData.Select(value => (short)(value + offset)).ToArray();
+ short[] highLeftData = leftData.Select(value => (short)(value + offset)).ToArray();
+ byte[] expected = new byte[16];
+ short[] actual = new short[16];
+
+ Av1PredictorFactory.DirectionalPredictor(
+ expected,
+ 4,
+ Av1TransformSize.Size4x4,
+ aboveData.AsSpan(128),
+ leftData.AsSpan(128),
+ false,
+ false,
+ angle);
+
+ Av1PredictorFactory.DirectionalPredictor(
+ actual,
+ 4,
+ Av1TransformSize.Size4x4,
+ highAboveData.AsSpan(128),
+ highLeftData.AsSpan(128),
+ false,
+ false,
+ angle,
+ 10);
+
+ Assert.Equal(expected.Select(value => (short)(value + offset)), actual);
+ }
+
+ [Theory]
+ [MemberData(nameof(GetFilterIntraPredictions))]
+ public void HighBitDepthFilterIntraMatchesTranslatedLibaomVector(int mode, byte[] expected)
+ {
+ const int offset = 512;
+ short[] destination = new short[16];
+ short[] aboveData = [529, 542, 582, 622, 662];
+ short[] left = [552, 592, 632, 672];
+
+ Av1PredictorFactory.FilterIntraPredictor(
+ destination,
+ 4,
+ Av1TransformSize.Size4x4,
+ aboveData.AsSpan(1),
+ left,
+ (Av1FilterIntraMode)mode,
+ 10);
+
+ Assert.Equal(expected.Select(value => (short)(value + offset)), destination);
+ }
+
private static void AssertValue(byte expected, byte actual)
{
Assert.NotEqual(0, actual);