mirror of https://github.com/SixLabors/ImageSharp
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2007 lines
80 KiB
2007 lines
80 KiB
// Copyright (c) Six Labors.
|
|
// Licensed under the Six Labors Split License.
|
|
|
|
using System.Runtime.InteropServices;
|
|
using SixLabors.ImageSharp.Formats;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
|
|
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.Prediction.Inter;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
|
|
using SixLabors.ImageSharp.Formats.Heif.Components;
|
|
using SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
|
|
using SixLabors.ImageSharp.Memory;
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
using SixLabors.ImageSharp.Tests.Memory;
|
|
|
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
|
|
|
|
public class Av1EncoderFrameTests
|
|
{
|
|
private const int EightBit = (int)Av1BitDepth.EightBit;
|
|
private const int TenBit = (int)Av1BitDepth.TenBit;
|
|
private const int TwelveBit = (int)Av1BitDepth.TwelveBit;
|
|
private const int Yuv400 = (int)Av1ColorFormat.Yuv400;
|
|
private const int Yuv420 = (int)Av1ColorFormat.Yuv420;
|
|
private const int Yuv422 = (int)Av1ColorFormat.Yuv422;
|
|
private const int Yuv444 = (int)Av1ColorFormat.Yuv444;
|
|
|
|
[Theory]
|
|
[InlineData(EightBit, false, false)]
|
|
[InlineData(EightBit, false, true)]
|
|
[InlineData(EightBit, true, false)]
|
|
[InlineData(EightBit, true, true)]
|
|
[InlineData(TenBit, false, false)]
|
|
[InlineData(TenBit, false, true)]
|
|
[InlineData(TenBit, true, false)]
|
|
[InlineData(TenBit, true, true)]
|
|
[InlineData(TwelveBit, false, false)]
|
|
[InlineData(TwelveBit, false, true)]
|
|
[InlineData(TwelveBit, true, false)]
|
|
[InlineData(TwelveBit, true, true)]
|
|
public void RectangularIntraReferencesExtendTheLastAvailableSample(int bitDepthValue, bool transpose, bool extensionAvailable)
|
|
{
|
|
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
|
|
if (bitDepth == Av1BitDepth.EightBit)
|
|
{
|
|
AssertRectangularIntraReferences<byte, Av1IntraSuperblockEncoder.ByteOperator>(bitDepth, transpose, extensionAvailable);
|
|
}
|
|
else
|
|
{
|
|
AssertRectangularIntraReferences<ushort, Av1IntraSuperblockEncoder.UInt16Operator>(bitDepth, transpose, extensionAvailable);
|
|
}
|
|
}
|
|
|
|
private static void AssertRectangularIntraReferences<TSample, TOperator>(
|
|
Av1BitDepth bitDepth,
|
|
bool transpose,
|
|
bool extensionAvailable)
|
|
where TSample : unmanaged
|
|
where TOperator : struct, Av1IntraSuperblockEncoder.IBlockEncodingOperator<TSample>
|
|
{
|
|
int width = transpose ? 16 : 4;
|
|
int height = transpose ? 4 : 16;
|
|
int scale = 1 << (bitDepth.GetBitCount() - 8);
|
|
using Buffer2D<TSample> plane = Configuration.Default.MemoryAllocator.Allocate2D<TSample>(33, 33);
|
|
for (int i = 0; i < 32; i++)
|
|
{
|
|
plane.DangerousGetRowSpan(0)[i + 1] = TOperator.CreateSample((10 + i) * scale);
|
|
plane.DangerousGetRowSpan(i + 1)[0] = TOperator.CreateSample((50 + i) * scale);
|
|
}
|
|
|
|
plane.DangerousGetRowSpan(0)[0] = TOperator.CreateSample(100 * scale);
|
|
|
|
// Native reconintra.c extends a four-sample edge through its four-sample neighbor, then
|
|
// repeats sample seven to cover the twenty samples required by a 4x16 directional ray.
|
|
// These explicit offsets also distinguish unavailable neighbors from available extension.
|
|
int[] shortEdge = extensionAvailable
|
|
? [0, 1, 2, 3, 4, 5, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]
|
|
: [0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3];
|
|
|
|
int[] longEdge = extensionAvailable
|
|
? [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
|
|
: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15, 15, 15];
|
|
|
|
int[] expectedAbove = transpose ? longEdge : shortEdge;
|
|
int[] expectedLeft = transpose ? shortEdge : longEdge;
|
|
TSample poison = TOperator.CreateSample((1 << bitDepth.GetBitCount()) - 1);
|
|
TSample[] above = new TSample[23];
|
|
TSample[] left = new TSample[23];
|
|
above.AsSpan().Fill(poison);
|
|
left.AsSpan().Fill(poison);
|
|
|
|
// The exact-sized interior includes the corner and twenty projected samples. Sentinel samples
|
|
// on either side detect writes outside the reference view, including the former 2*long-edge span.
|
|
Av1IntraSuperblockEncoder.ModeDecision<TSample, TOperator>.PrepareReferenceSamples(
|
|
plane.GetRegion(),
|
|
new Point(1, 1),
|
|
width,
|
|
height,
|
|
true,
|
|
true,
|
|
extensionAvailable,
|
|
extensionAvailable,
|
|
bitDepth,
|
|
above.AsSpan(1, 21),
|
|
left.AsSpan(1, 21));
|
|
|
|
Assert.Equal(poison, above[0]);
|
|
Assert.Equal(poison, left[0]);
|
|
Assert.Equal(poison, above[^1]);
|
|
Assert.Equal(poison, left[^1]);
|
|
Assert.Equal(TOperator.CreateSample(100 * scale), above[1]);
|
|
Assert.Equal(TOperator.CreateSample(100 * scale), left[1]);
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
Assert.Equal(TOperator.CreateSample((10 + expectedAbove[i]) * scale), above[i + 2]);
|
|
Assert.Equal(TOperator.CreateSample((50 + expectedLeft[i]) * scale), left[i + 2]);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeUsesMultipleTilesWhenSingleTileWidthLimitIsExceeded()
|
|
{
|
|
const int Width = Av1Constants.MaxTileWidth + 1;
|
|
const int SuperblockSize = 1 << (Av1Constants.MaxSuperBlockSizeLog2 - 1);
|
|
const int Height = SuperblockSize;
|
|
int superblockColumns = (Width + SuperblockSize - 1) / SuperblockSize;
|
|
int secondTileStart = ((superblockColumns + 1) / 2) * SuperblockSize;
|
|
using Image<L8> source = new(Width, Height, new L8(128));
|
|
source[0, 0] = new L8(1);
|
|
source[secondTileStart - 1, 0] = new L8(17);
|
|
source[secondTileStart, 0] = new L8(241);
|
|
source[Width - 1, Height - 1] = new L8(255);
|
|
using MemoryStream stream = new();
|
|
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv400);
|
|
|
|
Av1FrameEncoder.Encode(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
colorConfig,
|
|
qIndex: 0,
|
|
effort: 0);
|
|
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Image<L8> decoded = decoder.Decode<L8>(stream.ToArray());
|
|
|
|
Assert.Equal(2, decoder.FrameHeader.TilesInfo.TileColumnCount);
|
|
Assert.Equal(1, decoder.FrameHeader.TilesInfo.TileRowCount);
|
|
Assert.Equal(source.Size, decoded.Size);
|
|
Assert.Equal(source[0, 0], decoded[0, 0]);
|
|
Assert.Equal(source[secondTileStart - 1, 0], decoded[secondTileStart - 1, 0]);
|
|
Assert.Equal(source[secondTileStart, 0], decoded[secondTileStart, 0]);
|
|
Assert.Equal(source[Width - 1, Height - 1], decoded[Width - 1, Height - 1]);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(8, 8, false, EightBit, Yuv400)]
|
|
[InlineData(8, 8, true, EightBit, Yuv400)]
|
|
[InlineData(16, 16, false, EightBit, Yuv400)]
|
|
[InlineData(16, 16, true, EightBit, Yuv400)]
|
|
[InlineData(8, 8, false, TenBit, Yuv400)]
|
|
[InlineData(8, 8, true, TenBit, Yuv400)]
|
|
[InlineData(8, 8, false, TwelveBit, Yuv400)]
|
|
[InlineData(8, 8, true, TwelveBit, Yuv400)]
|
|
[InlineData(16, 16, false, EightBit, Yuv420)]
|
|
[InlineData(16, 16, true, EightBit, Yuv420)]
|
|
[InlineData(13, 11, true, EightBit, Yuv420)]
|
|
[InlineData(16, 16, false, TenBit, Yuv420)]
|
|
[InlineData(16, 16, true, TenBit, Yuv420)]
|
|
[InlineData(16, 16, false, TwelveBit, Yuv420)]
|
|
[InlineData(16, 16, true, TwelveBit, Yuv420)]
|
|
[InlineData(16, 16, false, EightBit, Yuv422)]
|
|
[InlineData(16, 16, true, EightBit, Yuv422)]
|
|
[InlineData(13, 11, true, EightBit, Yuv422)]
|
|
[InlineData(16, 16, false, TenBit, Yuv422)]
|
|
[InlineData(16, 16, true, TenBit, Yuv422)]
|
|
[InlineData(16, 16, false, TwelveBit, Yuv422)]
|
|
[InlineData(16, 16, true, TwelveBit, Yuv422)]
|
|
[InlineData(16, 16, false, EightBit, Yuv444)]
|
|
[InlineData(16, 16, true, EightBit, Yuv444)]
|
|
[InlineData(13, 11, true, EightBit, Yuv444)]
|
|
[InlineData(16, 16, false, TenBit, Yuv444)]
|
|
[InlineData(16, 16, true, TenBit, Yuv444)]
|
|
[InlineData(16, 16, false, TwelveBit, Yuv444)]
|
|
[InlineData(16, 16, true, TwelveBit, Yuv444)]
|
|
public void EncodeWritesReducedStillPictureConsumedByProductionDecoder(int width, int height, bool hasGradient, int bitDepthValue, int colorFormatValue)
|
|
{
|
|
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
|
|
Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue;
|
|
using Image<Rgba32> source = new(width, height);
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
Span<Rgba32> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
if (colorFormat == Av1ColorFormat.Yuv400)
|
|
{
|
|
byte value = hasGradient ? (byte)((x * 13) + (y * 17)) : (byte)128;
|
|
row[x] = new Rgba32(value, value, value);
|
|
}
|
|
else
|
|
{
|
|
byte red = hasGradient ? (byte)((x * 13) + (y * 17)) : (byte)192;
|
|
byte green = hasGradient ? (byte)((x * 7) + (y * 5)) : (byte)64;
|
|
byte blue = hasGradient ? (byte)((x * 3) + (y * 11)) : (byte)32;
|
|
row[x] = new Rgba32(red, green, blue);
|
|
}
|
|
}
|
|
}
|
|
|
|
ObuColorConfig colorConfig = CreateColorConfig(bitDepth, colorFormat);
|
|
using MemoryStream stream = new();
|
|
ObuSequenceHeader encodedHeader = Av1FrameEncoder.Encode(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
colorConfig,
|
|
qIndex: 37,
|
|
effort: 5);
|
|
|
|
byte[] payload = stream.ToArray();
|
|
string outputDirectory = Path.Combine(
|
|
TestEnvironment.ActualOutputDirectoryFullPath,
|
|
"Formats",
|
|
"Heif",
|
|
"Av1");
|
|
|
|
Directory.CreateDirectory(outputDirectory);
|
|
string contentName = hasGradient ? "gradient" : "constant";
|
|
int bitCount = bitDepth.GetBitCount();
|
|
string colorName = colorFormat.ToString()[3..];
|
|
string fileName = $"encoder-frame-{width}x{height}-{bitCount}b-{colorName}-{contentName}.obu";
|
|
File.WriteAllBytes(Path.Combine(outputDirectory, fileName), payload);
|
|
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
|
|
|
|
Assert.Equal(width, decoded.Width);
|
|
Assert.Equal(height, decoded.Height);
|
|
ObuSequenceProfile expectedProfile = bitDepth == Av1BitDepth.TwelveBit || colorFormat == Av1ColorFormat.Yuv422
|
|
? ObuSequenceProfile.Professional
|
|
: colorFormat == Av1ColorFormat.Yuv444
|
|
? ObuSequenceProfile.High
|
|
: ObuSequenceProfile.Main;
|
|
|
|
Assert.Equal(expectedProfile, encodedHeader.SequenceProfile);
|
|
Assert.True(encodedHeader.IsReducedStillPictureHeader);
|
|
|
|
Rgba32 first = decoded[0, 0];
|
|
Assert.Equal(byte.MaxValue, first.A);
|
|
if (colorFormat == Av1ColorFormat.Yuv400)
|
|
{
|
|
Assert.Equal(first.R, first.G);
|
|
Assert.Equal(first.R, first.B);
|
|
}
|
|
else
|
|
{
|
|
Rgba32 center = decoded[width / 2, height / 2];
|
|
Assert.True(center.R != center.G || center.G != center.B);
|
|
}
|
|
|
|
if (hasGradient)
|
|
{
|
|
Assert.NotEqual(first, decoded[width - 1, height - 1]);
|
|
}
|
|
else
|
|
{
|
|
if (colorFormat == Av1ColorFormat.Yuv400)
|
|
{
|
|
Assert.InRange(first.R, 120, 136);
|
|
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
foreach (Rgba32 pixel in decoded.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y))
|
|
{
|
|
Assert.Equal(first, pixel);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void EncodeSequenceFrameWritesNonReducedHeaderConsumedByProductionDecoder(bool encodeAlpha)
|
|
{
|
|
const int Width = 16;
|
|
const int Height = 16;
|
|
using Image<Rgba32> source = new(Width, Height, new Rgba32(48, 96, 192));
|
|
using MemoryStream stream = new();
|
|
ObuColorConfig colorConfig = encodeAlpha
|
|
? CreateColorConfig(Av1BitDepth.EightBit)
|
|
: CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv420);
|
|
|
|
using Av1FrameEncoder.SequenceEncoder encoder = encodeAlpha
|
|
? Av1FrameEncoder.CreateAlphaSequenceEncoder(
|
|
Configuration.Default,
|
|
Width,
|
|
Height,
|
|
colorConfig,
|
|
qIndex: 37,
|
|
effort: 5)
|
|
: Av1FrameEncoder.CreateColorSequenceEncoder(
|
|
Configuration.Default,
|
|
Width,
|
|
Height,
|
|
colorConfig,
|
|
qIndex: 37,
|
|
effort: 5);
|
|
|
|
encoder.EncodeKeyFrame(source.Frames.RootFrame, stream);
|
|
ObuSequenceHeader encodedHeader = encoder.SequenceHeader;
|
|
byte[] payload = stream.ToArray();
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
|
|
ObuSequenceHeader decodedHeader = decoder.SequenceHeader;
|
|
|
|
Assert.False(encodedHeader.IsStillPicture);
|
|
Assert.False(encodedHeader.IsReducedStillPictureHeader);
|
|
Assert.Equal(encodeAlpha, encodedHeader.ColorConfig.IsMonochrome);
|
|
Assert.NotNull(decodedHeader);
|
|
Assert.False(decodedHeader.IsStillPicture);
|
|
Assert.False(decodedHeader.IsReducedStillPictureHeader);
|
|
Assert.Equal(new Size(Width, Height), decoded.Size);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies dependent color samples with odd visible dimensions and motion across subsampled chroma phases.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(EightBit, Yuv420, 8)]
|
|
[InlineData(TenBit, Yuv420, 8)]
|
|
[InlineData(TwelveBit, Yuv420, 8)]
|
|
[InlineData(EightBit, Yuv420, 9)]
|
|
[InlineData(TenBit, Yuv420, 9)]
|
|
[InlineData(TwelveBit, Yuv420, 9)]
|
|
[InlineData(EightBit, Yuv422, 9)]
|
|
[InlineData(TenBit, Yuv422, 9)]
|
|
[InlineData(TwelveBit, Yuv422, 9)]
|
|
[InlineData(EightBit, Yuv444, 9)]
|
|
[InlineData(TenBit, Yuv444, 9)]
|
|
[InlineData(TwelveBit, Yuv444, 9)]
|
|
public void SequenceEncoderPreservesNativeColorPlanesWithSubpixelMotion(int bitDepthValue, int colorFormatValue, int effort)
|
|
{
|
|
const int Width = 23;
|
|
const int Height = 19;
|
|
const int QIndex = 17;
|
|
const int ByteToUInt16Scale = ushort.MaxValue / byte.MaxValue;
|
|
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
|
|
Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue;
|
|
ObuColorConfig colorConfig = CreateColorConfig(bitDepth, colorFormat);
|
|
ReadOnlySpan<int> period = [0, 28, 40, 28, 0, -28, -40, -12];
|
|
using Image<Rgb48> source = new(Width, Height);
|
|
using Av1FrameEncoder.SequenceEncoder encoder = Av1FrameEncoder.CreateColorSequenceEncoder(
|
|
Configuration.Default, Width, Height, colorConfig, QIndex, effort);
|
|
|
|
string outputDirectory = TestEnvironment.CreateOutputDirectory("Heif", "Av1", nameof(this.SequenceEncoderPreservesNativeColorPlanesWithSubpixelMotion));
|
|
string outputName = $"{bitDepth.GetBitCount()}-{colorFormat}-effort{effort}";
|
|
using FileStream output = File.Create(Path.Combine(outputDirectory, outputName + ".obu"));
|
|
using BinaryWriter rawOutput = new(File.Create(Path.Combine(outputDirectory, outputName + ".managed.yuv")));
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using MemoryStream sample = new();
|
|
for (int frameIndex = 0; frameIndex < 2; frameIndex++)
|
|
{
|
|
// The second source translates all three channels by one luma sample on each axis. Chroma is
|
|
// converted independently by the production converter, so 4:2:0 and 4:2:2 cannot hide behind
|
|
// constant neutral planes. Odd dimensions also exercise each plane's visible-edge clipping.
|
|
for (int y = 0; y < Height; y++)
|
|
{
|
|
Span<Rgb48> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
int referenceY = Math.Min(y + frameIndex, Height - 1);
|
|
for (int x = 0; x < Width; x++)
|
|
{
|
|
int referenceX = Math.Min(x + frameIndex, Width - 1);
|
|
row[x] = new Rgb48(
|
|
(ushort)((128 + period[referenceX % period.Length]) * ByteToUInt16Scale),
|
|
(ushort)((128 + period[referenceY % period.Length]) * ByteToUInt16Scale),
|
|
(ushort)((128 + period[(referenceX + referenceY) % period.Length]) * ByteToUInt16Scale));
|
|
}
|
|
}
|
|
|
|
sample.SetLength(0);
|
|
if (frameIndex == 0)
|
|
{
|
|
encoder.EncodeKeyFrame(source.Frames.RootFrame, sample);
|
|
}
|
|
else
|
|
{
|
|
encoder.EncodeInterFrame(source.Frames.RootFrame, sample);
|
|
}
|
|
|
|
sample.Position = 0;
|
|
sample.CopyTo(output);
|
|
decoder.DecodeSequenceReference(sample.ToArray(), null, null);
|
|
Av1FrameBuffer<byte> decoded = Assert.IsType<Av1FrameBuffer<byte>>(decoder.FrameBuffer);
|
|
Assert.Equal(Width, decoded.Width);
|
|
Assert.Equal(Height, decoded.Height);
|
|
Assert.Equal(bitDepth, decoded.BitDepth);
|
|
Av1FrameInfo decodedFrameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
|
|
foreach (Av1BlockModeInfo mode in decodedFrameInfo.GetModeInfos(Point.Empty, decodedFrameInfo.GetModeInfoCount(Point.Empty)))
|
|
{
|
|
// The same ordinary-intra policy applies in key and inter frames. Read the emitted syntax,
|
|
// rather than infer the skip flag from pixel agreement between encoder and decoder.
|
|
if (mode.ReferenceFrames[0] == Av1ReferenceFrameType.Intra && !mode.UseIntraBlockCopy)
|
|
{
|
|
Assert.False(mode.Skip);
|
|
}
|
|
}
|
|
|
|
for (int planeIndex = 0; planeIndex < 3; planeIndex++)
|
|
{
|
|
Av1Plane plane = (Av1Plane)planeIndex;
|
|
int subsamplingX = plane == Av1Plane.Y || !colorConfig.SubSamplingX ? 0 : 1;
|
|
int subsamplingY = plane == Av1Plane.Y || !colorConfig.SubSamplingY ? 0 : 1;
|
|
int planeHeight = (Height + subsamplingY) >> subsamplingY;
|
|
if (bitDepth == Av1BitDepth.EightBit)
|
|
{
|
|
Buffer2DRegion<byte> planeSamples = decoded.DeriveBlockPointer(plane, subsamplingX, subsamplingY);
|
|
for (int y = 0; y < planeHeight; y++)
|
|
{
|
|
rawOutput.Write(planeSamples.DangerousGetRowSpan(y));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int y = 0; y < planeHeight; y++)
|
|
{
|
|
foreach (ushort value in decoded.GetHighBitDepthRowSpan(plane, y, subsamplingX, subsamplingY))
|
|
{
|
|
// Raw high-bit-depth output uses explicit little-endian samples on every host.
|
|
rawOutput.Write(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader);
|
|
Assert.Equal(ObuFrameType.InterFrame, frameHeader.FrameType);
|
|
Assert.Equal(Av1InterpolationFilter.Switchable, frameHeader.InterpolationFilter);
|
|
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
|
|
bool hasMotion = false;
|
|
bool hasFractionalChromaMotion = false;
|
|
foreach (Av1BlockModeInfo mode in frameInfo.GetModeInfos(Point.Empty, frameInfo.GetModeInfoCount(Point.Empty)))
|
|
{
|
|
if (mode.ReferenceFrames[0] == Av1ReferenceFrameType.Last)
|
|
{
|
|
Av1MotionVector vector = mode.MotionVectors[0];
|
|
hasMotion |= vector.Column != 0 || vector.Row != 0;
|
|
|
|
// A subsampled chroma phase repeats every two luma pixels, or sixteen Q3 motion units.
|
|
int chromaPhaseMask = (Av1MotionVector.SubpixelScale << 1) - 1;
|
|
hasFractionalChromaMotion |=
|
|
(colorConfig.SubSamplingX && (vector.Column & chromaPhaseMask) != 0) ||
|
|
(colorConfig.SubSamplingY && (vector.Row & chromaPhaseMask) != 0);
|
|
}
|
|
}
|
|
|
|
Assert.True(hasMotion);
|
|
if (colorConfig.SubSamplingX || colorConfig.SubSamplingY)
|
|
{
|
|
Assert.True(hasFractionalChromaMotion);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies retained reference reconstruction and effort-dependent filter signaling through production sequence decoding.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(5, false, false)]
|
|
[InlineData(7, false, false)]
|
|
[InlineData(8, true, false)]
|
|
[InlineData(9, true, true)]
|
|
public void SequenceEncoderUsesRetainedReconstructionForInterFrame(int effort, bool switchableFilters, bool dualFilters)
|
|
{
|
|
const int Width = 16;
|
|
const int Height = 16;
|
|
Rgba32 sourceColor = new(48, 96, 192);
|
|
using Image<Rgba32> source = new(Width, Height, sourceColor);
|
|
using MemoryStream firstSample = new();
|
|
using MemoryStream secondSample = new();
|
|
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv420);
|
|
using Av1FrameEncoder.SequenceEncoder encoder = Av1FrameEncoder.CreateColorSequenceEncoder(
|
|
Configuration.Default,
|
|
Width,
|
|
Height,
|
|
colorConfig,
|
|
qIndex: 37,
|
|
effort);
|
|
|
|
encoder.EncodeKeyFrame(source.Frames.RootFrame, firstSample);
|
|
encoder.EncodeInterFrame(source.Frames.RootFrame, secondSample);
|
|
|
|
// Retain the exact two-sample elementary stream for independent reference-decoder acceptance.
|
|
string outputDirectory = TestEnvironment.CreateOutputDirectory("Heif", "Av1", nameof(this.SequenceEncoderUsesRetainedReconstructionForInterFrame));
|
|
using (FileStream output = File.Create(Path.Combine(outputDirectory, $"effort-{effort}.obu")))
|
|
{
|
|
firstSample.Position = 0;
|
|
firstSample.CopyTo(output);
|
|
secondSample.Position = 0;
|
|
secondSample.CopyTo(output);
|
|
}
|
|
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using ImageFrame<Rgba32> decodedFirst = decoder.DecodeSequenceFrame<Rgba32>(
|
|
firstSample.ToArray(),
|
|
null,
|
|
null);
|
|
|
|
Av1FrameInfo firstFrameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
|
|
foreach (Av1BlockModeInfo mode in firstFrameInfo.GetModeInfos(Point.Empty, firstFrameInfo.GetModeInfoCount(Point.Empty)))
|
|
{
|
|
Assert.Equal(Av1ReferenceFrameType.Intra, mode.ReferenceFrames[0]);
|
|
Assert.False(mode.UseIntraBlockCopy);
|
|
Assert.False(mode.Skip);
|
|
}
|
|
|
|
using ImageFrame<Rgba32> decodedSecond = decoder.DecodeSequenceFrame<Rgba32>(
|
|
secondSample.ToArray(),
|
|
null,
|
|
null);
|
|
|
|
ObuFrameHeader frameHeader = decoder.FrameHeader;
|
|
Assert.Equal(ObuFrameType.InterFrame, frameHeader.FrameType);
|
|
Assert.False(frameHeader.SegmentationParameters.Enabled);
|
|
Assert.False(frameHeader.AllowScreenContentTools);
|
|
Assert.False(frameHeader.ForceIntegerMotionVector);
|
|
Assert.Equal(effort >= 8, frameHeader.AllowHighPrecisionMotionVector);
|
|
Assert.Equal(37, frameHeader.QuantizationParameters.BaseQIndex);
|
|
Assert.Equal(switchableFilters ? Av1InterpolationFilter.Switchable : Av1InterpolationFilter.Regular, frameHeader.InterpolationFilter);
|
|
Assert.Equal(dualFilters, decoder.SequenceHeader.EnableDualFilter);
|
|
Av1FrameInfo secondFrameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
|
|
bool hasSkippedInterBlock = false;
|
|
foreach (Av1BlockModeInfo mode in secondFrameInfo.GetModeInfos(Point.Empty, secondFrameInfo.GetModeInfoCount(Point.Empty)))
|
|
{
|
|
hasSkippedInterBlock |= mode.ReferenceFrames[0] == Av1ReferenceFrameType.Last && mode.Skip;
|
|
}
|
|
|
|
// Repeated frames still use the inter skip alternative when prediction supplies the retained samples.
|
|
Assert.True(hasSkippedInterBlock);
|
|
for (int y = 0; y < Height; y++)
|
|
{
|
|
Assert.Equal(
|
|
decodedFirst.PixelBuffer.DangerousGetRowSpan(y),
|
|
decodedSecond.PixelBuffer.DangerousGetRowSpan(y));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SequenceEncoderWritesSelectedGlobalTranslation()
|
|
{
|
|
const int Width = 64;
|
|
const int Height = 64;
|
|
const int HorizontalOffset = 4;
|
|
using Image<Rgba32> first = new(Width, Height);
|
|
using Image<Rgba32> second = new(Width, Height);
|
|
for (int y = 0; y < Height; y++)
|
|
{
|
|
Span<Rgba32> firstRow = first.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
for (int x = 0; x < Width; x++)
|
|
{
|
|
byte value = (byte)(((x * 37) + (y * 53) + ((x * y) * 11)) & byte.MaxValue);
|
|
firstRow[x] = new Rgba32(value, value, value);
|
|
}
|
|
}
|
|
|
|
for (int y = 0; y < Height; y++)
|
|
{
|
|
ReadOnlySpan<Rgba32> firstRow = first.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
Span<Rgba32> secondRow = second.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
for (int x = 0; x < Width; x++)
|
|
{
|
|
secondRow[x] = firstRow[Math.Min(x + HorizontalOffset, Width - 1)];
|
|
}
|
|
}
|
|
|
|
using MemoryStream firstSample = new();
|
|
using MemoryStream secondSample = new();
|
|
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv420);
|
|
using Av1FrameEncoder.SequenceEncoder encoder = Av1FrameEncoder.CreateColorSequenceEncoder(
|
|
Configuration.Default,
|
|
Width,
|
|
Height,
|
|
colorConfig,
|
|
qIndex: 4,
|
|
effort: 6);
|
|
|
|
encoder.EncodeKeyFrame(first.Frames.RootFrame, firstSample);
|
|
encoder.EncodeInterFrame(second.Frames.RootFrame, secondSample);
|
|
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using ImageFrame<Rgba32> decodedFirst = decoder.DecodeSequenceFrame<Rgba32>(
|
|
firstSample.ToArray(),
|
|
null,
|
|
null);
|
|
|
|
using ImageFrame<Rgba32> decodedSecond = decoder.DecodeSequenceFrame<Rgba32>(
|
|
secondSample.ToArray(),
|
|
null,
|
|
null);
|
|
|
|
ObuFrameHeader frameHeader = decoder.FrameHeader;
|
|
Av1GlobalMotionParameters globalMotion = frameHeader.GetGlobalMotionParameters()[0];
|
|
Av1MotionVector vector = globalMotion.GetMotionVector(
|
|
frameHeader.AllowHighPrecisionMotionVector,
|
|
Av1BlockSize.Block8x8,
|
|
default,
|
|
frameHeader.ForceIntegerMotionVector);
|
|
|
|
Assert.Equal(Av1GlobalMotionType.RotationZoom, globalMotion.Type);
|
|
Assert.False(frameHeader.AllowScreenContentTools);
|
|
Assert.False(frameHeader.ForceIntegerMotionVector);
|
|
Assert.Equal(0, vector.Row);
|
|
Assert.Equal(HorizontalOffset * 8, vector.Column);
|
|
Assert.Equal(first.Size, decodedFirst.Size);
|
|
Assert.Equal(second.Size, decodedSecond.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void SequenceEncoderRejectsInvalidConversionBeforeAllocatingStorage()
|
|
{
|
|
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.TenBit, Av1ColorFormat.Yuv420);
|
|
colorConfig.MatrixCoefficients = ObuMatrixCoefficients.YCgCoRe;
|
|
Configuration configuration = Configuration.Default.Clone();
|
|
TestMemoryAllocator allocator = new();
|
|
allocator.EnableNonThreadSafeLogging();
|
|
configuration.MemoryAllocator = allocator;
|
|
|
|
// This internal factory receives resolved AV1 settings. The shared converter already rejects a
|
|
// reversible matrix with subsampling; that rejection must occur before any owner can be stranded.
|
|
Assert.Throws<InvalidImageContentException>(() =>
|
|
{
|
|
using Av1FrameEncoder.SequenceEncoder encoder = Av1FrameEncoder.CreateColorSequenceEncoder(
|
|
configuration, 32, 32, colorConfig, 17, 9);
|
|
});
|
|
|
|
Assert.Empty(allocator.AllocationLog);
|
|
Assert.Empty(allocator.ReturnLog);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false, EightBit)]
|
|
[InlineData(false, TenBit)]
|
|
[InlineData(false, TwelveBit)]
|
|
[InlineData(true, EightBit)]
|
|
[InlineData(true, TenBit)]
|
|
[InlineData(true, TwelveBit)]
|
|
public void SequenceEncoderConstructionFailureReturnsEveryAllocation(bool encodeAlpha, int bitDepthValue)
|
|
{
|
|
ObuColorConfig colorConfig = CreateColorConfig(
|
|
(Av1BitDepth)bitDepthValue,
|
|
encodeAlpha ? Av1ColorFormat.Yuv400 : Av1ColorFormat.Yuv420);
|
|
|
|
Configuration configuration = Configuration.Default.Clone();
|
|
TestMemoryAllocator successfulAllocator = new();
|
|
successfulAllocator.EnableNonThreadSafeLogging();
|
|
configuration.MemoryAllocator = successfulAllocator;
|
|
using (Av1FrameEncoder.SequenceEncoder encoder = encodeAlpha
|
|
? Av1FrameEncoder.CreateAlphaSequenceEncoder(configuration, 32, 32, colorConfig, 17, 9)
|
|
: Av1FrameEncoder.CreateColorSequenceEncoder(configuration, 32, 32, colorConfig, 17, 9))
|
|
{
|
|
Assert.NotEmpty(successfulAllocator.AllocationLog);
|
|
}
|
|
|
|
Assert.Equal(successfulAllocator.AllocationLog.Count, successfulAllocator.ReturnLog.Count);
|
|
for (int failureIndex = 0; failureIndex < successfulAllocator.AllocationLog.Count; failureIndex++)
|
|
{
|
|
FailingSequenceAllocator allocator = new(failureIndex);
|
|
configuration.MemoryAllocator = allocator;
|
|
|
|
// Fail each real allocator request, including those made inside nested constructors. A constructor
|
|
// that throws never reaches the caller's using statement, so its completed owners must unwind there.
|
|
InvalidMemoryOperationException exception = Assert.Throws<InvalidMemoryOperationException>(() =>
|
|
{
|
|
using Av1FrameEncoder.SequenceEncoder encoder = encodeAlpha
|
|
? Av1FrameEncoder.CreateAlphaSequenceEncoder(configuration, 32, 32, colorConfig, 17, 9)
|
|
: Av1FrameEncoder.CreateColorSequenceEncoder(configuration, 32, 32, colorConfig, 17, 9);
|
|
});
|
|
|
|
Assert.Equal("Sequence allocation failure.", exception.Message);
|
|
Assert.Equal(failureIndex, allocator.AllocationLog.Count);
|
|
Assert.All(
|
|
allocator.AllocationLog,
|
|
allocation => Assert.Single(allocator.ReturnLog, returned => returned.AllocationId == allocation.AllocationId));
|
|
|
|
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false, EightBit, Yuv420, 384)]
|
|
[InlineData(false, TwelveBit, Yuv444, 288)]
|
|
[InlineData(true, EightBit, Yuv400, 192)]
|
|
[InlineData(true, TwelveBit, Yuv400, 192)]
|
|
public void SequenceEncoderReusesAllocatorOwnedRowStorage(
|
|
bool encodeAlpha,
|
|
int bitDepthValue,
|
|
int colorFormatValue,
|
|
int expectedRowStorageLength)
|
|
{
|
|
const int Width = 64;
|
|
const int Height = 64;
|
|
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
|
|
Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue;
|
|
using Image<Rgba64> source = new(
|
|
Width,
|
|
Height,
|
|
new Rgba64(ushort.MaxValue, 32768, 16384, 49152));
|
|
|
|
TestMemoryAllocator allocator = new();
|
|
allocator.EnableNonThreadSafeLogging();
|
|
Configuration configuration = Configuration.Default.Clone();
|
|
configuration.MemoryAllocator = allocator;
|
|
ObuColorConfig colorConfig = CreateColorConfig(bitDepth, colorFormat);
|
|
TestMemoryAllocator.AllocationRequest rowStorage;
|
|
int allocationCount;
|
|
using (Av1FrameEncoder.SequenceEncoder encoder = encodeAlpha
|
|
? Av1FrameEncoder.CreateAlphaSequenceEncoder(
|
|
configuration,
|
|
Width,
|
|
Height,
|
|
colorConfig,
|
|
qIndex: 37,
|
|
effort: 6)
|
|
: Av1FrameEncoder.CreateColorSequenceEncoder(
|
|
configuration,
|
|
Width,
|
|
Height,
|
|
colorConfig,
|
|
qIndex: 37,
|
|
effort: 6))
|
|
{
|
|
rowStorage = Assert.Single(
|
|
allocator.AllocationLog,
|
|
allocation => allocation.ElementType == typeof(float));
|
|
|
|
allocationCount = allocator.AllocationLog.Count;
|
|
using MemoryStream output = new(256 * 1024);
|
|
encoder.EncodeKeyFrame(source.Frames.RootFrame, output);
|
|
encoder.EncodeInterFrame(source.Frames.RootFrame, output);
|
|
|
|
// Fixed sequence geometry lets libaom retain its frame-sized compressor data. The ImageSharp
|
|
// sequence encoder must likewise perform every sample conversion and coding pass without another rent.
|
|
Assert.Equal(allocationCount, allocator.AllocationLog.Count);
|
|
}
|
|
|
|
Assert.Equal(expectedRowStorageLength, rowStorage.Length);
|
|
Assert.Contains(
|
|
allocator.ReturnLog,
|
|
returned => returned.AllocationId == rowStorage.AllocationId);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(TenBit, 8, 8, 0)]
|
|
[InlineData(TwelveBit, 8, 8, 0)]
|
|
[InlineData(TenBit, 24, 16, 9)]
|
|
[InlineData(TwelveBit, 24, 16, 9)]
|
|
[InlineData(TenBit, 16, 24, 10)]
|
|
[InlineData(TwelveBit, 16, 24, 10)]
|
|
public void LosslessHighBitDepthEncodingPreservesNativePlanes(int bitDepthValue, int width, int height, int effort)
|
|
{
|
|
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
|
|
using Image<Rgb48> source = new(width, height);
|
|
for (int row = 0; row < height; row++)
|
|
{
|
|
Span<Rgb48> pixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
|
|
for (int column = 0; column < width; column++)
|
|
{
|
|
pixels[column] = new Rgb48(
|
|
(ushort)(((column * 7001) + (row * 997)) & ushort.MaxValue),
|
|
(ushort)(((row * 6007) + (column * 1231)) & ushort.MaxValue),
|
|
(ushort)(((column * 4001) + (row * 3001)) & ushort.MaxValue));
|
|
}
|
|
}
|
|
|
|
ObuColorConfig colorConfig = new()
|
|
{
|
|
IsColorDescriptionPresent = true,
|
|
ColorPrimaries = ObuColorPrimaries.Bt709,
|
|
TransferCharacteristics = ObuTransferCharacteristics.Srgb,
|
|
MatrixCoefficients = ObuMatrixCoefficients.Identity,
|
|
ColorRange = true,
|
|
BitDepth = bitDepth
|
|
};
|
|
|
|
using Av1EncoderFrameBuffer<ushort> expected = new(
|
|
Configuration.Default,
|
|
width,
|
|
height,
|
|
bitDepth.GetBitCount(),
|
|
Av1ColorFormat.Yuv444,
|
|
1,
|
|
1);
|
|
|
|
Av1FrameEncoder.PrepareSource(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
expected.Frame,
|
|
colorConfig);
|
|
|
|
using MemoryStream stream = new();
|
|
Av1FrameEncoder.Encode(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
colorConfig,
|
|
qIndex: 0,
|
|
effort);
|
|
|
|
byte[] payload = stream.ToArray();
|
|
string outputDirectory = Path.Combine(
|
|
TestEnvironment.ActualOutputDirectoryFullPath,
|
|
"Formats",
|
|
"Heif",
|
|
"Av1");
|
|
|
|
Directory.CreateDirectory(outputDirectory);
|
|
string outputName = $"encoder-frame-{width}x{height}-{bitDepth.GetBitCount()}b-444-lossless-effort{effort}";
|
|
File.WriteAllBytes(
|
|
Path.Combine(outputDirectory, outputName + ".obu"),
|
|
payload);
|
|
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Av1FrameBuffer<byte> actual = decoder.DecodeFrameBuffer(payload, null, null, out _);
|
|
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader);
|
|
|
|
Assert.Equal(width, actual.Width);
|
|
Assert.Equal(height, actual.Height);
|
|
Assert.True(frameHeader.CodedLossless);
|
|
Assert.True(frameHeader.AllLossless);
|
|
Assert.Equal(Av1TransformMode.Only4x4, frameHeader.TransformMode);
|
|
|
|
// Lossless native planes are the oracle for external decoding, not the packed RGB conversion on return.
|
|
// UInt16 raw samples are explicitly little-endian even when these tests run on a different host byte order.
|
|
using BinaryWriter rawOutput = new(File.Create(Path.Combine(outputDirectory, outputName + ".source.yuv")));
|
|
foreach (Av1Plane plane in new[] { Av1Plane.Y, Av1Plane.U, Av1Plane.V })
|
|
{
|
|
Buffer2DRegion<ushort> expectedPlane = expected.Frame.View.GetPlane(plane);
|
|
for (int row = 0; row < height; row++)
|
|
{
|
|
ReadOnlySpan<ushort> expectedRow = expectedPlane.DangerousGetRowSpan(row);
|
|
Assert.Equal(expectedRow, actual.GetHighBitDepthRowSpan(plane, row, 0, 0));
|
|
foreach (ushort sample in expectedRow)
|
|
{
|
|
rawOutput.Write(sample);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that live partition search preserves lossless syntax across clipped parent nodes and superblocks.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(48, 24, 9)]
|
|
[InlineData(24, 48, 9)]
|
|
[InlineData(80, 24, 9)]
|
|
[InlineData(24, 80, 9)]
|
|
[InlineData(96, 24, 10)]
|
|
[InlineData(24, 96, 10)]
|
|
public void EncodeLosslessPartitionSearchAcrossClippedSuperblocks(int width, int height, int effort)
|
|
{
|
|
ReadOnlySpan<int> period = [0, 28, 40, 28, 0, -28, -40, -12];
|
|
using Image<L8> source = new(width, height);
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
Span<L8> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
row[x] = new L8((byte)(128 + period[x % period.Length] + period[y % period.Length]));
|
|
}
|
|
}
|
|
|
|
// The repeated surface favors larger early leaves. Later clipped parents must still split from their
|
|
// own geometry instead of reading a stale position in the original fixed-eight partition preorder.
|
|
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv400);
|
|
colorConfig.ColorRange = true;
|
|
using MemoryStream stream = new();
|
|
Av1FrameEncoder.Encode(Configuration.Default, source.Frames.RootFrame, stream, colorConfig, qIndex: 0, effort);
|
|
byte[] payload = stream.ToArray();
|
|
string outputDirectory = TestEnvironment.CreateOutputDirectory("Heif", "Av1", nameof(this.EncodeLosslessPartitionSearchAcrossClippedSuperblocks));
|
|
string outputName = $"{width}x{height}-effort{effort}";
|
|
File.WriteAllBytes(Path.Combine(outputDirectory, outputName + ".obu"), payload);
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Av1FrameBuffer<byte> decoded = decoder.DecodeFrameBuffer(payload, null, null, out _);
|
|
Assert.Equal(width, decoded.Width);
|
|
Assert.Equal(height, decoded.Height);
|
|
Buffer2DRegion<byte> actual = decoded.DeriveBlockPointer(Av1Plane.Y, 0, 0);
|
|
using FileStream rawOutput = File.Create(Path.Combine(outputDirectory, outputName + ".source.yuv"));
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
ReadOnlySpan<byte> expectedRow = MemoryMarshal.AsBytes(source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y));
|
|
Assert.Equal(expectedRow, actual.DangerousGetRowSpan(y));
|
|
rawOutput.Write(expectedRow);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeEffortNineSelectsSubEightPartition()
|
|
{
|
|
const int Size = 16;
|
|
using Image<Rgba32> source = new(Size, Size);
|
|
for (int y = 0; y < Size; y++)
|
|
{
|
|
Span<Rgba32> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
for (int x = 0; x < Size; x++)
|
|
{
|
|
// The bottom-right 8x8 uses horizontal prediction on its left half and vertical prediction
|
|
// on its right half. Twelve source values keep a parent palette from reproducing both halves.
|
|
byte value;
|
|
if (x < 8 && y < 8)
|
|
{
|
|
value = 128;
|
|
}
|
|
else if (y < 8)
|
|
{
|
|
value = (byte)(16 + ((x - 8) * 20));
|
|
}
|
|
else
|
|
{
|
|
value = x < 12
|
|
? (byte)(176 + ((y - 8) * 9))
|
|
: (byte)(16 + ((x - 8) * 20));
|
|
}
|
|
|
|
row[x] = new Rgba32(value, value, value);
|
|
}
|
|
}
|
|
|
|
using MemoryStream stream = new();
|
|
_ = Av1FrameEncoder.Encode(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv400),
|
|
qIndex: 4,
|
|
effort: 9);
|
|
|
|
byte[] payload = stream.ToArray();
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
|
|
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
|
|
Point[] leafPositions =
|
|
[
|
|
new(2, 2),
|
|
new(3, 2),
|
|
new(2, 3),
|
|
new(3, 3)
|
|
];
|
|
|
|
foreach (Point leafPosition in leafPositions)
|
|
{
|
|
Assert.Equal(
|
|
Av1BlockSize.Block4x8,
|
|
frameInfo.GetModeInfoAt(leafPosition).BlockSize);
|
|
}
|
|
|
|
Assert.Equal(new Size(Size, Size), decoded.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeEffortNineSelectsSixteenBySixteenVerticalPartition()
|
|
{
|
|
const int Size = 32;
|
|
using Image<Rgba32> source = new(Size, Size);
|
|
for (int y = 0; y < Size; y++)
|
|
{
|
|
Span<Rgba32> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
for (int x = 0; x < Size; x++)
|
|
{
|
|
byte value = 128;
|
|
if (x == 15 && y >= 16)
|
|
{
|
|
value = (byte)(24 + ((y - 16) * 13));
|
|
}
|
|
else if (y == 15 && x >= 16)
|
|
{
|
|
value = (byte)(16 + ((x - 16) * 15));
|
|
}
|
|
else if (x >= 16 && y >= 16)
|
|
{
|
|
// The left 8x16 half repeats its external left edge, while the right half repeats
|
|
// its external top edge. One 16x16 predictor cannot reproduce both surfaces.
|
|
value = x < 24
|
|
? (byte)(24 + ((y - 16) * 13))
|
|
: (byte)(16 + ((x - 16) * 15));
|
|
}
|
|
|
|
row[x] = new Rgba32(value, value, value);
|
|
}
|
|
}
|
|
|
|
using MemoryStream stream = new();
|
|
_ = Av1FrameEncoder.Encode(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv400),
|
|
qIndex: 4,
|
|
effort: 9);
|
|
|
|
byte[] payload = stream.ToArray();
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
|
|
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
|
|
for (int modeInfoY = 4; modeInfoY < 8; modeInfoY++)
|
|
{
|
|
for (int modeInfoX = 4; modeInfoX < 8; modeInfoX++)
|
|
{
|
|
Assert.Equal(
|
|
Av1BlockSize.Block8x16,
|
|
frameInfo.GetModeInfoAt(new Point(modeInfoX, modeInfoY)).BlockSize);
|
|
}
|
|
}
|
|
|
|
Assert.Equal(new Size(Size, Size), decoded.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeEffortTenSelectsThirtyTwoByThirtyTwoBlocks()
|
|
{
|
|
const int Size = 32;
|
|
using Image<Rgba32> source = new(Size, Size);
|
|
for (int y = 0; y < Size; y++)
|
|
{
|
|
Span<Rgba32> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
for (int x = 0; x < Size; x++)
|
|
{
|
|
row[x] = new Rgba32(128, 128, 128);
|
|
}
|
|
}
|
|
|
|
using MemoryStream stream = new();
|
|
_ = Av1FrameEncoder.Encode(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv400),
|
|
qIndex: 4,
|
|
effort: 10);
|
|
|
|
byte[] payload = stream.ToArray();
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
|
|
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
|
|
for (int modeInfoY = 0; modeInfoY < 8; modeInfoY++)
|
|
{
|
|
for (int modeInfoX = 0; modeInfoX < 8; modeInfoX++)
|
|
{
|
|
Assert.Equal(
|
|
Av1BlockSize.Block32x32,
|
|
frameInfo.GetModeInfoAt(new Point(modeInfoX, modeInfoY)).BlockSize);
|
|
}
|
|
}
|
|
|
|
Assert.Equal(new Size(Size, Size), decoded.Size);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Yuv400)]
|
|
[InlineData(Yuv444)]
|
|
public void EncodeEffortTenSelectsSixtyFourBySixtyFourBlock(int colorFormatValue)
|
|
{
|
|
const int Size = 64;
|
|
Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue;
|
|
using Image<Rgba32> source = new(Size, Size);
|
|
for (int y = 0; y < Size; y++)
|
|
{
|
|
Span<Rgba32> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
for (int x = 0; x < Size; x++)
|
|
{
|
|
row[x] = new Rgba32(180, 64, 220);
|
|
}
|
|
}
|
|
|
|
using MemoryStream stream = new();
|
|
_ = Av1FrameEncoder.Encode(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
CreateColorConfig(Av1BitDepth.EightBit, colorFormat),
|
|
qIndex: 4,
|
|
effort: 10);
|
|
|
|
byte[] payload = stream.ToArray();
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
|
|
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
|
|
for (int modeInfoY = 0; modeInfoY < 16; modeInfoY++)
|
|
{
|
|
for (int modeInfoX = 0; modeInfoX < 16; modeInfoX++)
|
|
{
|
|
Assert.Equal(
|
|
Av1BlockSize.Block64x64,
|
|
frameInfo.GetModeInfoAt(new Point(modeInfoX, modeInfoY)).BlockSize);
|
|
}
|
|
}
|
|
|
|
Assert.Equal(new Size(Size, Size), decoded.Size);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Yuv400)]
|
|
[InlineData(Yuv444)]
|
|
public void EncodeEffortTenSelectsOneHundredTwentyEightByOneHundredTwentyEightBlock(int colorFormatValue)
|
|
{
|
|
const int Size = 128;
|
|
Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue;
|
|
using Image<Rgba32> source = new(Size, Size);
|
|
for (int y = 0; y < Size; y++)
|
|
{
|
|
Span<Rgba32> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
for (int x = 0; x < Size; x++)
|
|
{
|
|
row[x] = new Rgba32(180, 64, 220);
|
|
}
|
|
}
|
|
|
|
using MemoryStream stream = new();
|
|
ObuSequenceHeader sequenceHeader = Av1FrameEncoder.Encode(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
CreateColorConfig(Av1BitDepth.EightBit, colorFormat),
|
|
qIndex: 4,
|
|
effort: 10);
|
|
|
|
Assert.True(sequenceHeader.Use128x128Superblock);
|
|
byte[] payload = stream.ToArray();
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
|
|
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
|
|
for (int modeInfoY = 0; modeInfoY < 32; modeInfoY++)
|
|
{
|
|
for (int modeInfoX = 0; modeInfoX < 32; modeInfoX++)
|
|
{
|
|
Assert.Equal(
|
|
Av1BlockSize.Block128x128,
|
|
frameInfo.GetModeInfoAt(new Point(modeInfoX, modeInfoY)).BlockSize);
|
|
}
|
|
}
|
|
|
|
Assert.Equal(new Size(Size, Size), decoded.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeEffortTenSearchesHighBitDepthOneHundredTwentyEightRoot()
|
|
{
|
|
const int Size = 128;
|
|
using Image<Rgba32> source = new(Size, Size);
|
|
for (int y = 0; y < Size; y++)
|
|
{
|
|
Span<Rgba32> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
for (int x = 0; x < Size; x++)
|
|
{
|
|
row[x] = new Rgba32(180, 64, 220);
|
|
}
|
|
}
|
|
|
|
using MemoryStream stream = new();
|
|
ObuSequenceHeader sequenceHeader = Av1FrameEncoder.Encode(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
CreateColorConfig(Av1BitDepth.TwelveBit, Av1ColorFormat.Yuv444),
|
|
qIndex: 4,
|
|
effort: 10);
|
|
|
|
Assert.True(sequenceHeader.Use128x128Superblock);
|
|
byte[] payload = stream.ToArray();
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
|
|
Assert.Equal(new Size(Size, Size), decoded.Size);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(EightBit)]
|
|
[InlineData(TenBit)]
|
|
[InlineData(TwelveBit)]
|
|
public void EncodeAlphaWritesMonochromeReducedStillPicture(int bitDepthValue)
|
|
{
|
|
const int Width = 16;
|
|
const int Height = 16;
|
|
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
|
|
using Image<Rgba64> source = new(Width, Height);
|
|
for (int y = 0; y < Height; y++)
|
|
{
|
|
Span<Rgba64> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
for (int x = 0; x < Width; x++)
|
|
{
|
|
ushort alpha = (ushort)(((x + y) * ushort.MaxValue) / (Width + Height - 2));
|
|
row[x] = new Rgba64(ushort.MaxValue, 0, 0, alpha);
|
|
}
|
|
}
|
|
|
|
using MemoryStream stream = new();
|
|
ObuSequenceHeader encodedHeader = Av1FrameEncoder.EncodeAlpha(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
CreateColorConfig(bitDepth),
|
|
qIndex: 37,
|
|
effort: 5);
|
|
|
|
byte[] payload = stream.ToArray();
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Image<Rgba64> decoded = decoder.Decode<Rgba64>(payload);
|
|
|
|
Assert.True(encodedHeader.ColorConfig.IsMonochrome);
|
|
Assert.Equal(
|
|
bitDepth == Av1BitDepth.TwelveBit ? ObuSequenceProfile.Professional : ObuSequenceProfile.Main,
|
|
encodedHeader.SequenceProfile);
|
|
|
|
Assert.Equal(new Size(Width, Height), decoded.Size);
|
|
Assert.True(decoded[0, 0].R < decoded[Width - 1, Height - 1].R);
|
|
Assert.Equal(decoded[0, 0].R, decoded[0, 0].G);
|
|
Assert.Equal(decoded[0, 0].R, decoded[0, 0].B);
|
|
Assert.Equal(ushort.MaxValue, decoded[0, 0].A);
|
|
|
|
string outputDirectory = Path.Combine(
|
|
TestEnvironment.ActualOutputDirectoryFullPath,
|
|
"Formats",
|
|
"Heif",
|
|
"Av1");
|
|
|
|
Directory.CreateDirectory(outputDirectory);
|
|
File.WriteAllBytes(
|
|
Path.Combine(outputDirectory, $"encoder-alpha-{Width}x{Height}-{bitDepth.GetBitCount()}b.obu"),
|
|
payload);
|
|
}
|
|
|
|
[Fact]
|
|
public void AlphaConversionUsesOnePooledRowAndPreservesTwelveBitPrecision()
|
|
{
|
|
const int Width = 19;
|
|
const int Border = Av1EncoderFrame<ushort>.LumaBorder;
|
|
using Image<Rgba64> image = new(Width, 1);
|
|
ushort[] expected = new ushort[Width];
|
|
for (int x = 0; x < Width; x++)
|
|
{
|
|
ushort alpha = (ushort)((x * (long)ushort.MaxValue) / (Width - 1));
|
|
image[x, 0] = new Rgba64(0, 0, 0, alpha);
|
|
expected[x] = (ushort)(((alpha * 4095L) + (ushort.MaxValue / 2)) / ushort.MaxValue);
|
|
}
|
|
|
|
using Av1EncoderFrameBuffer<ushort> frameBuffer = new(
|
|
Configuration.Default,
|
|
Width,
|
|
1,
|
|
12,
|
|
Av1ColorFormat.Yuv400,
|
|
0,
|
|
0);
|
|
|
|
TestMemoryAllocator allocator = new();
|
|
allocator.EnableNonThreadSafeLogging();
|
|
Configuration configuration = Configuration.Default.Clone();
|
|
configuration.MemoryAllocator = allocator;
|
|
|
|
HeifPlanarAlphaEncoder.Convert<
|
|
Rgba64,
|
|
Av1EncoderFrame<ushort>.PlanarView,
|
|
ushort,
|
|
HeifUShortSampleConverter>(
|
|
configuration,
|
|
image.Frames.RootFrame,
|
|
frameBuffer.Frame.View);
|
|
|
|
frameBuffer.Frame.ExtendBorders();
|
|
|
|
AssertReplicatedSingleRow(frameBuffer.Luma, Border, expected);
|
|
TestMemoryAllocator.AllocationRequest allocation = Assert.Single(allocator.AllocationLog);
|
|
Assert.Equal(typeof(float), allocation.ElementType);
|
|
Assert.Equal(Width * 3, allocation.Length);
|
|
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
|
|
Assert.Equal(allocation.AllocationId, returned.AllocationId);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(EightBit, Yuv400, 0x1F, 0x1C)]
|
|
[InlineData(TenBit, Yuv420, 0x1F, 0x4C)]
|
|
[InlineData(TenBit, Yuv444, 0x3F, 0x40)]
|
|
[InlineData(TwelveBit, Yuv422, 0x5F, 0x68)]
|
|
public void CodecConfigurationWritesFixedHeaderFromEncodedSequenceHeader(
|
|
int bitDepthValue,
|
|
int colorFormatValue,
|
|
byte expectedProfileAndLevel,
|
|
byte expectedColorFlags)
|
|
{
|
|
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
|
|
Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue;
|
|
using Image<Rgba32> source = new(8, 8);
|
|
using MemoryStream stream = new();
|
|
ObuSequenceHeader sequenceHeader = Av1FrameEncoder.Encode(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
CreateColorConfig(bitDepth, colorFormat),
|
|
qIndex: 37,
|
|
effort: 5);
|
|
|
|
Av1CodecConfiguration configuration = new(sequenceHeader);
|
|
byte[] fixedHeader = new byte[Av1CodecConfiguration.FixedHeaderSize];
|
|
configuration.WriteFixedHeader(fixedHeader);
|
|
|
|
Assert.Equal([0x81, expectedProfileAndLevel, expectedColorFlags, 0x00], fixedHeader);
|
|
Av1CodecConfiguration parsed = new(fixedHeader, new DecoderOptions());
|
|
Assert.True(configuration.HasMatchingImageConfiguration(parsed));
|
|
parsed.Validate(sequenceHeader);
|
|
}
|
|
|
|
[Fact]
|
|
public void ScreenContentDetectorMatchesLibaomFeatureThresholds()
|
|
{
|
|
const int width = 160;
|
|
const int height = 16;
|
|
using Av1EncoderFrameBuffer<byte> byteFrame = new(
|
|
Configuration.Default,
|
|
width,
|
|
height,
|
|
8,
|
|
Av1ColorFormat.Yuv400,
|
|
0,
|
|
0);
|
|
|
|
for (int row = 0; row < height; row++)
|
|
{
|
|
Span<byte> samples = byteFrame.Frame.View.GetLumaRowSpan(row)[..width];
|
|
samples.Fill(96);
|
|
for (int column = 0; column < 16; column++)
|
|
{
|
|
samples[column] = column < 8 ? (byte)32 : (byte)224;
|
|
}
|
|
}
|
|
|
|
// One qualifying block is exactly ten percent of this frame, and the reference threshold is strict.
|
|
Assert.False(Av1ScreenContentDetector.IsPaletteLikely(byteFrame.Frame));
|
|
Av1ScreenContentDetector.Detect(
|
|
byteFrame.Frame,
|
|
out bool allowScreenContentTools,
|
|
out bool allowIntraBlockCopy);
|
|
|
|
Assert.False(allowScreenContentTools);
|
|
Assert.False(allowIntraBlockCopy);
|
|
for (int row = 0; row < height; row++)
|
|
{
|
|
Span<byte> samples = byteFrame.Frame.View.GetLumaRowSpan(row);
|
|
for (int column = 16; column < 32; column++)
|
|
{
|
|
samples[column] = column < 24 ? (byte)48 : (byte)208;
|
|
}
|
|
}
|
|
|
|
Assert.True(Av1ScreenContentDetector.IsPaletteLikely(byteFrame.Frame));
|
|
Av1ScreenContentDetector.Detect(
|
|
byteFrame.Frame,
|
|
out allowScreenContentTools,
|
|
out allowIntraBlockCopy);
|
|
|
|
Assert.True(allowScreenContentTools);
|
|
Assert.True(allowIntraBlockCopy);
|
|
using Av1EncoderFrameBuffer<ushort> highBitDepthFrame = new(
|
|
Configuration.Default,
|
|
16,
|
|
16,
|
|
10,
|
|
Av1ColorFormat.Yuv400,
|
|
0,
|
|
0);
|
|
|
|
for (int row = 0; row < 16; row++)
|
|
{
|
|
Span<ushort> samples = highBitDepthFrame.Frame.View.GetLumaRowSpan(row);
|
|
for (int column = 0; column < 16; column++)
|
|
{
|
|
samples[column] = column < 8 ? (ushort)128 : (ushort)131;
|
|
}
|
|
}
|
|
|
|
Assert.False(Av1ScreenContentDetector.IsPaletteLikely(highBitDepthFrame.Frame));
|
|
Av1ScreenContentDetector.Detect(
|
|
highBitDepthFrame.Frame,
|
|
out allowScreenContentTools,
|
|
out allowIntraBlockCopy);
|
|
|
|
Assert.False(allowScreenContentTools);
|
|
Assert.False(allowIntraBlockCopy);
|
|
for (int row = 0; row < 16; row++)
|
|
{
|
|
Span<ushort> samples = highBitDepthFrame.Frame.View.GetLumaRowSpan(row);
|
|
samples[8..16].Fill(640);
|
|
}
|
|
|
|
Assert.True(Av1ScreenContentDetector.IsPaletteLikely(highBitDepthFrame.Frame));
|
|
Av1ScreenContentDetector.Detect(
|
|
highBitDepthFrame.Frame,
|
|
out allowScreenContentTools,
|
|
out allowIntraBlockCopy);
|
|
|
|
Assert.True(allowScreenContentTools);
|
|
Assert.True(allowIntraBlockCopy);
|
|
for (int row = 0; row < 16; row++)
|
|
{
|
|
Span<ushort> samples = highBitDepthFrame.Frame.View.GetLumaRowSpan(row);
|
|
for (int column = 0; column < 16; column++)
|
|
{
|
|
samples[column] = (ushort)((column % 5) * 200);
|
|
}
|
|
}
|
|
|
|
Assert.False(Av1ScreenContentDetector.IsPaletteLikely(highBitDepthFrame.Frame));
|
|
Av1ScreenContentDetector.Detect(
|
|
highBitDepthFrame.Frame,
|
|
out allowScreenContentTools,
|
|
out allowIntraBlockCopy);
|
|
|
|
Assert.False(allowScreenContentTools);
|
|
Assert.False(allowIntraBlockCopy);
|
|
}
|
|
|
|
[Fact]
|
|
public void ScreenContentDetectorMatchesLibaomIntraBlockCopyVarianceThreshold()
|
|
{
|
|
const int Width = 16;
|
|
const int Height = 16;
|
|
using Av1EncoderFrameBuffer<byte> frame = new(
|
|
Configuration.Default,
|
|
Width,
|
|
Height,
|
|
8,
|
|
Av1ColorFormat.Yuv400,
|
|
0,
|
|
0);
|
|
|
|
Buffer2DRegion<byte> luma = frame.Frame.View.GetPlane(Av1Plane.Y);
|
|
for (int row = 0; row < Height; row++)
|
|
{
|
|
luma.DangerousGetRowSpan(row).Fill(96);
|
|
}
|
|
|
|
// A single delta of eleven leaves total variance below half a sample after per-pixel rounding.
|
|
luma.DangerousGetRowSpan(0)[0] = 107;
|
|
Av1ScreenContentDetector.Detect(
|
|
frame.Frame,
|
|
out bool allowScreenContentTools,
|
|
out bool allowIntraBlockCopy);
|
|
|
|
Assert.True(allowScreenContentTools);
|
|
Assert.False(allowIntraBlockCopy);
|
|
|
|
// Raising that delta to twelve crosses the exact integer rounding boundary used by libaom.
|
|
luma.DangerousGetRowSpan(0)[0] = 108;
|
|
Av1ScreenContentDetector.Detect(
|
|
frame.Frame,
|
|
out allowScreenContentTools,
|
|
out allowIntraBlockCopy);
|
|
|
|
Assert.True(allowScreenContentTools);
|
|
Assert.True(allowIntraBlockCopy);
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeActivatesScreenContentTools()
|
|
{
|
|
const int width = 16;
|
|
const int height = 16;
|
|
using Image<Rgba32> source = new(width, height);
|
|
for (int row = 0; row < height; row++)
|
|
{
|
|
Span<Rgba32> pixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
|
|
for (int column = 0; column < width; column++)
|
|
{
|
|
pixels[column] = (((column >> 2) + (row >> 2)) & 1) == 0
|
|
? new Rgba32(224, 32, 32)
|
|
: new Rgba32(32, 32, 224);
|
|
}
|
|
}
|
|
|
|
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv444);
|
|
using MemoryStream stream = new();
|
|
_ = Av1FrameEncoder.Encode(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
colorConfig,
|
|
qIndex: 37,
|
|
effort: 5);
|
|
|
|
byte[] payload = stream.ToArray();
|
|
Av1BitStreamReader reader = new(payload);
|
|
Av1TileDecoderStub tileReader = new();
|
|
ObuReader obuReader = new();
|
|
obuReader.ReadAll(ref reader, payload.Length, () => tileReader);
|
|
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(obuReader.FrameHeader);
|
|
Assert.True(frameHeader.AllowScreenContentTools);
|
|
Assert.True(frameHeader.AllowIntraBlockCopy);
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
|
|
Assert.Equal(new Size(width, height), decoded.Size);
|
|
|
|
string outputDirectory = Path.Combine(
|
|
TestEnvironment.ActualOutputDirectoryFullPath,
|
|
"Formats",
|
|
"Heif",
|
|
"Av1");
|
|
|
|
Directory.CreateDirectory(outputDirectory);
|
|
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-frame-16x16-8b-444-palette.obu"), payload);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, false, false, false)]
|
|
[InlineData(1, false, false, false)]
|
|
[InlineData(2, false, false, false)]
|
|
[InlineData(3, false, false, false)]
|
|
[InlineData(4, true, false, false)]
|
|
[InlineData(5, true, true, false)]
|
|
[InlineData(6, true, true, true)]
|
|
[InlineData(7, true, true, true)]
|
|
[InlineData(8, true, true, true)]
|
|
[InlineData(10, true, true, true)]
|
|
public void EncodeEffortControlsSearchFeatures(
|
|
int effort,
|
|
bool enableFilterIntra,
|
|
bool enableScreenContentTools,
|
|
bool selectTransformSize)
|
|
{
|
|
const int width = 16;
|
|
const int height = 16;
|
|
using Image<Rgba32> source = new(width, height);
|
|
for (int row = 0; row < height; row++)
|
|
{
|
|
Span<Rgba32> pixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
|
|
for (int column = 0; column < width; column++)
|
|
{
|
|
pixels[column] = (((column >> 2) + (row >> 2)) & 1) == 0
|
|
? new Rgba32(224, 32, 32)
|
|
: new Rgba32(32, 32, 224);
|
|
}
|
|
}
|
|
|
|
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv444);
|
|
using MemoryStream stream = new();
|
|
_ = Av1FrameEncoder.Encode(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
colorConfig,
|
|
qIndex: 37,
|
|
effort);
|
|
|
|
byte[] payload = stream.ToArray();
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
|
|
ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(decoder.SequenceHeader);
|
|
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader);
|
|
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
|
|
Assert.Equal(enableFilterIntra, sequenceHeader.EnableFilterIntra);
|
|
Assert.Equal(enableScreenContentTools, frameHeader.AllowScreenContentTools);
|
|
Assert.Equal(enableScreenContentTools, frameHeader.AllowIntraBlockCopy);
|
|
Assert.Equal(
|
|
selectTransformSize ? Av1TransformMode.Select : Av1TransformMode.Largest,
|
|
frameHeader.TransformMode);
|
|
Assert.Equal(new Size(width, height), decoded.Size);
|
|
|
|
int modeCount = 0;
|
|
foreach (Av1BlockModeInfo modeInfo in frameInfo.GetSuperblock(Point.Empty).GetModeInfos())
|
|
{
|
|
modeCount++;
|
|
if (effort == 0)
|
|
{
|
|
Assert.Equal(Av1PredictionMode.DC, modeInfo.YMode);
|
|
Assert.Equal(Av1ChromaPredictionMode.DC, modeInfo.UvMode);
|
|
}
|
|
|
|
if (effort <= 1)
|
|
{
|
|
Assert.Equal(0, modeInfo.GetAngleDelta(Av1Plane.Y));
|
|
Assert.Equal(0, modeInfo.GetAngleDelta(Av1Plane.U));
|
|
}
|
|
|
|
if (effort < 4)
|
|
{
|
|
Assert.False(modeInfo.UseFilterIntra);
|
|
}
|
|
|
|
if (effort < 5)
|
|
{
|
|
Assert.False(modeInfo.UseIntraBlockCopy);
|
|
Assert.Equal(0, modeInfo.GetPaletteSize(Av1Plane.Y));
|
|
Assert.Equal(0, modeInfo.GetPaletteSize(Av1Plane.U));
|
|
}
|
|
}
|
|
|
|
Assert.NotEqual(0, modeCount);
|
|
|
|
string outputDirectory = Path.Combine(
|
|
TestEnvironment.ActualOutputDirectoryFullPath,
|
|
"Formats",
|
|
"Heif",
|
|
"Av1");
|
|
|
|
Directory.CreateDirectory(outputDirectory);
|
|
File.WriteAllBytes(Path.Combine(outputDirectory, $"encoder-frame-16x16-8b-444-effort-{effort}.obu"), payload);
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeEffortSixSelectsFourByFourLumaTransforms()
|
|
{
|
|
const int Width = 16;
|
|
const int Height = 16;
|
|
using Image<Rgba32> source = new(Width, Height);
|
|
for (int row = 0; row < Height; row++)
|
|
{
|
|
Span<Rgba32> pixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
|
|
for (int column = 0; column < Width; column++)
|
|
{
|
|
byte value = (byte)(16 + ((((row >> 2) * 4) + (column >> 2)) * 14));
|
|
pixels[column] = new Rgba32(value, value, value);
|
|
}
|
|
}
|
|
|
|
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv400);
|
|
using MemoryStream stream = new();
|
|
_ = Av1FrameEncoder.Encode(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
colorConfig,
|
|
qIndex: 37,
|
|
effort: 6);
|
|
|
|
byte[] payload = stream.ToArray();
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Image<L8> decoded = decoder.Decode<L8>(payload);
|
|
Assert.NotNull(decoder.FrameHeader);
|
|
Assert.Equal(Av1TransformMode.Select, decoder.FrameHeader.TransformMode);
|
|
Assert.NotNull(decoder.FrameInfo);
|
|
bool foundSplitTransform = false;
|
|
foreach (Av1BlockModeInfo modeInfo in decoder.FrameInfo.GetSuperblock(Point.Empty).GetModeInfos())
|
|
{
|
|
foundSplitTransform |= modeInfo.GetTransformUnitCount(Av1Plane.Y) == 4;
|
|
}
|
|
|
|
Assert.True(foundSplitTransform);
|
|
Assert.Equal(new Size(Width, Height), decoded.Size);
|
|
|
|
string outputDirectory = Path.Combine(
|
|
TestEnvironment.ActualOutputDirectoryFullPath,
|
|
"Formats",
|
|
"Heif",
|
|
"Av1");
|
|
|
|
Directory.CreateDirectory(outputDirectory);
|
|
File.WriteAllBytes(
|
|
Path.Combine(outputDirectory, "encoder-frame-16x16-8b-400-transform-size-select.obu"),
|
|
payload);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(5, false)]
|
|
[InlineData(6, true)]
|
|
public void EncodeSelectsIntraBlockCopyForRepeatedScreenContent(
|
|
int effort,
|
|
bool selectTransformSize)
|
|
{
|
|
const int Width = 328;
|
|
const int Height = 16;
|
|
const ulong Pattern = 0xD6A5_3C97_E18B_4F20UL;
|
|
using Image<Rgba32> source = new(Width, Height);
|
|
for (int row = 0; row < Height; row++)
|
|
{
|
|
Span<Rgba32> pixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
|
|
for (int column = 0; column < Width; column++)
|
|
{
|
|
int patternIndex = ((row & 7) * 8) + (column & 7);
|
|
pixels[column] = ((Pattern >> patternIndex) & 1) == 0
|
|
? new Rgba32(224, 32, 32)
|
|
: new Rgba32(32, 32, 224);
|
|
}
|
|
}
|
|
|
|
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv444);
|
|
using MemoryStream stream = new();
|
|
_ = Av1FrameEncoder.Encode(
|
|
Configuration.Default,
|
|
source.Frames.RootFrame,
|
|
stream,
|
|
colorConfig,
|
|
qIndex: 37,
|
|
effort);
|
|
|
|
byte[] payload = stream.ToArray();
|
|
using Av1Decoder decoder = new(Configuration.Default);
|
|
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
|
|
Assert.NotNull(decoder.FrameHeader);
|
|
Assert.True(decoder.FrameHeader.AllowScreenContentTools);
|
|
Assert.True(decoder.FrameHeader.AllowIntraBlockCopy);
|
|
Assert.Equal(
|
|
selectTransformSize ? Av1TransformMode.Select : Av1TransformMode.Largest,
|
|
decoder.FrameHeader.TransformMode);
|
|
Assert.NotNull(decoder.FrameInfo);
|
|
Av1SuperblockInfo targetSuperblock = decoder.FrameInfo.GetSuperblock(new Point(5, 0));
|
|
bool usesIntraBlockCopy = false;
|
|
foreach (Av1BlockModeInfo modeInfo in targetSuperblock.GetModeInfos())
|
|
{
|
|
usesIntraBlockCopy |= modeInfo.UseIntraBlockCopy;
|
|
}
|
|
|
|
Assert.True(usesIntraBlockCopy);
|
|
Assert.Equal(new Size(Width, Height), decoded.Size);
|
|
|
|
string outputDirectory = Path.Combine(
|
|
TestEnvironment.ActualOutputDirectoryFullPath,
|
|
"Formats",
|
|
"Heif",
|
|
"Av1");
|
|
|
|
Directory.CreateDirectory(outputDirectory);
|
|
string fileName = effort == 5
|
|
? "encoder-frame-328x16-8b-444-intrabc.obu"
|
|
: "encoder-frame-328x16-8b-444-intrabc-effort-6.obu";
|
|
|
|
File.WriteAllBytes(Path.Combine(outputDirectory, fileName), payload);
|
|
}
|
|
|
|
[Fact]
|
|
public void PrepareSourceConvertsRgba32DirectlyIntoBorderedEightBitPlane()
|
|
{
|
|
const int width = 4;
|
|
const int height = 1;
|
|
const int border = Av1EncoderFrame<byte>.LumaBorder;
|
|
|
|
using Image<Rgba32> image = new(width, height);
|
|
image[0, 0] = new Rgba32(byte.MaxValue, 0, 0, 0);
|
|
image[1, 0] = new Rgba32(0, byte.MaxValue, 0);
|
|
image[2, 0] = new Rgba32(0, 0, byte.MaxValue);
|
|
image[3, 0] = new Rgba32(byte.MaxValue, byte.MaxValue, byte.MaxValue);
|
|
|
|
using Av1EncoderFrameBuffer<byte> frameBuffer = new(
|
|
Configuration.Default,
|
|
width,
|
|
height,
|
|
8,
|
|
Av1ColorFormat.Yuv400,
|
|
0,
|
|
0);
|
|
|
|
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit);
|
|
|
|
Av1FrameEncoder.PrepareSource(Configuration.Default, image.Frames.RootFrame, frameBuffer.Frame, colorConfig);
|
|
|
|
byte[] expected = [76, 150, 29, 255];
|
|
AssertReplicatedSingleRow(frameBuffer.Luma, border, expected);
|
|
}
|
|
|
|
[Fact]
|
|
public void PrepareSourcePreservesHighBitDepthPrecision()
|
|
{
|
|
const int width = 4;
|
|
const int height = 1;
|
|
const int border = Av1EncoderFrame<ushort>.LumaBorder;
|
|
|
|
using Image<Rgba64> image = new(width, height);
|
|
image[0, 0] = new Rgba64(ushort.MaxValue, 0, 0, 0);
|
|
image[1, 0] = new Rgba64(0, ushort.MaxValue, 0, ushort.MaxValue);
|
|
image[2, 0] = new Rgba64(0, 0, ushort.MaxValue, ushort.MaxValue);
|
|
image[3, 0] = new Rgba64(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue, ushort.MaxValue);
|
|
|
|
using Av1EncoderFrameBuffer<ushort> frameBuffer = new(
|
|
Configuration.Default,
|
|
width,
|
|
height,
|
|
10,
|
|
Av1ColorFormat.Yuv400,
|
|
0,
|
|
0);
|
|
|
|
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.TenBit);
|
|
|
|
Av1FrameEncoder.PrepareSource(Configuration.Default, image.Frames.RootFrame, frameBuffer.Frame, colorConfig);
|
|
|
|
ushort[] expected = [306, 601, 117, 1023];
|
|
AssertReplicatedSingleRow(frameBuffer.Luma, border, expected);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExtendBordersReplicatesEveryPhysicalPlaneEdge()
|
|
{
|
|
const int visibleWidth = 5;
|
|
const int visibleHeight = 3;
|
|
const int lumaBorder = Av1EncoderFrame<byte>.LumaBorder;
|
|
const int chromaBorder = lumaBorder / 2;
|
|
|
|
using Av1EncoderFrameBuffer<byte> frameBuffer = new(
|
|
Configuration.Default,
|
|
visibleWidth,
|
|
visibleHeight,
|
|
8,
|
|
Av1ColorFormat.Yuv420,
|
|
1,
|
|
1);
|
|
|
|
Buffer2D<byte> luma = frameBuffer.Luma;
|
|
Buffer2D<byte> chromaBlue = Assert.IsType<Buffer2D<byte>>(frameBuffer.ChromaBlue);
|
|
Buffer2D<byte> chromaRed = Assert.IsType<Buffer2D<byte>>(frameBuffer.ChromaRed);
|
|
|
|
FillVisible(luma, lumaBorder, lumaBorder, visibleWidth, visibleHeight, 10);
|
|
FillVisible(chromaBlue, chromaBorder, chromaBorder, (visibleWidth + 1) / 2, (visibleHeight + 1) / 2, 80);
|
|
FillVisible(chromaRed, chromaBorder, chromaBorder, (visibleWidth + 1) / 2, (visibleHeight + 1) / 2, 120);
|
|
|
|
frameBuffer.Frame.ExtendBorders();
|
|
|
|
AssertReplicatedPlane(luma, lumaBorder, lumaBorder, visibleWidth, visibleHeight, 10);
|
|
AssertReplicatedPlane(chromaBlue, chromaBorder, chromaBorder, (visibleWidth + 1) / 2, (visibleHeight + 1) / 2, 80);
|
|
AssertReplicatedPlane(chromaRed, chromaBorder, chromaBorder, (visibleWidth + 1) / 2, (visibleHeight + 1) / 2, 120);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(5, 3, 0, 0, 160, 136)]
|
|
[InlineData(5, 3, 1, 0, 80, 136)]
|
|
[InlineData(5, 3, 1, 1, 80, 68)]
|
|
[InlineData(1921, 1081, 0, 0, 2080, 1216)]
|
|
[InlineData(1921, 1081, 1, 1, 1040, 608)]
|
|
public void GetPlaneBufferSizeMatchesLibaomLayout(
|
|
int width,
|
|
int height,
|
|
int subsamplingX,
|
|
int subsamplingY,
|
|
int expectedWidth,
|
|
int expectedHeight)
|
|
{
|
|
Size actual = Av1EncoderFrame<byte>.GetPlaneBufferSize(width, height, subsamplingX, subsamplingY);
|
|
|
|
Assert.Equal(new Size(expectedWidth, expectedHeight), actual);
|
|
}
|
|
|
|
[Fact]
|
|
public void FrameBufferUsesOneExactSizeOwnerForAllPlanes()
|
|
{
|
|
TestMemoryAllocator allocator = new();
|
|
allocator.EnableNonThreadSafeLogging();
|
|
Configuration configuration = Configuration.Default.Clone();
|
|
configuration.MemoryAllocator = allocator;
|
|
|
|
TestMemoryAllocator.AllocationRequest allocation;
|
|
using (Av1EncoderFrameBuffer<byte> frameBuffer = new(
|
|
configuration,
|
|
64,
|
|
64,
|
|
8,
|
|
Av1ColorFormat.Yuv420,
|
|
1,
|
|
1))
|
|
{
|
|
allocation = Assert.Single(allocator.AllocationLog);
|
|
Assert.Empty(allocator.ReturnLog);
|
|
Assert.Equal(typeof(byte), allocation.ElementType);
|
|
Assert.Equal(55_296, allocation.Length);
|
|
Assert.Single(frameBuffer.Luma.MemoryGroup);
|
|
Assert.Single(Assert.IsType<Buffer2D<byte>>(frameBuffer.ChromaBlue).MemoryGroup);
|
|
Assert.Single(Assert.IsType<Buffer2D<byte>>(frameBuffer.ChromaRed).MemoryGroup);
|
|
}
|
|
|
|
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
|
|
Assert.Equal(allocation.AllocationId, returned.AllocationId);
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeReturnsEveryOperationAllocationAndUsesOneLibaomSizedTileReservation()
|
|
{
|
|
const int Width = 64;
|
|
const int Height = 64;
|
|
const int ExpectedTileOutputLength = 60 * 1024;
|
|
|
|
using Image<Rgba32> source = new(Width, Height);
|
|
for (int y = 0; y < Height; y++)
|
|
{
|
|
Span<Rgba32> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
for (int x = 0; x < Width; x++)
|
|
{
|
|
row[x] = new Rgba32(
|
|
(byte)((x * 3) + y),
|
|
(byte)(x + (y * 5)),
|
|
(byte)((x * 7) + (y * 11)));
|
|
}
|
|
}
|
|
|
|
TestMemoryAllocator allocator = new();
|
|
allocator.EnableNonThreadSafeLogging();
|
|
Configuration configuration = Configuration.Default.Clone();
|
|
configuration.MemoryAllocator = allocator;
|
|
using MemoryStream storage = new();
|
|
using NonSeekableStream destination = new(storage);
|
|
|
|
_ = Av1FrameEncoder.Encode(
|
|
configuration,
|
|
source.Frames.RootFrame,
|
|
destination,
|
|
CreateColorConfig(Av1BitDepth.TwelveBit, Av1ColorFormat.Yuv444),
|
|
qIndex: 37,
|
|
effort: 5);
|
|
|
|
Assert.False(destination.CanSeek);
|
|
Assert.NotEqual(0, storage.Length);
|
|
TestMemoryAllocator.AllocationRequest tileOutput = Assert.Single(
|
|
allocator.AllocationLog,
|
|
allocation => allocation.ElementType == typeof(byte) && allocation.Length == ExpectedTileOutputLength);
|
|
|
|
Assert.Equal(ExpectedTileOutputLength, tileOutput.Length);
|
|
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
|
|
Assert.Equal(
|
|
allocator.AllocationLog.Select(allocation => allocation.AllocationId).Order(),
|
|
allocator.ReturnLog.Select(returned => returned.AllocationId).Order());
|
|
}
|
|
|
|
private static ObuColorConfig CreateColorConfig(
|
|
Av1BitDepth bitDepth,
|
|
Av1ColorFormat colorFormat = Av1ColorFormat.Yuv400)
|
|
=> new()
|
|
{
|
|
IsColorDescriptionPresent = true,
|
|
IsMonochrome = colorFormat == Av1ColorFormat.Yuv400,
|
|
ColorPrimaries = ObuColorPrimaries.Bt601,
|
|
TransferCharacteristics = ObuTransferCharacteristics.Bt601,
|
|
MatrixCoefficients = ObuMatrixCoefficients.Bt601,
|
|
ColorRange = true,
|
|
SubSamplingX = colorFormat != Av1ColorFormat.Yuv444,
|
|
SubSamplingY = colorFormat == Av1ColorFormat.Yuv400 || colorFormat == Av1ColorFormat.Yuv420,
|
|
ChromaSamplePosition = ObuChromoSamplePosition.Unknown,
|
|
BitDepth = bitDepth
|
|
};
|
|
|
|
private static void FillVisible(Buffer2D<byte> plane, int originX, int originY, int width, int height, int seed)
|
|
{
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
Span<byte> row = plane.DangerousGetRowSpan(originY + y);
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
row[originX + x] = (byte)(seed + (y * width) + x);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void AssertReplicatedPlane(Buffer2D<byte> plane, int originX, int originY, int width, int height, int seed)
|
|
{
|
|
for (int y = 0; y < plane.Height; y++)
|
|
{
|
|
ReadOnlySpan<byte> row = plane.DangerousGetRowSpan(y);
|
|
int sourceY = Math.Clamp(y - originY, 0, height - 1);
|
|
for (int x = 0; x < row.Length; x++)
|
|
{
|
|
int sourceX = Math.Clamp(x - originX, 0, width - 1);
|
|
Assert.Equal((byte)(seed + (sourceY * width) + sourceX), row[x]);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void AssertReplicatedSingleRow<TSample>(
|
|
Buffer2D<TSample> plane,
|
|
int originX,
|
|
ReadOnlySpan<TSample> expected)
|
|
where TSample : unmanaged, IEquatable<TSample>
|
|
{
|
|
for (int y = 0; y < plane.Height; y++)
|
|
{
|
|
ReadOnlySpan<TSample> row = plane.DangerousGetRowSpan(y);
|
|
for (int x = 0; x < row.Length; x++)
|
|
{
|
|
int sourceX = Math.Clamp(x - originX, 0, expected.Length - 1);
|
|
Assert.Equal(expected[sourceX], row[x]);
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class FailingSequenceAllocator : TestMemoryAllocator
|
|
{
|
|
private readonly int failureIndex;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="FailingSequenceAllocator"/> class.
|
|
/// </summary>
|
|
/// <param name="failureIndex">The zero-based allocation request that fails.</param>
|
|
public FailingSequenceAllocator(int failureIndex)
|
|
{
|
|
this.failureIndex = failureIndex;
|
|
this.EnableNonThreadSafeLogging();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override AllocationTrackedMemoryManager<T> AllocateCore<T>(int length, AllocationOptions options)
|
|
{
|
|
if (this.AllocationLog.Count == this.failureIndex)
|
|
{
|
|
throw new InvalidMemoryOperationException("Sequence allocation failure.");
|
|
}
|
|
|
|
return base.AllocateCore<T>(length, options);
|
|
}
|
|
}
|
|
}
|
|
|