Browse Source

Add live AV1 filter-intra search

pull/2633/head
James Jackson-South 1 month ago
parent
commit
97f7d892ab
  1. 9
      HEIF_IMPLEMENTATION_PLAN.md
  2. 2
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs
  3. 136
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs
  4. 206
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.Operator.cs
  5. 169
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs
  6. 251
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs

9
HEIF_IMPLEMENTATION_PLAN.md

File diff suppressed because one or more lines are too long

2
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs

@ -57,7 +57,7 @@ internal static class Av1FrameEncoder
Use128x128Superblock = false,
ForceScreenContentTools = 2,
ForceIntegerMotionVector = 2,
EnableFilterIntra = false,
EnableFilterIntra = true,
EnableIntraEdgeFilter = false,
EnableSuperResolution = false,
EnableCdef = false,

136
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs

@ -192,9 +192,11 @@ internal static partial class Av1IntraSuperblockEncoder
tileIndex,
lumaCoefficients[this.codedAreaLuma..],
ref lumaState,
out int lumaAngleDelta);
out int lumaAngleDelta,
out Av1FilterIntraMode filterIntraMode);
block.PredictionUnit.AngleDelta[(int)Av1PlaneType.Y] = (sbyte)lumaAngleDelta;
block.FilterIntraMode = filterIntraMode;
this.codedAreaLuma += LumaTransformSize.GetSize2d();
bool skipTransform = lumaState.EndOfBlock == 0;
@ -257,7 +259,8 @@ internal static partial class Av1IntraSuperblockEncoder
ushort tileIndex,
Span<int> retainedCoefficients,
ref Av1EncoderTransformBlockState retainedState,
out int selectedAngleDelta)
out int selectedAngleDelta,
out Av1FilterIntraMode selectedFilterIntraMode)
{
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
const Av1TransformSize TransformSize = Av1TransformSize.Size8x8;
@ -376,6 +379,7 @@ internal static partial class Av1IntraSuperblockEncoder
long bestCost = long.MaxValue;
Av1PredictionMode bestMode = Av1PredictionMode.DC;
selectedAngleDelta = 0;
selectedFilterIntraMode = Av1FilterIntraMode.AllFilterIntraModes;
int baseModeCount = LumaModeSearchOrder.Length;
int deltaCount = AngleDeltaSearchOrder.Length;
int directionalModeCount = (int)Av1PredictionMode.Directional67Degrees - (int)Av1PredictionMode.Vertical + 1;
@ -490,6 +494,74 @@ internal static partial class Av1IntraSuperblockEncoder
}
}
if (this.picture.Sequence.SequenceHeader.EnableFilterIntra)
{
Span<TSample> filterPrediction = stackalloc TSample[SampleCount];
Span<short> filterResidual = stackalloc short[SampleCount];
// Each recursive filter prediction and its source residual are independent of transform type.
// Prepare them once per filter mode so all legal transforms reuse the same samples.
for (Av1FilterIntraMode filterIntraMode = Av1FilterIntraMode.DC;
filterIntraMode < Av1FilterIntraMode.AllFilterIntraModes;
filterIntraMode++)
{
TOperator.PrepareFilterIntra(
this.blockWorkspace,
sourcePlane,
blockOrigin,
filterPrediction,
above,
left,
filterResidual,
filterIntraMode,
TransformSize,
this.bitDepth);
for (Av1TransformType transformType = Av1TransformType.DctDct;
transformType < Av1TransformType.AllTransformTypes;
transformType++)
{
if (!transformType.IsExtendedSetUsed(transformSetType))
{
continue;
}
Av1EncoderTransformBlockState candidateState = default;
long candidateCost = this.GetFilterIntraCandidateCost(
writer,
macroBlock,
sourcePlane,
blockOrigin,
filterPrediction,
filterResidual,
filterIntraMode,
transformType,
blockContext,
candidateReconstruction,
candidateCoefficients,
ref candidateState);
if (candidateCost < bestTransformCost)
{
CopyCandidate(
candidateReconstruction,
candidateCoefficients,
reconstructionPlane,
blockOrigin,
retainedCoefficients,
TransformSize,
candidateState,
ref retainedState);
bestTransformCost = candidateCost;
bestMode = Av1PredictionMode.DC;
selectedAngleDelta = 0;
selectedFilterIntraMode = filterIntraMode;
}
}
}
}
return bestMode;
}
@ -534,6 +606,11 @@ internal static partial class Av1IntraSuperblockEncoder
ref candidateState);
int rate = Av1TileWriter.GetLumaModeCost(writer, macroBlock, BlockSize, mode, angleDelta);
if (mode == Av1PredictionMode.DC && this.picture.Sequence.SequenceHeader.EnableFilterIntra)
{
rate += writer.GetFilterIntraModeCost(Av1FilterIntraMode.AllFilterIntraModes, BlockSize);
}
rate += writer.GetCoefficientCost(
TransformSize,
transformType,
@ -548,6 +625,61 @@ internal static partial class Av1IntraSuperblockEncoder
return Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion);
}
private long GetFilterIntraCandidateCost(
Av1SymbolEncoder writer,
Av1MacroBlockD macroBlock,
Buffer2DRegion<TSample> sourcePlane,
Point blockOrigin,
ReadOnlySpan<TSample> prediction,
ReadOnlySpan<short> residual,
Av1FilterIntraMode filterIntraMode,
Av1TransformType transformType,
Av1TransformBlockContext blockContext,
Span<TSample> candidateReconstruction,
Span<int> candidateCoefficients,
ref Av1EncoderTransformBlockState candidateState)
{
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
const Av1TransformSize TransformSize = Av1TransformSize.Size8x8;
long distortion = TOperator.EncodePredictionCandidate(
this.blockWorkspace,
sourcePlane,
blockOrigin,
prediction,
residual,
candidateReconstruction,
candidateCoefficients,
TransformSize,
transformType,
Av1Plane.Y,
this.quantization.QIndex[0],
this.quantization.DeltaQDc[(int)Av1Plane.Y],
this.quantization.DeltaQAc[(int)Av1Plane.Y],
this.bitDepth,
ref candidateState);
int rate = Av1TileWriter.GetLumaModeCost(
writer,
macroBlock,
BlockSize,
Av1PredictionMode.DC,
0);
rate += writer.GetFilterIntraModeCost(filterIntraMode, BlockSize);
rate += writer.GetCoefficientCost(
TransformSize,
transformType,
Av1PredictionMode.DC,
candidateCoefficients,
Av1ComponentType.Luminance,
blockContext,
candidateState.EndOfBlock,
this.picture.Parent.FrameHeader.UseReducedTransformSet,
filterIntraMode);
return Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion);
}
private static void CopyCandidate(
ReadOnlySpan<TSample> candidateReconstruction,
ReadOnlySpan<int> candidateCoefficients,

206
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.Operator.cs

@ -154,6 +154,67 @@ internal static partial class Av1IntraSuperblockEncoder
Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state);
/// <summary>
/// Builds one filter-intra prediction for reuse across transform candidates.
/// </summary>
/// <param name="workspace">The reusable block workspace.</param>
/// <param name="source">The coded source plane.</param>
/// <param name="blockOrigin">The transform-block origin in plane samples.</param>
/// <param name="prediction">The contiguous prediction destination.</param>
/// <param name="above">The top reference samples, with prefix storage for the shared corner.</param>
/// <param name="left">The left reference samples.</param>
/// <param name="residual">The contiguous source-minus-prediction destination.</param>
/// <param name="filterIntraMode">The selected filter-intra mode.</param>
/// <param name="transformSize">The prediction dimensions.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
public static abstract void PrepareFilterIntra(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<TSample> source,
Point blockOrigin,
Span<TSample> prediction,
ReadOnlySpan<TSample> above,
ReadOnlySpan<TSample> left,
Span<short> residual,
Av1FilterIntraMode filterIntraMode,
Av1TransformSize transformSize,
Av1BitDepth bitDepth);
/// <summary>
/// Encodes one prepared prediction with the selected transform into decision scratch.
/// </summary>
/// <param name="workspace">The reusable block workspace.</param>
/// <param name="source">The coded source plane.</param>
/// <param name="blockOrigin">The transform-block origin in plane samples.</param>
/// <param name="prediction">The contiguous prediction samples.</param>
/// <param name="residual">The contiguous source-minus-prediction samples.</param>
/// <param name="reconstruction">The contiguous candidate reconstruction.</param>
/// <param name="quantizedCoefficients">The candidate entropy-coding coefficients.</param>
/// <param name="transformSize">The transform dimensions.</param>
/// <param name="transformType">The compound transform applied to the residual.</param>
/// <param name="plane">The component plane containing the block.</param>
/// <param name="qIndex">The effective segment quantizer index.</param>
/// <param name="dcDeltaQ">The plane DC quantizer adjustment.</param>
/// <param name="acDeltaQ">The plane AC quantizer adjustment.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
/// <param name="state">The candidate transform state.</param>
/// <returns>The normalized pixel-domain distortion in AV1 transform units.</returns>
public static abstract long EncodePredictionCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<TSample> source,
Point blockOrigin,
ReadOnlySpan<TSample> prediction,
ReadOnlySpan<short> residual,
Span<TSample> reconstruction,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
Av1Plane plane,
int qIndex,
int dcDeltaQ,
int acDeltaQ,
Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state);
/// <summary>
/// Encodes one chroma-from-luma candidate into contiguous decision scratch.
/// </summary>
@ -318,6 +379,74 @@ internal static partial class Av1IntraSuperblockEncoder
plane,
ref state);
/// <inheritdoc/>
public static void PrepareFilterIntra(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<byte> source,
Point blockOrigin,
Span<byte> prediction,
ReadOnlySpan<byte> above,
ReadOnlySpan<byte> left,
Span<short> residual,
Av1FilterIntraMode filterIntraMode,
Av1TransformSize transformSize,
Av1BitDepth bitDepth)
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
// Prediction finishes before transform search, so its temporary rows can borrow the transform workspace.
Span<byte> filterScratch = MemoryMarshal.AsBytes(workspace.TransformWorkspace).Slice(
0,
Av1FilterIntraPredictorBase.ScratchLength);
Av1FilterIntraPredictorBase.GetPredictor(filterIntraMode)
.Predict(prediction, width, above, left, width, height, filterScratch);
Av1ResidualBuilder.Subtract(
Av1TransformBlockEncoder.GetPlaneSpan(source, blockOrigin),
source.Stride,
prediction,
width,
residual,
width,
width,
height);
}
/// <inheritdoc/>
public static long EncodePredictionCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<byte> source,
Point blockOrigin,
ReadOnlySpan<byte> prediction,
ReadOnlySpan<short> residual,
Span<byte> reconstruction,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
Av1Plane plane,
int qIndex,
int dcDeltaQ,
int acDeltaQ,
Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state)
=> Av1TransformBlockEncoder.EncodePredictionLossyCandidate(
workspace,
source,
blockOrigin,
prediction,
residual,
reconstruction,
quantizedCoefficients,
transformSize,
transformType,
qIndex,
dcDeltaQ,
acDeltaQ,
plane,
ref state);
/// <inheritdoc/>
public static long EncodeChromaFromLumaCandidate(
Av1EncoderBlockWorkspace workspace,
@ -482,6 +611,83 @@ internal static partial class Av1IntraSuperblockEncoder
bitDepth,
ref state);
/// <inheritdoc/>
public static void PrepareFilterIntra(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<ushort> source,
Point blockOrigin,
Span<ushort> prediction,
ReadOnlySpan<ushort> above,
ReadOnlySpan<ushort> left,
Span<short> residual,
Av1FilterIntraMode filterIntraMode,
Av1TransformSize transformSize,
Av1BitDepth bitDepth)
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
// Prediction finishes before transform search, so its temporary rows can borrow the transform workspace.
Span<short> filterScratch = MemoryMarshal.Cast<int, short>(workspace.TransformWorkspace).Slice(
0,
Av1FilterIntraPredictorBase.ScratchLength);
Av1FilterIntraPredictorBase.GetPredictor(filterIntraMode)
.Predict(
MemoryMarshal.Cast<ushort, short>(prediction),
width,
MemoryMarshal.Cast<ushort, short>(above),
MemoryMarshal.Cast<ushort, short>(left),
width,
height,
bitDepth.GetBitCount(),
filterScratch);
Av1ResidualBuilder.Subtract(
Av1TransformBlockEncoder.GetPlaneSpan(source, blockOrigin),
source.Stride,
prediction,
width,
residual,
width,
width,
height);
}
/// <inheritdoc/>
public static long EncodePredictionCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<ushort> source,
Point blockOrigin,
ReadOnlySpan<ushort> prediction,
ReadOnlySpan<short> residual,
Span<ushort> reconstruction,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
Av1Plane plane,
int qIndex,
int dcDeltaQ,
int acDeltaQ,
Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state)
=> Av1TransformBlockEncoder.EncodePredictionLossyCandidate(
workspace,
source,
blockOrigin,
prediction,
residual,
reconstruction,
quantizedCoefficients,
transformSize,
transformType,
qIndex,
dcDeltaQ,
acDeltaQ,
plane,
bitDepth,
ref state);
/// <inheritdoc/>
public static long EncodeChromaFromLumaCandidate(
Av1EncoderBlockWorkspace workspace,

169
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs

@ -158,6 +158,86 @@ internal static class Av1TransformBlockEncoder
return Av1ResidualBuilder.SumSquares(workspace.Residual[..transformSize.GetSize2d()]) << 4;
}
/// <summary>
/// Encodes one eight-bit candidate from a cached prediction and source residual.
/// </summary>
/// <param name="workspace">The reusable residual, coefficient, and transform storage.</param>
/// <param name="source">The coded source plane.</param>
/// <param name="blockOrigin">The block origin in plane samples.</param>
/// <param name="prediction">The contiguous prediction samples.</param>
/// <param name="residual">The contiguous source-minus-prediction samples.</param>
/// <param name="reconstruction">The contiguous candidate reconstruction.</param>
/// <param name="quantizedCoefficients">The candidate entropy-coding coefficients.</param>
/// <param name="transformSize">The selected transform dimensions.</param>
/// <param name="transformType">The selected compound transform type.</param>
/// <param name="qIndex">The segment quantizer index.</param>
/// <param name="dcDeltaQ">The plane DC quantizer adjustment.</param>
/// <param name="acDeltaQ">The plane AC quantizer adjustment.</param>
/// <param name="plane">The component plane containing the block.</param>
/// <param name="state">The candidate transform type and end-of-block syntax.</param>
/// <returns>The normalized pixel-domain distortion in AV1 transform units.</returns>
public static long EncodePredictionLossyCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<byte> source,
Point blockOrigin,
ReadOnlySpan<byte> prediction,
ReadOnlySpan<short> residual,
Span<byte> reconstruction,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
int qIndex,
int dcDeltaQ,
int acDeltaQ,
Av1Plane plane,
ref Av1EncoderTransformBlockState state)
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
int sampleCount = transformSize.GetSize2d();
ReadOnlySpan<byte> sourceSamples = GetPlaneSpan(source, blockOrigin);
// Each transform trial mutates reconstruction and residual scratch, so restore both prepared inputs.
prediction[..sampleCount].CopyTo(reconstruction);
residual[..sampleCount].CopyTo(workspace.Residual);
EncodeLossy(
workspace,
quantizedCoefficients,
transformSize,
transformType,
qIndex,
dcDeltaQ,
acDeltaQ,
Av1BitDepth.EightBit,
ref state);
if (state.EndOfBlock > 0)
{
Av1InverseTransformer.Reconstruct8Bit(
workspace.DequantizedCoefficients,
reconstruction,
width,
transformSize,
transformType,
(int)plane,
state.EndOfBlock,
false,
workspace.TransformWorkspace);
}
Av1ResidualBuilder.Subtract(
sourceSamples,
source.Stride,
reconstruction,
width,
workspace.Residual,
width,
width,
height);
return Av1ResidualBuilder.SumSquares(workspace.Residual[..sampleCount]) << 4;
}
/// <summary>
/// Encodes one eight-bit chroma-from-luma candidate into contiguous decision scratch.
/// </summary>
@ -402,6 +482,95 @@ internal static class Av1TransformBlockEncoder
return normalizedDistortion << 4;
}
/// <summary>
/// Encodes one high-bit-depth candidate from a cached prediction and source residual.
/// </summary>
/// <param name="workspace">The reusable residual, coefficient, and transform storage.</param>
/// <param name="source">The coded source plane.</param>
/// <param name="blockOrigin">The block origin in plane samples.</param>
/// <param name="prediction">The contiguous prediction samples.</param>
/// <param name="residual">The contiguous source-minus-prediction samples.</param>
/// <param name="reconstruction">The contiguous candidate reconstruction.</param>
/// <param name="quantizedCoefficients">The candidate entropy-coding coefficients.</param>
/// <param name="transformSize">The selected transform dimensions.</param>
/// <param name="transformType">The selected compound transform type.</param>
/// <param name="qIndex">The segment quantizer index.</param>
/// <param name="dcDeltaQ">The plane DC quantizer adjustment.</param>
/// <param name="acDeltaQ">The plane AC quantizer adjustment.</param>
/// <param name="plane">The component plane containing the block.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
/// <param name="state">The candidate transform type and end-of-block syntax.</param>
/// <returns>The normalized pixel-domain distortion in AV1 transform units.</returns>
public static long EncodePredictionLossyCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<ushort> source,
Point blockOrigin,
ReadOnlySpan<ushort> prediction,
ReadOnlySpan<short> residual,
Span<ushort> reconstruction,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
int qIndex,
int dcDeltaQ,
int acDeltaQ,
Av1Plane plane,
Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state)
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
int sampleCount = transformSize.GetSize2d();
ReadOnlySpan<ushort> sourceSamples = GetPlaneSpan(source, blockOrigin);
// Each transform trial mutates reconstruction and residual scratch, so restore both prepared inputs.
prediction[..sampleCount].CopyTo(reconstruction);
residual[..sampleCount].CopyTo(workspace.Residual);
EncodeLossy(
workspace,
quantizedCoefficients,
transformSize,
transformType,
qIndex,
dcDeltaQ,
acDeltaQ,
bitDepth,
ref state);
if (state.EndOfBlock > 0)
{
Av1InverseTransformer.ReconstructHighBitDepth(
workspace.DequantizedCoefficients,
MemoryMarshal.Cast<ushort, short>(reconstruction),
width,
transformSize,
transformType,
(int)plane,
state.EndOfBlock,
false,
bitDepth,
workspace.TransformWorkspace);
}
Av1ResidualBuilder.Subtract(
sourceSamples,
source.Stride,
reconstruction,
width,
workspace.Residual,
width,
width,
height);
long distortion = Av1ResidualBuilder.SumSquares(workspace.Residual[..sampleCount]);
int shift = (bitDepth.GetBitCount() - 8) * 2;
long normalizedDistortion = shift == 0
? distortion
: (distortion + (1L << (shift - 1))) >> shift;
return normalizedDistortion << 4;
}
/// <summary>
/// Encodes one high-bit-depth chroma-from-luma candidate into contiguous decision scratch.
/// </summary>

251
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs

@ -3,6 +3,7 @@
using System.Buffers;
using System.Numerics;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
@ -66,6 +67,7 @@ public class Av1IntraSuperblockEncoderTests
using Av1EncoderModeInfoBuffer modeInfo = new(Configuration.Default, Width, Height, disallow4x4AllFrames: true);
Av1PictureControlSet picture = CreatePicture(modeInfo, colorConfig, use128x128Superblock: true, qIndex: 73);
picture.Sequence.SequenceHeader.EnableFilterIntra = true;
using Av1EncoderCoefficientBuffer coefficients = new(
Configuration.Default,
picture.Sequence.SequenceHeader,
@ -1194,6 +1196,246 @@ public class Av1IntraSuperblockEncoderTests
Assert.NotEqual(0, tileWriter.GetTileData(0).Length);
}
[Theory]
[InlineData((int)Av1FilterIntraMode.DC)]
[InlineData((int)Av1FilterIntraMode.Vertical)]
[InlineData((int)Av1FilterIntraMode.Horizontal)]
[InlineData((int)Av1FilterIntraMode.Directional157)]
[InlineData((int)Av1FilterIntraMode.Paeth)]
public void ProductionTileSelectsFilterIntraMode(int filterIntraModeValue)
=> VerifyProductionTileSelectsFilterIntraMode<byte>(
filterIntraModeValue,
8,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new(
Configuration.Default,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512),
static (mode, destination, above, left, _, scratch) =>
Av1FilterIntraPredictorBase.GetPredictor(mode)
.Predict(destination, 8, above, left, 8, 8, scratch));
[Theory]
[InlineData((int)Av1FilterIntraMode.DC, 10)]
[InlineData((int)Av1FilterIntraMode.DC, 12)]
[InlineData((int)Av1FilterIntraMode.Vertical, 10)]
[InlineData((int)Av1FilterIntraMode.Vertical, 12)]
[InlineData((int)Av1FilterIntraMode.Horizontal, 10)]
[InlineData((int)Av1FilterIntraMode.Horizontal, 12)]
[InlineData((int)Av1FilterIntraMode.Directional157, 10)]
[InlineData((int)Av1FilterIntraMode.Directional157, 12)]
[InlineData((int)Av1FilterIntraMode.Paeth, 10)]
[InlineData((int)Av1FilterIntraMode.Paeth, 12)]
public void ProductionTileSelectsFilterIntraModeHighBitDepth(
int filterIntraModeValue,
int bitDepth)
=> VerifyProductionTileSelectsFilterIntraMode<ushort>(
filterIntraModeValue,
bitDepth,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new(
Configuration.Default,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512),
static (mode, destination, above, left, sampleBitDepth, scratch) =>
Av1FilterIntraPredictorBase.GetPredictor(mode)
.Predict(
MemoryMarshal.Cast<ushort, short>(destination),
8,
MemoryMarshal.Cast<ushort, short>(above),
MemoryMarshal.Cast<ushort, short>(left),
8,
8,
sampleBitDepth,
MemoryMarshal.Cast<ushort, short>(scratch)));
private static void VerifyProductionTileSelectsFilterIntraMode<TSample>(
int filterIntraModeValue,
int bitDepth,
TileWriterFactory<TSample> createWriter,
FilterPrediction<TSample> predictFilter)
where TSample : unmanaged, IBinaryInteger<TSample>
{
const int Width = 16;
const int Height = 16;
const int QIndex = 1;
const int TargetX = 8;
const int TargetY = 8;
const Av1TransformSize TransformSize = Av1TransformSize.Size8x8;
Av1FilterIntraMode filterIntraMode = (Av1FilterIntraMode)filterIntraModeValue;
int sampleScale = 1 << (bitDepth - 8);
ObuColorConfig colorConfig = new()
{
IsMonochrome = true,
SubSamplingX = true,
SubSamplingY = true,
BitDepth = (Av1BitDepth)((bitDepth - 8) / 2)
};
using Av1EncoderFrameBuffer<TSample> pilotSource = new(
Configuration.Default,
Width,
Height,
bitDepth,
Av1ColorFormat.Yuv400,
1,
1);
using Av1EncoderFrameBuffer<TSample> pilotReconstruction = new(
Configuration.Default,
Width,
Height,
bitDepth,
Av1ColorFormat.Yuv400,
1,
1);
Buffer2DRegion<TSample> pilotLuma = pilotSource.Frame.CodedView.GetPlane(Av1Plane.Y);
for (int y = 0; y < pilotLuma.Height; y++)
{
Span<TSample> row = pilotLuma.DangerousGetRowSpan(y);
for (int x = 0; x < row.Length; x++)
{
row[x] = TSample.CreateChecked(
(64 + (((x * 71) + (y * 109) + (((x ^ y) & 3) * 37)) & 127)) * sampleScale);
}
}
ClearPlane(pilotReconstruction.Luma);
using Av1EncoderModeInfoBuffer pilotModeInfo = new(Configuration.Default, Width, Height, disallow4x4AllFrames: true);
Av1PictureControlSet pilotTemplate = CreatePicture(pilotModeInfo, colorConfig, use128x128Superblock: false, QIndex);
pilotTemplate.Sequence.SequenceHeader.EnableFilterIntra = true;
using Av1EncoderPictureBuffer pilotPicture = new(
Configuration.Default,
pilotTemplate.Sequence.SequenceHeader,
pilotTemplate.Parent.FrameHeader,
Width,
Height);
using Av1EncoderCoefficientBuffer pilotCoefficients = new(
Configuration.Default,
pilotTemplate.Sequence.SequenceHeader,
Width,
Height);
using Av1EncoderSuperblockWorkspace pilotSuperblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace pilotBlockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter pilotWriter = createWriter(
pilotSource.Frame,
pilotReconstruction.Frame,
pilotPicture.Picture,
pilotCoefficients,
pilotSuperblockWorkspace,
pilotBlockWorkspace);
Buffer2DRegion<TSample> reconstructedLuma = pilotReconstruction.Frame.CodedView.GetPlane(Av1Plane.Y);
Span<TSample> aboveStorage = stackalloc TSample[9];
Span<TSample> above = aboveStorage[1..];
Span<TSample> left = stackalloc TSample[8];
ReadOnlySpan<TSample> reconstructedAbove = reconstructedLuma.DangerousGetRowSpan(TargetY - 1);
aboveStorage[0] = reconstructedAbove[TargetX - 1];
reconstructedAbove.Slice(TargetX, 8).CopyTo(above);
for (int row = 0; row < 8; row++)
{
left[row] = reconstructedLuma.DangerousGetRowSpan(TargetY + row)[TargetX - 1];
}
Span<TSample> target = stackalloc TSample[TransformSize.GetSize2d()];
Span<TSample> filterScratch = stackalloc TSample[Av1FilterIntraPredictorBase.ScratchLength];
predictFilter(filterIntraMode, target, above, left, bitDepth, filterScratch);
using Av1EncoderFrameBuffer<TSample> source = new(
Configuration.Default,
Width,
Height,
bitDepth,
Av1ColorFormat.Yuv400,
1,
1);
using Av1EncoderFrameBuffer<TSample> reconstruction = new(
Configuration.Default,
Width,
Height,
bitDepth,
Av1ColorFormat.Yuv400,
1,
1);
Buffer2DRegion<TSample> sourceLuma = source.Frame.CodedView.GetPlane(Av1Plane.Y);
for (int y = 0; y < pilotLuma.Height; y++)
{
pilotLuma.DangerousGetRowSpan(y).CopyTo(sourceLuma.DangerousGetRowSpan(y));
}
for (int row = 0; row < 8; row++)
{
target.Slice(row * 8, 8).CopyTo(sourceLuma.DangerousGetRowSpan(TargetY + row).Slice(TargetX, 8));
}
ClearPlane(reconstruction.Luma);
using Av1EncoderModeInfoBuffer modeInfo = new(Configuration.Default, Width, Height, disallow4x4AllFrames: true);
Av1PictureControlSet pictureTemplate = CreatePicture(modeInfo, colorConfig, use128x128Superblock: false, QIndex);
pictureTemplate.Sequence.SequenceHeader.EnableFilterIntra = true;
using Av1EncoderPictureBuffer picture = new(
Configuration.Default,
pictureTemplate.Sequence.SequenceHeader,
pictureTemplate.Parent.FrameHeader,
Width,
Height);
using Av1EncoderCoefficientBuffer coefficients = new(
Configuration.Default,
pictureTemplate.Sequence.SequenceHeader,
Width,
Height);
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = createWriter(
source.Frame,
reconstruction.Frame,
picture.Picture,
coefficients,
superblockWorkspace,
blockWorkspace);
Buffer2DRegion<TSample> actualLuma = reconstruction.Frame.CodedView.GetPlane(Av1Plane.Y);
Assert.Equal(above, actualLuma.DangerousGetRowSpan(TargetY - 1).Slice(TargetX, 8));
for (int row = 0; row < 8; row++)
{
Assert.Equal(left[row], actualLuma.DangerousGetRowSpan(TargetY + row)[TargetX - 1]);
Assert.Equal(
target.Slice(row * 8, 8),
actualLuma.DangerousGetRowSpan(TargetY + row).Slice(TargetX, 8));
}
ref Av1MacroBlockModeInfo targetBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(2, 2));
Assert.Equal(Av1PredictionMode.DC, targetBlock.Block.Mode);
Assert.Equal(filterIntraMode, superblockWorkspace.FinalBlocks[3].FilterIntraMode);
int targetTransformIndex = (3 * TransformSize.GetSize2d()) /
Av1EncoderCoefficientBuffer.TransformBlockUnitCoefficientCount;
Av1EncoderTransformBlockState targetState =
coefficients.GetTransformBlockSpan(0, Av1Plane.Y)[targetTransformIndex];
Assert.Equal((ushort)0, targetState.EndOfBlock);
Assert.Equal(Av1TransformType.DctDct, targetState.TransformType);
Assert.NotEqual(0, pilotWriter.GetTileData(0).Length);
Assert.NotEqual(0, tileWriter.GetTileData(0).Length);
}
[Fact]
public void ProductionDirectionalModesConsumeAvailableExtendedEdges()
{
@ -1604,6 +1846,15 @@ public class Av1IntraSuperblockEncoderTests
Av1EncoderBlockWorkspace blockWorkspace)
where TSample : unmanaged;
private delegate void FilterPrediction<TSample>(
Av1FilterIntraMode mode,
Span<TSample> destination,
ReadOnlySpan<TSample> above,
ReadOnlySpan<TSample> left,
int bitDepth,
Span<TSample> scratch)
where TSample : unmanaged;
private static void ClearPlane<TSample>(Buffer2D<TSample> plane)
where TSample : unmanaged
{

Loading…
Cancel
Save