mirror of https://github.com/SixLabors/ImageSharp
4 changed files with 841 additions and 3 deletions
@ -0,0 +1,161 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.InteropServices; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; |
|||
|
|||
/// <content>
|
|||
/// Defines the sample-storage operations used by fixed intra superblock traversal.
|
|||
/// </content>
|
|||
internal static partial class Av1IntraSuperblockEncoder |
|||
{ |
|||
/// <summary>
|
|||
/// Defines type-specific block encoding without coupling traversal to sample storage width.
|
|||
/// </summary>
|
|||
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
|
|||
private interface IBlockEncodingOperator<TSample> |
|||
where TSample : unmanaged |
|||
{ |
|||
/// <summary>
|
|||
/// Gets temporary contiguous storage for left reference samples.
|
|||
/// </summary>
|
|||
/// <param name="residual">The reusable residual workspace.</param>
|
|||
/// <param name="length">The number of reference samples.</param>
|
|||
/// <returns>The writable reference span.</returns>
|
|||
public static abstract Span<TSample> GetLeftReference(Span<short> residual, int length); |
|||
|
|||
/// <summary>
|
|||
/// Encodes and reconstructs one DC intra transform block.
|
|||
/// </summary>
|
|||
/// <param name="workspace">The reusable block workspace.</param>
|
|||
/// <param name="source">The coded source plane.</param>
|
|||
/// <param name="reconstruction">The coded reconstruction plane.</param>
|
|||
/// <param name="blockOrigin">The transform-block origin in plane samples.</param>
|
|||
/// <param name="above">The top reference samples.</param>
|
|||
/// <param name="left">The left reference samples.</param>
|
|||
/// <param name="hasLeft">Whether the left reference is available.</param>
|
|||
/// <param name="hasAbove">Whether the top reference is available.</param>
|
|||
/// <param name="quantizedCoefficients">The retained entropy-coding coefficients.</param>
|
|||
/// <param name="transformSize">The transform dimensions.</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="plane">The component plane containing the block.</param>
|
|||
/// <param name="bitDepth">The coded sample bit depth.</param>
|
|||
/// <param name="state">The retained transform state.</param>
|
|||
public static abstract void Encode( |
|||
Av1EncoderBlockWorkspace workspace, |
|||
Buffer2DRegion<TSample> source, |
|||
Buffer2DRegion<TSample> reconstruction, |
|||
Point blockOrigin, |
|||
ReadOnlySpan<TSample> above, |
|||
ReadOnlySpan<TSample> left, |
|||
bool hasLeft, |
|||
bool hasAbove, |
|||
Span<int> quantizedCoefficients, |
|||
Av1TransformSize transformSize, |
|||
int qIndex, |
|||
int dcDeltaQ, |
|||
int acDeltaQ, |
|||
Av1Plane plane, |
|||
Av1BitDepth bitDepth, |
|||
ref Av1EncoderTransformBlockState state); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Encodes blocks stored as eight-bit samples.
|
|||
/// </summary>
|
|||
private readonly struct ByteOperator : IBlockEncodingOperator<byte> |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public static Span<byte> GetLeftReference(Span<short> residual, int length) |
|||
=> MemoryMarshal.AsBytes(residual)[..length]; |
|||
|
|||
/// <inheritdoc/>
|
|||
public static void Encode( |
|||
Av1EncoderBlockWorkspace workspace, |
|||
Buffer2DRegion<byte> source, |
|||
Buffer2DRegion<byte> reconstruction, |
|||
Point blockOrigin, |
|||
ReadOnlySpan<byte> above, |
|||
ReadOnlySpan<byte> left, |
|||
bool hasLeft, |
|||
bool hasAbove, |
|||
Span<int> quantizedCoefficients, |
|||
Av1TransformSize transformSize, |
|||
int qIndex, |
|||
int dcDeltaQ, |
|||
int acDeltaQ, |
|||
Av1Plane plane, |
|||
Av1BitDepth bitDepth, |
|||
ref Av1EncoderTransformBlockState state) |
|||
=> Av1TransformBlockEncoder.EncodeIntraDcLossy( |
|||
workspace, |
|||
source, |
|||
reconstruction, |
|||
blockOrigin, |
|||
above, |
|||
left, |
|||
hasLeft, |
|||
hasAbove, |
|||
quantizedCoefficients, |
|||
transformSize, |
|||
Av1TransformType.DctDct, |
|||
qIndex, |
|||
dcDeltaQ, |
|||
acDeltaQ, |
|||
plane, |
|||
ref state); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Encodes blocks stored as high-bit-depth samples.
|
|||
/// </summary>
|
|||
private readonly struct UInt16Operator : IBlockEncodingOperator<ushort> |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public static Span<ushort> GetLeftReference(Span<short> residual, int length) |
|||
=> MemoryMarshal.Cast<short, ushort>(residual)[..length]; |
|||
|
|||
/// <inheritdoc/>
|
|||
public static void Encode( |
|||
Av1EncoderBlockWorkspace workspace, |
|||
Buffer2DRegion<ushort> source, |
|||
Buffer2DRegion<ushort> reconstruction, |
|||
Point blockOrigin, |
|||
ReadOnlySpan<ushort> above, |
|||
ReadOnlySpan<ushort> left, |
|||
bool hasLeft, |
|||
bool hasAbove, |
|||
Span<int> quantizedCoefficients, |
|||
Av1TransformSize transformSize, |
|||
int qIndex, |
|||
int dcDeltaQ, |
|||
int acDeltaQ, |
|||
Av1Plane plane, |
|||
Av1BitDepth bitDepth, |
|||
ref Av1EncoderTransformBlockState state) |
|||
=> Av1TransformBlockEncoder.EncodeIntraDcLossy( |
|||
workspace, |
|||
source, |
|||
reconstruction, |
|||
blockOrigin, |
|||
above, |
|||
left, |
|||
hasLeft, |
|||
hasAbove, |
|||
quantizedCoefficients, |
|||
transformSize, |
|||
Av1TransformType.DctDct, |
|||
qIndex, |
|||
dcDeltaQ, |
|||
acDeltaQ, |
|||
plane, |
|||
bitDepth, |
|||
ref state); |
|||
} |
|||
} |
|||
@ -0,0 +1,276 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; |
|||
|
|||
/// <summary>
|
|||
/// Builds fixed DC intra decisions and reconstructed samples for one AV1 superblock.
|
|||
/// </summary>
|
|||
internal static partial class Av1IntraSuperblockEncoder |
|||
{ |
|||
/// <summary>
|
|||
/// Encodes one superblock stored as eight-bit samples.
|
|||
/// </summary>
|
|||
/// <param name="source">The coded source frame.</param>
|
|||
/// <param name="reconstruction">The reconstructed frame updated by the block transforms.</param>
|
|||
/// <param name="picture">The frame coding and mode-information state.</param>
|
|||
/// <param name="superblock">The reusable partition and final-block decisions.</param>
|
|||
/// <param name="coefficientBuffer">The frame-owned quantized coefficient and transform state.</param>
|
|||
/// <param name="blockWorkspace">The reusable block arithmetic workspace.</param>
|
|||
public static void Encode( |
|||
Av1EncoderFrame<byte> source, |
|||
Av1EncoderFrame<byte> reconstruction, |
|||
Av1PictureControlSet picture, |
|||
Av1Superblock superblock, |
|||
Av1EncoderCoefficientBuffer coefficientBuffer, |
|||
Av1EncoderBlockWorkspace blockWorkspace) |
|||
=> Encode<byte, ByteOperator>(source, reconstruction, picture, superblock, coefficientBuffer, blockWorkspace); |
|||
|
|||
/// <summary>
|
|||
/// Encodes one superblock stored as high-bit-depth samples.
|
|||
/// </summary>
|
|||
/// <param name="source">The coded source frame.</param>
|
|||
/// <param name="reconstruction">The reconstructed frame updated by the block transforms.</param>
|
|||
/// <param name="picture">The frame coding and mode-information state.</param>
|
|||
/// <param name="superblock">The reusable partition and final-block decisions.</param>
|
|||
/// <param name="coefficientBuffer">The frame-owned quantized coefficient and transform state.</param>
|
|||
/// <param name="blockWorkspace">The reusable block arithmetic workspace.</param>
|
|||
public static void Encode( |
|||
Av1EncoderFrame<ushort> source, |
|||
Av1EncoderFrame<ushort> reconstruction, |
|||
Av1PictureControlSet picture, |
|||
Av1Superblock superblock, |
|||
Av1EncoderCoefficientBuffer coefficientBuffer, |
|||
Av1EncoderBlockWorkspace blockWorkspace) |
|||
=> Encode<ushort, UInt16Operator>(source, reconstruction, picture, superblock, coefficientBuffer, blockWorkspace); |
|||
|
|||
private static void Encode<TSample, TOperator>( |
|||
Av1EncoderFrame<TSample> source, |
|||
Av1EncoderFrame<TSample> reconstruction, |
|||
Av1PictureControlSet picture, |
|||
Av1Superblock superblock, |
|||
Av1EncoderCoefficientBuffer coefficientBuffer, |
|||
Av1EncoderBlockWorkspace blockWorkspace) |
|||
where TSample : unmanaged |
|||
where TOperator : struct, IBlockEncodingOperator<TSample> |
|||
{ |
|||
int superblockSize = picture.Sequence.SequenceHeader.SuperblockSize.GetWidth(); |
|||
Point superblockOrigin = new( |
|||
(superblock.Index % coefficientBuffer.SuperblockColumnCount) * superblockSize, |
|||
(superblock.Index / coefficientBuffer.SuperblockColumnCount) * superblockSize); |
|||
|
|||
superblock.Workspace.Reset(); |
|||
Traversal<TSample, TOperator> traversal = new( |
|||
source.CodedView, |
|||
reconstruction.CodedView, |
|||
picture, |
|||
superblock, |
|||
coefficientBuffer, |
|||
blockWorkspace); |
|||
|
|||
traversal.EncodePartitionTree(superblockOrigin, picture.Sequence.SequenceHeader.SuperblockSize); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Retains the stack-only state shared by recursive partition and final-block traversal.
|
|||
/// </summary>
|
|||
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
|
|||
/// <typeparam name="TOperator">The type-specific block encoding operations.</typeparam>
|
|||
private ref struct Traversal<TSample, TOperator> |
|||
where TSample : unmanaged |
|||
where TOperator : struct, IBlockEncodingOperator<TSample> |
|||
{ |
|||
private readonly Av1EncoderFrame<TSample>.PlanarView source; |
|||
private readonly Av1EncoderFrame<TSample>.PlanarView reconstruction; |
|||
private readonly Av1PictureControlSet picture; |
|||
private readonly Av1Superblock superblock; |
|||
private readonly Av1EncoderBlockWorkspace blockWorkspace; |
|||
private readonly ObuQuantizationParameters quantization; |
|||
private readonly Av1BitDepth bitDepth; |
|||
private readonly Span<int> lumaCoefficients; |
|||
private readonly Span<int> blueCoefficients; |
|||
private readonly Span<int> redCoefficients; |
|||
private readonly Span<Av1EncoderTransformBlockState> lumaTransformBlocks; |
|||
private readonly Span<Av1EncoderTransformBlockState> blueTransformBlocks; |
|||
private readonly Span<Av1EncoderTransformBlockState> redTransformBlocks; |
|||
private int partitionIndex; |
|||
private int finalBlockIndex; |
|||
private int codedAreaLuma; |
|||
private int codedAreaChroma; |
|||
|
|||
public Traversal( |
|||
Av1EncoderFrame<TSample>.PlanarView source, |
|||
Av1EncoderFrame<TSample>.PlanarView reconstruction, |
|||
Av1PictureControlSet picture, |
|||
Av1Superblock superblock, |
|||
Av1EncoderCoefficientBuffer coefficientBuffer, |
|||
Av1EncoderBlockWorkspace blockWorkspace) |
|||
{ |
|||
this.source = source; |
|||
this.reconstruction = reconstruction; |
|||
this.picture = picture; |
|||
this.superblock = superblock; |
|||
this.blockWorkspace = blockWorkspace; |
|||
this.quantization = picture.Parent.FrameHeader.QuantizationParameters; |
|||
this.bitDepth = picture.Sequence.SequenceHeader.ColorConfig.BitDepth; |
|||
this.lumaCoefficients = coefficientBuffer.GetPlaneSpan(superblock.Index, Av1Plane.Y); |
|||
this.blueCoefficients = coefficientBuffer.GetPlaneSpan(superblock.Index, Av1Plane.U); |
|||
this.redCoefficients = coefficientBuffer.GetPlaneSpan(superblock.Index, Av1Plane.V); |
|||
this.lumaTransformBlocks = coefficientBuffer.GetTransformBlockSpan(superblock.Index, Av1Plane.Y); |
|||
this.blueTransformBlocks = coefficientBuffer.GetTransformBlockSpan(superblock.Index, Av1Plane.U); |
|||
this.redTransformBlocks = coefficientBuffer.GetTransformBlockSpan(superblock.Index, Av1Plane.V); |
|||
this.partitionIndex = 0; |
|||
this.finalBlockIndex = 0; |
|||
this.codedAreaLuma = 0; |
|||
this.codedAreaChroma = 0; |
|||
} |
|||
|
|||
public void EncodePartitionTree(Point blockOrigin, Av1BlockSize blockSize) |
|||
{ |
|||
Av1EncoderCommon common = this.picture.Parent.Common; |
|||
Point modeInfoPosition = blockOrigin >> Av1Constants.ModeInfoSizeLog2; |
|||
if (modeInfoPosition.Y >= common.ModeInfoRowCount || modeInfoPosition.X >= common.ModeInfoColumnCount) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (blockSize == Av1BlockSize.Block8x8) |
|||
{ |
|||
this.superblock.CodingUnitPartitionTypes[this.partitionIndex++] = (byte)Av1PartitionType.None; |
|||
this.EncodeFinalBlock(blockOrigin, modeInfoPosition); |
|||
return; |
|||
} |
|||
|
|||
this.superblock.CodingUnitPartitionTypes[this.partitionIndex++] = (byte)Av1PartitionType.Split; |
|||
Av1BlockSize subSize = Av1PartitionType.Split.GetBlockSubSize(blockSize); |
|||
int halfBlockSize = blockSize.GetWidth() >> 1; |
|||
|
|||
// The preorder and out-of-frame pruning match tile writing, so one reusable decision workspace is sufficient.
|
|||
this.EncodePartitionTree(blockOrigin, subSize); |
|||
this.EncodePartitionTree(blockOrigin + new Size(halfBlockSize, 0), subSize); |
|||
this.EncodePartitionTree(blockOrigin + new Size(0, halfBlockSize), subSize); |
|||
this.EncodePartitionTree(blockOrigin + new Size(halfBlockSize, halfBlockSize), subSize); |
|||
} |
|||
|
|||
private void EncodeFinalBlock(Point blockOrigin, Point modeInfoPosition) |
|||
{ |
|||
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; |
|||
const Av1TransformSize LumaTransformSize = Av1TransformSize.Size8x8; |
|||
int qIndex = this.quantization.QIndex[0]; |
|||
ref Av1MacroBlockModeInfo modeInfo = ref this.picture.GetMacroBlockModeInfo(modeInfoPosition); |
|||
modeInfo.Block = new Av1EncoderBlockModeInfo |
|||
{ |
|||
BlockSize = BlockSize, |
|||
PartitionType = Av1PartitionType.None, |
|||
SegmentId = 0, |
|||
TransformSize = LumaTransformSize, |
|||
Mode = Av1PredictionMode.DC, |
|||
UvMode = Av1ChromaPredictionMode.DC |
|||
}; |
|||
|
|||
modeInfo.CdefStrength = 0; |
|||
ref Av1EncoderBlockStruct block = ref this.superblock.FinalBlocks[this.finalBlockIndex++]; |
|||
block.HasChroma = !this.source.IsMonochrome; |
|||
block.QuantizationIndex = qIndex; |
|||
block.SegmentId = 0; |
|||
|
|||
int lumaTransformIndex = this.codedAreaLuma / |
|||
Av1EncoderCoefficientBuffer.TransformBlockUnitCoefficientCount; |
|||
|
|||
ref Av1EncoderTransformBlockState lumaState = ref this.lumaTransformBlocks[lumaTransformIndex]; |
|||
this.EncodePlaneBlock( |
|||
Av1Plane.Y, |
|||
blockOrigin, |
|||
LumaTransformSize, |
|||
this.lumaCoefficients[this.codedAreaLuma..], |
|||
ref lumaState); |
|||
|
|||
this.codedAreaLuma += LumaTransformSize.GetSize2d(); |
|||
if (this.source.IsMonochrome) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
ObuColorConfig colorConfig = this.picture.Sequence.SequenceHeader.ColorConfig; |
|||
int subsamplingX = colorConfig.SubSamplingX ? 1 : 0; |
|||
int subsamplingY = colorConfig.SubSamplingY ? 1 : 0; |
|||
Point chromaOrigin = new(blockOrigin.X >> subsamplingX, blockOrigin.Y >> subsamplingY); |
|||
Av1TransformSize chromaTransformSize = BlockSize.GetMaxUvTransformSize( |
|||
colorConfig.SubSamplingX, |
|||
colorConfig.SubSamplingY); |
|||
|
|||
int chromaTransformIndex = this.codedAreaChroma / |
|||
Av1EncoderCoefficientBuffer.TransformBlockUnitCoefficientCount; |
|||
|
|||
ref Av1EncoderTransformBlockState blueState = ref this.blueTransformBlocks[chromaTransformIndex]; |
|||
ref Av1EncoderTransformBlockState redState = ref this.redTransformBlocks[chromaTransformIndex]; |
|||
this.EncodePlaneBlock( |
|||
Av1Plane.U, |
|||
chromaOrigin, |
|||
chromaTransformSize, |
|||
this.blueCoefficients[this.codedAreaChroma..], |
|||
ref blueState); |
|||
|
|||
this.EncodePlaneBlock( |
|||
Av1Plane.V, |
|||
chromaOrigin, |
|||
chromaTransformSize, |
|||
this.redCoefficients[this.codedAreaChroma..], |
|||
ref redState); |
|||
|
|||
this.codedAreaChroma += chromaTransformSize.GetSize2d(); |
|||
} |
|||
|
|||
private void EncodePlaneBlock( |
|||
Av1Plane plane, |
|||
Point blockOrigin, |
|||
Av1TransformSize transformSize, |
|||
Span<int> coefficients, |
|||
ref Av1EncoderTransformBlockState state) |
|||
{ |
|||
Buffer2DRegion<TSample> sourcePlane = this.source.GetPlane(plane); |
|||
Buffer2DRegion<TSample> reconstructionPlane = this.reconstruction.GetPlane(plane); |
|||
int width = transformSize.GetWidth(); |
|||
int height = transformSize.GetHeight(); |
|||
bool hasLeft = blockOrigin.X > 0; |
|||
bool hasAbove = blockOrigin.Y > 0; |
|||
ReadOnlySpan<TSample> above = hasAbove |
|||
? reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y - 1).Slice(blockOrigin.X, width) |
|||
: []; |
|||
|
|||
Span<TSample> left = TOperator.GetLeftReference(this.blockWorkspace.Residual, height); |
|||
if (hasLeft) |
|||
{ |
|||
for (int row = 0; row < height; row++) |
|||
{ |
|||
left[row] = reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y + row)[blockOrigin.X - 1]; |
|||
} |
|||
} |
|||
|
|||
// Prediction consumes every gathered reference before residual construction reuses the same workspace bytes.
|
|||
TOperator.Encode( |
|||
this.blockWorkspace, |
|||
sourcePlane, |
|||
reconstructionPlane, |
|||
blockOrigin, |
|||
above, |
|||
left, |
|||
hasLeft, |
|||
hasAbove, |
|||
coefficients, |
|||
transformSize, |
|||
this.quantization.QIndex[0], |
|||
this.quantization.DeltaQDc[(int)plane], |
|||
this.quantization.DeltaQAc[(int)plane], |
|||
plane, |
|||
this.bitDepth, |
|||
ref state); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,401 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
|||
|
|||
/// <summary>
|
|||
/// Verifies fixed DC intra superblock traversal and reconstruction.
|
|||
/// </summary>
|
|||
[Trait("Format", "Avif")] |
|||
public class Av1IntraSuperblockEncoderTests |
|||
{ |
|||
[Fact] |
|||
public void EncodesClipped128SuperblockInWriterPreorderWithoutAllocation() |
|||
{ |
|||
const int Width = 16; |
|||
const int Height = 16; |
|||
ObuColorConfig colorConfig = new() |
|||
{ |
|||
IsMonochrome = false, |
|||
SubSamplingX = true, |
|||
SubSamplingY = true, |
|||
BitDepth = Av1BitDepth.EightBit |
|||
}; |
|||
|
|||
using Av1EncoderFrameBuffer<byte> source = new( |
|||
Configuration.Default, |
|||
Width, |
|||
Height, |
|||
8, |
|||
Av1ColorFormat.Yuv420, |
|||
1, |
|||
1); |
|||
|
|||
using Av1EncoderFrameBuffer<byte> reconstruction = new( |
|||
Configuration.Default, |
|||
Width, |
|||
Height, |
|||
8, |
|||
Av1ColorFormat.Yuv420, |
|||
1, |
|||
1); |
|||
|
|||
FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.Y), 251, 17); |
|||
FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.U), 239, 31); |
|||
FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.V), 233, 47); |
|||
ClearPlane(reconstruction.Luma); |
|||
ClearPlane(Assert.IsType<Buffer2D<byte>>(reconstruction.ChromaBlue)); |
|||
ClearPlane(Assert.IsType<Buffer2D<byte>>(reconstruction.ChromaRed)); |
|||
|
|||
using Av1EncoderModeInfoBuffer modeInfo = new(Configuration.Default, Width, Height, disallow4x4AllFrames: true); |
|||
Av1PictureControlSet picture = CreatePicture(modeInfo, colorConfig, use128x128Superblock: true, qIndex: 73); |
|||
using Av1EncoderCoefficientBuffer coefficients = new( |
|||
Configuration.Default, |
|||
picture.Sequence.SequenceHeader, |
|||
Width, |
|||
Height); |
|||
|
|||
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); |
|||
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); |
|||
Av1Superblock superblock = new() |
|||
{ |
|||
Workspace = superblockWorkspace, |
|||
TileInfo = new Av1TileInfo(0, 0, picture.Parent.FrameHeader), |
|||
Index = 0 |
|||
}; |
|||
|
|||
Av1IntraSuperblockEncoder.Encode( |
|||
source.Frame, |
|||
reconstruction.Frame, |
|||
picture, |
|||
superblock, |
|||
coefficients, |
|||
blockWorkspace); |
|||
|
|||
long before = GC.GetAllocatedBytesForCurrentThread(); |
|||
for (int iteration = 0; iteration < 8; iteration++) |
|||
{ |
|||
Av1IntraSuperblockEncoder.Encode( |
|||
source.Frame, |
|||
reconstruction.Frame, |
|||
picture, |
|||
superblock, |
|||
coefficients, |
|||
blockWorkspace); |
|||
} |
|||
|
|||
Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before); |
|||
Av1PartitionType[] expectedPartitions = |
|||
[ |
|||
Av1PartitionType.Split, |
|||
Av1PartitionType.Split, |
|||
Av1PartitionType.Split, |
|||
Av1PartitionType.Split, |
|||
Av1PartitionType.None, |
|||
Av1PartitionType.None, |
|||
Av1PartitionType.None, |
|||
Av1PartitionType.None |
|||
]; |
|||
|
|||
for (int index = 0; index < expectedPartitions.Length; index++) |
|||
{ |
|||
Assert.Equal(expectedPartitions[index], (Av1PartitionType)superblock.CodingUnitPartitionTypes[index]); |
|||
} |
|||
|
|||
for (int index = 0; index < 4; index++) |
|||
{ |
|||
Assert.True(superblock.FinalBlocks[index].HasChroma); |
|||
Assert.Equal(73, superblock.FinalBlocks[index].QuantizationIndex); |
|||
Assert.Equal(Av1FilterIntraMode.AllFilterIntraModes, superblock.FinalBlocks[index].FilterIntraMode); |
|||
} |
|||
|
|||
Point[] modeInfoPositions = [new(0, 0), new(2, 0), new(0, 2), new(2, 2)]; |
|||
foreach (Point position in modeInfoPositions) |
|||
{ |
|||
ref Av1MacroBlockModeInfo block = ref picture.GetMacroBlockModeInfo(position); |
|||
Assert.Equal(Av1BlockSize.Block8x8, block.Block.BlockSize); |
|||
Assert.Equal(Av1TransformSize.Size8x8, block.Block.TransformSize); |
|||
Assert.Equal(Av1PredictionMode.DC, block.Block.Mode); |
|||
Assert.Equal(Av1ChromaPredictionMode.DC, block.Block.UvMode); |
|||
Assert.False(block.Block.Skip); |
|||
} |
|||
|
|||
Span<Av1EncoderTransformBlockState> lumaStates = coefficients.GetTransformBlockSpan(0, Av1Plane.Y); |
|||
Span<Av1EncoderTransformBlockState> blueStates = coefficients.GetTransformBlockSpan(0, Av1Plane.U); |
|||
Span<Av1EncoderTransformBlockState> redStates = coefficients.GetTransformBlockSpan(0, Av1Plane.V); |
|||
int[] lumaStateIndices = [0, 4, 8, 12]; |
|||
for (int index = 0; index < 4; index++) |
|||
{ |
|||
Assert.NotEqual((ushort)0, lumaStates[lumaStateIndices[index]].EndOfBlock); |
|||
Assert.Equal(Av1TransformType.DctDct, lumaStates[lumaStateIndices[index]].TransformType); |
|||
Assert.NotEqual((ushort)0, blueStates[index].EndOfBlock); |
|||
Assert.NotEqual((ushort)0, redStates[index].EndOfBlock); |
|||
} |
|||
|
|||
AssertContainsNonzero(reconstruction.Frame.CodedView.GetPlane(Av1Plane.Y)); |
|||
AssertContainsNonzero(reconstruction.Frame.CodedView.GetPlane(Av1Plane.U)); |
|||
AssertContainsNonzero(reconstruction.Frame.CodedView.GetPlane(Av1Plane.V)); |
|||
|
|||
// Edge contexts cover the complete 128x128 superblock because partition updates retain the coded geometry
|
|||
// even when most of the superblock lies beyond this deliberately clipped frame.
|
|||
const int ContextUnitCount = 128 >> Av1Constants.ModeInfoSizeLog2; |
|||
using Av1NeighborArrayUnit<Av1PartitionContext> partitions = new( |
|||
Configuration.Default, |
|||
ContextUnitCount, |
|||
ContextUnitCount) |
|||
{ |
|||
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 |
|||
}; |
|||
|
|||
using Av1NeighborArrayUnit<byte> lumaContexts = new( |
|||
Configuration.Default, |
|||
ContextUnitCount, |
|||
ContextUnitCount) |
|||
{ |
|||
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 |
|||
}; |
|||
|
|||
using Av1NeighborArrayUnit<byte> blueContexts = new( |
|||
Configuration.Default, |
|||
ContextUnitCount, |
|||
ContextUnitCount) |
|||
{ |
|||
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 |
|||
}; |
|||
|
|||
using Av1NeighborArrayUnit<byte> redContexts = new( |
|||
Configuration.Default, |
|||
ContextUnitCount, |
|||
ContextUnitCount) |
|||
{ |
|||
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 |
|||
}; |
|||
|
|||
using Av1NeighborArrayUnit<byte> transformContexts = new( |
|||
Configuration.Default, |
|||
ContextUnitCount, |
|||
ContextUnitCount) |
|||
{ |
|||
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 |
|||
}; |
|||
|
|||
picture.PartitionContexts = [partitions]; |
|||
picture.LuminanceDcSignLevelCoefficientNeighbors = [lumaContexts]; |
|||
picture.CbDcSignLevelCoefficientNeighbors = [blueContexts]; |
|||
picture.CrDcSignLevelCoefficientNeighbors = [redContexts]; |
|||
picture.TransformFunctionContexts = [transformContexts]; |
|||
Av1TileWriter.Av1EntropyCodingContext entropyContext = new() |
|||
{ |
|||
MacroBlock = new Av1MacroBlockD { Tile = superblock.TileInfo }, |
|||
MacroBlockModeInfo = picture.GetMacroBlockModeInfo(default), |
|||
SuperblockOrigin = default |
|||
}; |
|||
|
|||
using Av1SymbolEncoder writer = new(Configuration.Default, 512, 73); |
|||
Av1TileWriter.WriteSuperblock( |
|||
picture, |
|||
entropyContext, |
|||
writer, |
|||
superblock, |
|||
coefficients, |
|||
tileIndex: 0); |
|||
|
|||
using IMemoryOwner<byte> encoded = writer.Exit(); |
|||
|
|||
// The writer must consume exactly the transform areas populated above, proving both traversals stay synchronized.
|
|||
Assert.Equal(256, entropyContext.CodedAreaSuperblock); |
|||
Assert.Equal(64, entropyContext.CodedAreaSuperblockUv); |
|||
Assert.NotEqual(0, encoded.GetSpan().Length); |
|||
} |
|||
|
|||
[Fact] |
|||
public void PreservesTwelveBitMonochromeReconstructionPrecision() |
|||
{ |
|||
const int Width = 8; |
|||
const int Height = 8; |
|||
ObuColorConfig colorConfig = new() |
|||
{ |
|||
IsMonochrome = true, |
|||
SubSamplingX = true, |
|||
SubSamplingY = true, |
|||
BitDepth = Av1BitDepth.TwelveBit |
|||
}; |
|||
|
|||
using Av1EncoderFrameBuffer<ushort> source = new( |
|||
Configuration.Default, |
|||
Width, |
|||
Height, |
|||
12, |
|||
Av1ColorFormat.Yuv400, |
|||
0, |
|||
0); |
|||
|
|||
using Av1EncoderFrameBuffer<ushort> reconstruction = new( |
|||
Configuration.Default, |
|||
Width, |
|||
Height, |
|||
12, |
|||
Av1ColorFormat.Yuv400, |
|||
0, |
|||
0); |
|||
|
|||
Buffer2DRegion<ushort> sourcePlane = source.Frame.CodedView.GetPlane(Av1Plane.Y); |
|||
for (int y = 0; y < sourcePlane.Height; y++) |
|||
{ |
|||
Span<ushort> row = sourcePlane.DangerousGetRowSpan(y); |
|||
for (int x = 0; x < row.Length; x++) |
|||
{ |
|||
row[x] = (ushort)(3000 + (((x * 71) + (y * 113)) % 1000)); |
|||
} |
|||
} |
|||
|
|||
ClearPlane(reconstruction.Luma); |
|||
using Av1EncoderModeInfoBuffer modeInfo = new(Configuration.Default, Width, Height, disallow4x4AllFrames: true); |
|||
Av1PictureControlSet picture = CreatePicture(modeInfo, colorConfig, use128x128Superblock: false, qIndex: 37); |
|||
using Av1EncoderCoefficientBuffer coefficients = new( |
|||
Configuration.Default, |
|||
picture.Sequence.SequenceHeader, |
|||
Width, |
|||
Height); |
|||
|
|||
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); |
|||
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); |
|||
Av1Superblock superblock = new() |
|||
{ |
|||
Workspace = superblockWorkspace, |
|||
TileInfo = new Av1TileInfo(0, 0, picture.Parent.FrameHeader), |
|||
Index = 0 |
|||
}; |
|||
|
|||
Av1IntraSuperblockEncoder.Encode( |
|||
source.Frame, |
|||
reconstruction.Frame, |
|||
picture, |
|||
superblock, |
|||
coefficients, |
|||
blockWorkspace); |
|||
|
|||
Buffer2DRegion<ushort> reconstructionPlane = reconstruction.Frame.CodedView.GetPlane(Av1Plane.Y); |
|||
ushort maximum = 0; |
|||
for (int y = 0; y < reconstructionPlane.Height; y++) |
|||
{ |
|||
foreach (ushort sample in reconstructionPlane.DangerousGetRowSpan(y)) |
|||
{ |
|||
maximum = Math.Max(maximum, sample); |
|||
Assert.InRange(sample, (ushort)0, (ushort)4095); |
|||
} |
|||
} |
|||
|
|||
Assert.InRange(maximum, (ushort)(byte.MaxValue + 1), (ushort)4095); |
|||
Assert.False(superblock.FinalBlocks[0].HasChroma); |
|||
Assert.Equal(37, superblock.FinalBlocks[0].QuantizationIndex); |
|||
Assert.NotEqual((ushort)0, coefficients.GetTransformBlockSpan(0, Av1Plane.Y)[0].EndOfBlock); |
|||
Assert.Equal(0, coefficients.GetPlaneSpan(0, Av1Plane.U).Length); |
|||
Assert.Equal(0, coefficients.GetPlaneSpan(0, Av1Plane.V).Length); |
|||
} |
|||
|
|||
private static Av1PictureControlSet CreatePicture( |
|||
Av1EncoderModeInfoBuffer modeInfo, |
|||
ObuColorConfig colorConfig, |
|||
bool use128x128Superblock, |
|||
int qIndex) |
|||
{ |
|||
ObuTileGroupHeader tiles = new() |
|||
{ |
|||
TileColumnCount = 1, |
|||
TileRowCount = 1 |
|||
}; |
|||
|
|||
tiles.TileColumnStartModeInfo[1] = modeInfo.ModeInfoColumnCount; |
|||
tiles.TileRowStartModeInfo[1] = modeInfo.ModeInfoRowCount; |
|||
ObuSequenceHeader sequenceHeader = new() |
|||
{ |
|||
Use128x128Superblock = use128x128Superblock, |
|||
ColorConfig = colorConfig |
|||
}; |
|||
|
|||
ObuFrameHeader frameHeader = new() |
|||
{ |
|||
ModeInfoColumnCount = modeInfo.ModeInfoColumnCount, |
|||
ModeInfoRowCount = modeInfo.ModeInfoRowCount, |
|||
TilesInfo = tiles |
|||
}; |
|||
|
|||
frameHeader.QuantizationParameters.BaseQIndex = qIndex; |
|||
frameHeader.QuantizationParameters.QIndex.Fill(qIndex); |
|||
return new Av1PictureControlSet |
|||
{ |
|||
PartitionContexts = [], |
|||
LuminanceDcSignLevelCoefficientNeighbors = [], |
|||
CrDcSignLevelCoefficientNeighbors = [], |
|||
CbDcSignLevelCoefficientNeighbors = [], |
|||
TransformFunctionContexts = [], |
|||
Sequence = new Av1SequenceControlSet { SequenceHeader = sequenceHeader }, |
|||
Parent = new Av1PictureParentControlSet |
|||
{ |
|||
Common = new Av1EncoderCommon |
|||
{ |
|||
ModeInfoColumnCount = modeInfo.ModeInfoColumnCount, |
|||
ModeInfoRowCount = modeInfo.ModeInfoRowCount, |
|||
ModeInfoStride = modeInfo.ModeInfoStride, |
|||
TilesInfo = tiles, |
|||
FrameSize = new ObuFrameSize() |
|||
}, |
|||
FrameHeader = frameHeader, |
|||
PreviousQIndex = [qIndex] |
|||
}, |
|||
SegmentationNeighborMap = new byte[modeInfo.ModeInfoColumnCount * modeInfo.ModeInfoRowCount], |
|||
ModeInfoGrid = modeInfo.Grid, |
|||
ModeInfoAllocation = modeInfo.Allocation, |
|||
ModeInfoStride = modeInfo.ModeInfoStride, |
|||
Disallow4x4AllFrames = modeInfo.Disallow4x4AllFrames, |
|||
CdefPreset = [[-1, -1, -1, -1]] |
|||
}; |
|||
} |
|||
|
|||
private static void FillPlane(Buffer2DRegion<byte> plane, int modulus, int seed) |
|||
{ |
|||
for (int y = 0; y < plane.Height; y++) |
|||
{ |
|||
Span<byte> row = plane.DangerousGetRowSpan(y); |
|||
for (int x = 0; x < row.Length; x++) |
|||
{ |
|||
row[x] = (byte)(1 + ((seed + (x * 43) + (y * 79)) % modulus)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void ClearPlane<TSample>(Buffer2D<TSample> plane) |
|||
where TSample : unmanaged |
|||
{ |
|||
for (int y = 0; y < plane.Height; y++) |
|||
{ |
|||
plane.DangerousGetRowSpan(y).Clear(); |
|||
} |
|||
} |
|||
|
|||
private static void AssertContainsNonzero<TSample>(Buffer2DRegion<TSample> plane) |
|||
where TSample : unmanaged, IEquatable<TSample> |
|||
{ |
|||
bool containsNonzero = false; |
|||
for (int y = 0; y < plane.Height; y++) |
|||
{ |
|||
foreach (TSample sample in plane.DangerousGetRowSpan(y)) |
|||
{ |
|||
containsNonzero |= !sample.Equals(default); |
|||
} |
|||
} |
|||
|
|||
Assert.True(containsNonzero); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue