Browse Source

Add retained AV1 palette state

pull/2633/head
James Jackson-South 1 month ago
parent
commit
b33e207014
  1. 5
      HEIF_IMPLEMENTATION_PLAN.md
  2. 3
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs
  3. 23
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockStruct.cs
  4. 75
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteInfo.cs
  5. 62
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteMapBuffer.cs
  6. 50
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPictureBuffer.cs
  7. 32
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderSuperblockWorkspace.cs
  8. 66
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PaletteCache.cs
  9. 5
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PictureControlSet.cs
  10. 52
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
  11. 7
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.BlockEncoding.cs
  12. 200
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs
  13. 163
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs
  14. 33
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderModeInfoBufferTests.cs
  15. 12
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
  16. 184
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs

5
HEIF_IMPLEMENTATION_PLAN.md

File diff suppressed because one or more lines are too long

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

@ -157,7 +157,8 @@ internal static partial class Av1IntraSuperblockEncoder
Point blockOrigin,
ushort tileIndex,
ref Av1MacroBlockModeInfo modeInfo,
ref Av1EncoderBlockStruct block)
ref Av1EncoderBlockStruct block,
ref Av1EncoderPaletteInfo paletteInfo)
{
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
const Av1TransformSize LumaTransformSize = Av1TransformSize.Size8x8;

23
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockStruct.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -16,12 +15,7 @@ internal struct Av1EncoderBlockStruct
/// <summary>
/// The fixed byte width of one packed final-block decision.
/// </summary>
public const int StorageSize = 10;
/// <summary>
/// Stores the luma and shared chroma palette sizes inline with the block.
/// </summary>
private InlineArray2<byte> paletteSize;
public const int StorageSize = 8;
/// <summary>
/// Stores the block's prediction-unit syntax inline.
@ -71,24 +65,9 @@ internal struct Av1EncoderBlockStruct
set => this.filterIntraMode = (byte)value;
}
/// <summary>
/// Gets the writable palette sizes for luma and for the shared chroma mode.
/// </summary>
[UnscopedRef]
public Span<byte> PaletteSize => this.paletteSize;
/// <summary>
/// Gets the encoder prediction-unit state for the block.
/// </summary>
[UnscopedRef]
public ref Av1EncoderPredictionUnit PredictionUnit => ref this.predictionUnit;
/// <summary>
/// Stores the two palette-size values embedded by libaom in block mode information.
/// </summary>
[InlineArray(2)]
private struct InlineArray2<T>
{
private T element;
}
}

75
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteInfo.cs

@ -0,0 +1,75 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
/// Stores the selected luma and chroma palette sizes and colors for one encoder block.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = StorageSize)]
internal struct Av1EncoderPaletteInfo
{
/// <summary>
/// The packed size of two palette sizes and three eight-color planes.
/// </summary>
public const int StorageSize = 50;
private InlineArray2<byte> paletteSizes;
private InlineArray24<ushort> paletteColors;
/// <summary>
/// Gets the writable luma and shared chroma palette sizes.
/// </summary>
[UnscopedRef]
public Span<byte> PaletteSizes => this.paletteSizes;
/// <summary>
/// Gets the selected colors for one color plane.
/// </summary>
/// <param name="plane">The color plane.</param>
/// <returns>The selected colors in palette-index order.</returns>
[UnscopedRef]
public Span<ushort> GetColors(Av1Plane plane)
{
int planeIndex = (int)plane;
int paletteSize = this.paletteSizes[planeIndex == 0 ? 0 : 1];
Span<ushort> colors = this.paletteColors;
return colors.Slice(planeIndex * Av1Constants.PaletteMaxSize, paletteSize);
}
/// <summary>
/// Stores the selected colors for one color plane.
/// </summary>
/// <param name="plane">The color plane.</param>
/// <param name="colors">The colors in palette-index order.</param>
public void SetColors(Av1Plane plane, ReadOnlySpan<ushort> colors)
{
int offset = (int)plane * Av1Constants.PaletteMaxSize;
Span<ushort> destination = this.paletteColors;
colors.CopyTo(destination[offset..]);
}
/// <summary>
/// Provides fixed storage for the luma and shared chroma palette sizes.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(2)]
private struct InlineArray2<T>
{
private T element;
}
/// <summary>
/// Provides fixed storage for all three eight-color palette planes.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(3 * Av1Constants.PaletteMaxSize)]
private struct InlineArray24<T>
{
private T element;
}
}

62
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteMapBuffer.cs

@ -0,0 +1,62 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
/// Owns the reusable luma and chroma palette color-index maps for encoder block decisions.
/// </summary>
internal sealed class Av1EncoderPaletteMapBuffer : IDisposable
{
/// <summary>
/// The width and height of each maximum-superblock map.
/// </summary>
public const int MapLength = 1 << Av1Constants.MaxSuperBlockSizeLog2;
/// <summary>
/// The combined byte length of the luma and chroma maps.
/// </summary>
public const int StorageLength = 2 * MapLength * MapLength;
private readonly IMemoryOwner<byte> owner;
private readonly Buffer2D<byte> luma;
private readonly Buffer2D<byte> chroma;
/// <summary>
/// Initializes a new instance of the <see cref="Av1EncoderPaletteMapBuffer"/> class.
/// </summary>
/// <param name="configuration">The configuration providing the encoder allocator.</param>
public Av1EncoderPaletteMapBuffer(Configuration configuration)
{
this.owner = configuration.MemoryAllocator.Allocate<byte>(StorageLength);
int mapArea = MapLength * MapLength;
Memory<byte> storage = this.owner.Memory[..StorageLength];
this.luma = Buffer2D<byte>.WrapMemory(storage[..mapArea], MapLength, MapLength);
this.chroma = Buffer2D<byte>.WrapMemory(storage[mapArea..], MapLength, MapLength);
}
/// <summary>
/// Gets a block-sized view of the luma or shared chroma color-index map.
/// </summary>
/// <param name="planeType">The luma or shared chroma plane class.</param>
/// <param name="width">The padded plane-block width.</param>
/// <param name="height">The padded plane-block height.</param>
/// <returns>The reusable map region beginning at the workspace origin.</returns>
public Buffer2DRegion<byte> GetMap(Av1PlaneType planeType, int width, int height)
=> new(
planeType == Av1PlaneType.Y ? this.luma : this.chroma,
new Rectangle(0, 0, width, height));
/// <summary>
/// Returns the shared palette-map owner to the configured allocator.
/// </summary>
public void Dispose()
{
this.luma.Dispose();
this.chroma.Dispose();
this.owner.Dispose();
}
}

50
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPictureBuffer.cs

@ -21,6 +21,7 @@ internal sealed class Av1EncoderPictureBuffer : IDisposable
private readonly Av1NeighborArrayUnit<byte>[] blueCoefficientContexts;
private readonly Av1NeighborArrayUnit<byte>[] redCoefficientContexts;
private readonly Av1NeighborArrayUnit<byte>[] transformContexts;
private readonly Av1NeighborArrayUnit<Av1EncoderPaletteInfo>[] paletteContexts;
/// <summary>
/// Initializes a new instance of the <see cref="Av1EncoderPictureBuffer"/> class.
@ -70,7 +71,20 @@ internal sealed class Av1EncoderPictureBuffer : IDisposable
partitionContextLength * Unsafe.SizeOf<Av1PartitionContext>());
int byteContextStorageOffset = checked(partitionStorageOffset + partitionStorageLength);
int stateStorageLength = checked(byteContextStorageOffset + (tileCount * byteContextLengthPerTile));
int byteContextStorageLength = checked(tileCount * byteContextLengthPerTile);
int byteContextStorageEnd = checked(byteContextStorageOffset + byteContextStorageLength);
int paletteLeftLength = alignedModeInfoRowCount;
int paletteTopLength = this.modeInfo.ModeInfoStride;
int paletteContextLength = checked(paletteLeftLength + paletteTopLength);
int paletteStorageOffset = frameHeader.AllowScreenContentTools
? Av1Math.AlignPowerOf2(byteContextStorageEnd, 1)
: byteContextStorageEnd;
int paletteStorageLength = frameHeader.AllowScreenContentTools
? checked(tileCount * paletteContextLength * Unsafe.SizeOf<Av1EncoderPaletteInfo>())
: 0;
int stateStorageLength = checked(paletteStorageOffset + paletteStorageLength);
// Segmentation and every tile edge share one clean picture lifetime. The partition region begins at its
// native alignment, while typed views keep the entropy writer independent from the packed byte owner.
@ -83,12 +97,27 @@ internal sealed class Av1EncoderPictureBuffer : IDisposable
stateStorage.Slice(partitionStorageOffset, partitionStorageLength));
Memory<Av1PartitionContext> partitionStorage = this.partitionContextMemory.Memory;
Memory<byte> byteContextStorage = stateStorage[byteContextStorageOffset..];
Memory<byte> byteContextStorage = stateStorage.Slice(byteContextStorageOffset, byteContextStorageLength);
this.partitionContexts = new Av1NeighborArrayUnit<Av1PartitionContext>[tileCount];
this.lumaCoefficientContexts = new Av1NeighborArrayUnit<byte>[tileCount];
this.blueCoefficientContexts = new Av1NeighborArrayUnit<byte>[tileCount];
this.redCoefficientContexts = new Av1NeighborArrayUnit<byte>[tileCount];
this.transformContexts = new Av1NeighborArrayUnit<byte>[tileCount];
Memory<Av1EncoderPaletteInfo> paletteStorage = Memory<Av1EncoderPaletteInfo>.Empty;
if (frameHeader.AllowScreenContentTools)
{
// Palette entries contain 16-bit colors, so their packed typed region begins at an even byte offset.
ByteMemoryManager<Av1EncoderPaletteInfo> paletteMemory = new(
stateStorage.Slice(paletteStorageOffset, paletteStorageLength));
paletteStorage = paletteMemory.Memory;
this.paletteContexts = new Av1NeighborArrayUnit<Av1EncoderPaletteInfo>[tileCount];
}
else
{
this.paletteContexts = [];
}
int[][] cdefPreset = new int[tileCount][];
int[] previousQIndex = new int[tileCount];
for (int tileIndex = 0; tileIndex < tileCount; tileIndex++)
@ -137,6 +166,17 @@ internal sealed class Av1EncoderPictureBuffer : IDisposable
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
if (frameHeader.AllowScreenContentTools)
{
this.paletteContexts[tileIndex] = new Av1NeighborArrayUnit<Av1EncoderPaletteInfo>(
paletteStorage.Slice(tileIndex * paletteContextLength, paletteContextLength),
paletteLeftLength,
paletteTopLength)
{
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
}
cdefPreset[tileIndex] = [-1, -1, -1, -1];
previousQIndex[tileIndex] = frameHeader.QuantizationParameters.BaseQIndex;
}
@ -148,6 +188,7 @@ internal sealed class Av1EncoderPictureBuffer : IDisposable
CbDcSignLevelCoefficientNeighbors = this.blueCoefficientContexts,
CrDcSignLevelCoefficientNeighbors = this.redCoefficientContexts,
TransformFunctionContexts = this.transformContexts,
PaletteContexts = this.paletteContexts,
Sequence = new Av1SequenceControlSet { SequenceHeader = sequenceHeader },
Parent = new Av1PictureParentControlSet
{
@ -186,6 +227,11 @@ internal sealed class Av1EncoderPictureBuffer : IDisposable
context.Dispose();
}
foreach (Av1NeighborArrayUnit<Av1EncoderPaletteInfo> context in this.paletteContexts)
{
context.Dispose();
}
for (int tileIndex = 0; tileIndex < this.lumaCoefficientContexts.Length; tileIndex++)
{
this.lumaCoefficientContexts[tileIndex].Dispose();

32
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderSuperblockWorkspace.cs

@ -26,7 +26,10 @@ internal sealed class Av1EncoderSuperblockWorkspace : IDisposable
/// </summary>
public const int StorageLength = MaximumFinalBlockCount + ((MaximumPartitionCount + Av1EncoderBlockStruct.StorageSize - 1) / Av1EncoderBlockStruct.StorageSize);
private readonly Configuration configuration;
private readonly IMemoryOwner<Av1EncoderBlockStruct> owner;
private Av1EncoderPaletteMapBuffer? paletteMaps;
private Av1EncoderPaletteInfo paletteInfo;
/// <summary>
/// Initializes a new instance of the <see cref="Av1EncoderSuperblockWorkspace"/> class.
@ -34,6 +37,7 @@ internal sealed class Av1EncoderSuperblockWorkspace : IDisposable
/// <param name="configuration">The configuration providing the encoder allocator.</param>
public Av1EncoderSuperblockWorkspace(Configuration configuration)
{
this.configuration = configuration;
this.owner = configuration.MemoryAllocator.Allocate<Av1EncoderBlockStruct>(StorageLength);
this.Reset();
}
@ -49,6 +53,27 @@ internal sealed class Av1EncoderSuperblockWorkspace : IDisposable
public Span<byte> PartitionTypes
=> MemoryMarshal.AsBytes(this.owner.Memory.Span[MaximumFinalBlockCount..])[..MaximumPartitionCount];
/// <summary>
/// Gets the palette sizes and colors selected for the block currently being written.
/// </summary>
public ref Av1EncoderPaletteInfo PaletteInfo => ref this.paletteInfo;
/// <summary>
/// Gets the reusable palette maps, allocating their shared owner only after a block selects palette mode.
/// </summary>
/// <returns>The reusable luma and chroma palette maps.</returns>
public Av1EncoderPaletteMapBuffer GetPaletteMaps()
{
Av1EncoderPaletteMapBuffer? maps = this.paletteMaps;
if (maps is null)
{
maps = new Av1EncoderPaletteMapBuffer(this.configuration);
this.paletteMaps = maps;
}
return maps;
}
/// <summary>
/// Clears all decisions before the workspace is reused for another superblock.
/// </summary>
@ -62,10 +87,15 @@ internal sealed class Av1EncoderSuperblockWorkspace : IDisposable
this.FinalBlocks.Fill(initialBlock);
MemoryMarshal.AsBytes(this.owner.Memory.Span[MaximumFinalBlockCount..]).Clear();
this.paletteInfo = default;
}
/// <summary>
/// Releases the reusable superblock workspace.
/// </summary>
public void Dispose() => this.owner.Dispose();
public void Dispose()
{
this.paletteMaps?.Dispose();
this.owner.Dispose();
}
}

66
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PaletteCache.cs

@ -0,0 +1,66 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
/// Builds the sorted unique palette-color cache shared by AV1 encoder and decoder syntax.
/// </summary>
internal static class Av1PaletteCache
{
/// <summary>
/// Merges the sorted colors from the available above and left block palettes.
/// </summary>
/// <param name="aboveColors">The above block's sorted base colors.</param>
/// <param name="leftColors">The left block's sorted base colors.</param>
/// <param name="cache">The destination cache, which can hold both palettes.</param>
/// <returns>The number of unique colors written to <paramref name="cache"/>.</returns>
public static int Merge(
ReadOnlySpan<ushort> aboveColors,
ReadOnlySpan<ushort> leftColors,
Span<ushort> cache)
{
int aboveIndex = 0;
int leftIndex = 0;
int count = 0;
while (aboveIndex < aboveColors.Length && leftIndex < leftColors.Length)
{
ushort aboveColor = aboveColors[aboveIndex];
ushort leftColor = leftColors[leftIndex];
if (leftColor < aboveColor)
{
Add(cache, ref count, leftColor);
leftIndex++;
}
else
{
Add(cache, ref count, aboveColor);
aboveIndex++;
if (leftColor == aboveColor)
{
leftIndex++;
}
}
}
while (aboveIndex < aboveColors.Length)
{
Add(cache, ref count, aboveColors[aboveIndex++]);
}
while (leftIndex < leftColors.Length)
{
Add(cache, ref count, leftColors[leftIndex++]);
}
return count;
}
private static void Add(Span<ushort> cache, ref int count, ushort color)
{
if (count == 0 || cache[count - 1] != color)
{
cache[count++] = color;
}
}
}

5
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PictureControlSet.cs

@ -33,6 +33,11 @@ internal class Av1PictureControlSet
/// </summary>
public required Av1NeighborArrayUnit<byte>[] TransformFunctionContexts { get; set; }
/// <summary>
/// Gets or sets the palette sizes and base colors exposed by the above and left block edges for each tile.
/// </summary>
public Av1NeighborArrayUnit<Av1EncoderPaletteInfo>[] PaletteContexts { get; set; } = [];
/// <summary>
/// Gets or sets the sequence-wide encoder state.
/// </summary>

52
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs

@ -2702,54 +2702,10 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
int leftPaletteSize = leftModeInfo is null ? 0 : left.GetPaletteSize(plane);
ReadOnlySpan<ushort> aboveColors = aboveModeInfo is null ? [] : above.GetPaletteColors(plane);
ReadOnlySpan<ushort> leftColors = leftModeInfo is null ? [] : left.GetPaletteColors(plane);
int aboveIndex = 0;
int leftIndex = 0;
int count = 0;
while (aboveIndex < abovePaletteSize && leftIndex < leftPaletteSize)
{
ushort aboveColor = aboveColors[aboveIndex];
ushort leftColor = leftColors[leftIndex];
if (leftColor < aboveColor)
{
AddPaletteCacheColor(cache, ref count, leftColor);
leftIndex++;
}
else
{
AddPaletteCacheColor(cache, ref count, aboveColor);
aboveIndex++;
if (leftColor == aboveColor)
{
leftIndex++;
}
}
}
while (aboveIndex < abovePaletteSize)
{
AddPaletteCacheColor(cache, ref count, aboveColors[aboveIndex++]);
}
while (leftIndex < leftPaletteSize)
{
AddPaletteCacheColor(cache, ref count, leftColors[leftIndex++]);
}
return count;
}
/// <summary>
/// Appends a palette cache color unless it duplicates the preceding sorted value.
/// </summary>
/// <param name="cache">The sorted cache being populated.</param>
/// <param name="count">The number of colors currently stored.</param>
/// <param name="color">The next sorted color.</param>
private static void AddPaletteCacheColor(Span<ushort> cache, ref int count, ushort color)
{
if (count == 0 || cache[count - 1] != color)
{
cache[count++] = color;
}
return Av1PaletteCache.Merge(
aboveColors[..abovePaletteSize],
leftColors[..leftPaletteSize],
cache);
}
/// <summary>

7
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.BlockEncoding.cs

@ -24,13 +24,15 @@ internal partial class Av1TileWriter
/// <param name="tileIndex">The zero-based tile index.</param>
/// <param name="modeInfo">The mode information to publish.</param>
/// <param name="block">The encoder block state to publish.</param>
/// <param name="paletteInfo">The current block's palette sizes and colors.</param>
void EncodeBlock(
Av1SymbolEncoder writer,
Av1MacroBlockD macroBlock,
Point blockOrigin,
ushort tileIndex,
ref Av1MacroBlockModeInfo modeInfo,
ref Av1EncoderBlockStruct block);
ref Av1EncoderBlockStruct block,
ref Av1EncoderPaletteInfo paletteInfo);
}
private readonly struct PrecomputedBlockEncodingHandler : IBlockEncodingHandler
@ -41,7 +43,8 @@ internal partial class Av1TileWriter
Point blockOrigin,
ushort tileIndex,
ref Av1MacroBlockModeInfo modeInfo,
ref Av1EncoderBlockStruct block)
ref Av1EncoderBlockStruct block,
ref Av1EncoderPaletteInfo paletteInfo)
{
}
}

200
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs

@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -716,13 +717,17 @@ internal partial class Av1TileWriter
// Producing the decision here exposes exactly the reconstructed neighbors, coefficient contexts,
// and adaptive probabilities that the following syntax writes.
tb_ptr.Workspace.PaletteInfo = default;
ref Av1EncoderPaletteInfo paletteInfo = ref tb_ptr.Workspace.PaletteInfo;
blockEncoder.EncodeBlock(
writer,
macroBlock,
blockOrigin,
tile_idx,
ref macroBlockModeInfo,
ref blk_ptr);
ref blk_ptr,
ref paletteInfo);
bool skipWritingCoefficients = macroBlockModeInfo.Block.Skip;
// This encoder path currently writes intra frames only, so every block follows the key-frame mode syntax.
@ -791,35 +796,65 @@ internal partial class Av1TileWriter
}
}
if (!macroBlockModeInfo.Block.UseIntraBlockCopy && IsPaletteAllowed(frm_hdr.AllowScreenContentTools, blockSize))
bool paletteAllowed = !macroBlockModeInfo.Block.UseIntraBlockCopy &&
IsPaletteAllowed(frm_hdr.AllowScreenContentTools, blockSize);
if (paletteAllowed)
{
WritePaletteModeInfo(
scs,
pcs,
writer,
macroBlock,
macroBlockModeInfo,
ref blk_ptr,
ref paletteInfo,
blockSize,
blockOrigin >> Av1Constants.ModeInfoSizeLog2);
blockOrigin,
tile_idx,
blk_ptr.HasChroma);
}
if (!macroBlockModeInfo.Block.UseIntraBlockCopy &&
IsFilterIntraAllowed(scs.SequenceHeader.EnableFilterIntra, blockSize, blk_ptr.PaletteSize[0], intra_luma_mode))
IsFilterIntraAllowed(
scs.SequenceHeader.EnableFilterIntra,
blockSize,
paletteInfo.PaletteSizes[0],
intra_luma_mode))
{
writer.WriteFilterIntraMode(blk_ptr.FilterIntraMode, blockSize);
}
if (!macroBlockModeInfo.Block.UseIntraBlockCopy)
if (paletteAllowed)
{
Guard.IsTrue(blk_ptr.PaletteSize[1] == 0, nameof(blk_ptr), "Palette of chroma plane shall be empty.");
// TOKENEXTRA tok = entropyCodingContext.tok;
for (int plane = 0; plane < 2; ++plane)
ObuColorConfig colorConfig = scs.SequenceHeader.ColorConfig;
int palettePlaneCount = Math.Min(2, colorConfig.PlaneCount);
for (int plane = 0; plane < palettePlaneCount; ++plane)
{
int palette_size_plane = blk_ptr.PaletteSize[plane];
if (palette_size_plane > 0)
int paletteSize = paletteInfo.PaletteSizes[plane];
if (paletteSize == 0)
{
throw new NotImplementedException("Tokenizing palette not implemented.");
continue;
}
Av1PlaneType planeType = (Av1PlaneType)plane;
int subX = planeType == Av1PlaneType.Uv && colorConfig.SubSamplingX ? 1 : 0;
int subY = planeType == Av1PlaneType.Uv && colorConfig.SubSamplingY ? 1 : 0;
int blockWidth = blockSize.GetWidth();
int blockHeight = blockSize.GetHeight();
int planeWidth = blockWidth >> subX;
int planeHeight = blockHeight >> subY;
int columns = Math.Min(blockWidth, frm_hdr.FrameSize.FrameWidth - blockOrigin.X) >> subX;
int rows = Math.Min(blockHeight, frm_hdr.FrameSize.FrameHeight - blockOrigin.Y) >> subY;
Buffer2DRegion<byte> colorIndexMap = tb_ptr.Workspace
.GetPaletteMaps()
.GetMap(planeType, planeWidth, planeHeight);
writer.WritePaletteColorMap(
paletteSize,
planeType,
rows,
columns,
colorIndexMap);
}
}
@ -851,7 +886,21 @@ internal partial class Av1TileWriter
}
}
// Neighbor state must be updated after all symbols for the block have used the preceding contexts.
// Palette colors are published only after the current block's mode and map have consumed the preceding edges.
if (frm_hdr.AllowScreenContentTools)
{
const Av1NeighborArrayUnit<Av1EncoderPaletteInfo>.UnitMask PaletteContextMask =
Av1NeighborArrayUnit<Av1EncoderPaletteInfo>.UnitMask.Left |
Av1NeighborArrayUnit<Av1EncoderPaletteInfo>.UnitMask.Top;
pcs.PaletteContexts[tile_idx].UnitModeWrite(
paletteInfo,
blockOrigin,
new Size(blockSize.GetWidth(), blockSize.GetHeight()),
PaletteContextMask);
}
// Coefficient neighbor state follows the same post-symbol ownership boundary.
UpdateNeighbors(pcs, entropyCodingContext, blockOrigin, ref blk_ptr, tile_idx, blockSize);
}
@ -1087,21 +1136,115 @@ internal partial class Av1TileWriter
/// Writes luma and chroma palette-mode syntax for a block.
/// </summary>
/// <param name="scs">The sequence coding state.</param>
/// <param name="pcs">The picture coding state.</param>
/// <param name="writer">The tile symbol encoder.</param>
/// <param name="macroBlock">The current block's mapped neighbor state.</param>
/// <param name="macroBlockModeInfo">The selected block modes.</param>
/// <param name="blk_ptr">The encoder block state.</param>
/// <param name="paletteInfo">The selected palette sizes and colors.</param>
/// <param name="blockSize">The block size.</param>
/// <param name="point">The block position in mode-information units.</param>
/// <exception cref="NotImplementedException">Palette-mode encoding is not implemented.</exception>
private static void WritePaletteModeInfo(
/// <param name="blockOrigin">The absolute luma-sample origin.</param>
/// <param name="tileIndex">The zero-based tile index.</param>
/// <param name="hasChroma">Whether the block owns chroma syntax.</param>
internal static void WritePaletteModeInfo(
Av1SequenceControlSet scs,
Av1PictureControlSet pcs,
Av1SymbolEncoder writer,
Av1MacroBlockD macroBlock,
Av1MacroBlockModeInfo macroBlockModeInfo,
ref Av1EncoderBlockStruct blk_ptr,
ref Av1EncoderPaletteInfo paletteInfo,
Av1BlockSize blockSize,
Point point)
Point blockOrigin,
int tileIndex,
bool hasChroma)
{
int blockSizeContext = Av1Math.Log2(blockSize.GetWidth() * blockSize.GetHeight()) - 6;
Av1NeighborArrayUnit<Av1EncoderPaletteInfo> paletteContexts = pcs.PaletteContexts[tileIndex];
int yPaletteSize = paletteInfo.PaletteSizes[0];
if (macroBlockModeInfo.Block.Mode == Av1PredictionMode.DC)
{
int neighborContext = 0;
if (macroBlock.IsUpAvailable &&
paletteContexts.Top[paletteContexts.GetTopIndex(blockOrigin)].PaletteSizes[0] != 0)
{
neighborContext++;
}
if (macroBlock.IsLeftAvailable &&
paletteContexts.Left[paletteContexts.GetLeftIndex(blockOrigin)].PaletteSizes[0] != 0)
{
neighborContext++;
}
writer.WritePaletteYMode(yPaletteSize != 0, blockSizeContext, neighborContext);
if (yPaletteSize != 0)
{
writer.WritePaletteSize(yPaletteSize, blockSizeContext, Av1PlaneType.Y);
Span<ushort> colorCache = stackalloc ushort[2 * Av1Constants.PaletteMaxSize];
int cacheSize = GetPaletteCache(
paletteContexts,
macroBlock,
blockOrigin,
Av1Plane.Y,
colorCache);
writer.WritePaletteYColors(
colorCache[..cacheSize],
paletteInfo.GetColors(Av1Plane.Y),
scs.SequenceHeader.ColorConfig.BitDepth.GetBitCount());
}
}
int uvPaletteSize = paletteInfo.PaletteSizes[1];
if (scs.SequenceHeader.ColorConfig.PlaneCount > 1 &&
macroBlockModeInfo.Block.UvMode == Av1ChromaPredictionMode.DC &&
hasChroma)
{
writer.WritePaletteUvMode(uvPaletteSize != 0, yPaletteSize != 0);
if (uvPaletteSize != 0)
{
writer.WritePaletteSize(uvPaletteSize, blockSizeContext, Av1PlaneType.Uv);
Span<ushort> colorCache = stackalloc ushort[2 * Av1Constants.PaletteMaxSize];
int cacheSize = GetPaletteCache(
paletteContexts,
macroBlock,
blockOrigin,
Av1Plane.U,
colorCache);
writer.WritePaletteUvColors(
colorCache[..cacheSize],
paletteInfo.GetColors(Av1Plane.U),
paletteInfo.GetColors(Av1Plane.V),
scs.SequenceHeader.ColorConfig.BitDepth.GetBitCount());
}
}
}
/// <summary>
/// Builds the sorted palette-color cache from the available above and left encoder edges.
/// </summary>
private static int GetPaletteCache(
Av1NeighborArrayUnit<Av1EncoderPaletteInfo> paletteContexts,
Av1MacroBlockD macroBlock,
Point blockOrigin,
Av1Plane plane,
Span<ushort> cache)
{
throw new NotImplementedException("Palette mode encoding not implemented.");
// AV1 excludes the above palette at each 64-sample row boundary, even with 128x128 superblocks.
bool hasAbove = macroBlock.IsUpAvailable &&
(blockOrigin.Y & (Av1BlockSize.Block64x64.GetHeight() - 1)) != 0;
Av1EncoderPaletteInfo above = hasAbove
? paletteContexts.Top[paletteContexts.GetTopIndex(blockOrigin)]
: default;
Av1EncoderPaletteInfo left = macroBlock.IsLeftAvailable
? paletteContexts.Left[paletteContexts.GetLeftIndex(blockOrigin)]
: default;
ReadOnlySpan<ushort> aboveColors = hasAbove ? above.GetColors(plane) : [];
ReadOnlySpan<ushort> leftColors = macroBlock.IsLeftAvailable ? left.GetColors(plane) : [];
return Av1PaletteCache.Merge(aboveColors, leftColors, cache);
}
/// <summary>
@ -1222,21 +1365,6 @@ internal partial class Av1TileWriter
}
}
/// <summary>
/// Determines whether the encoder palette level and block dimensions permit palette mode.
/// </summary>
/// <param name="allowPalette">The nonzero encoder palette level.</param>
/// <param name="blockSize">The block size.</param>
/// <returns><see langword="true"/> when palette mode is enabled for the block; otherwise, <see langword="false"/>.</returns>
private static bool IsPaletteAllowed(int allowPalette, Av1BlockSize blockSize)
{
Guard.MustBeLessThan((int)blockSize, (int)Av1BlockSize.AllSizes, nameof(blockSize));
return allowPalette != 0 &&
blockSize.GetWidth() <= 64 &&
blockSize.GetHeight() <= 64 &&
blockSize >= Av1BlockSize.Block8x8;
}
/// <summary>
/// Determines whether screen-content tools and block dimensions permit palette mode.
/// </summary>

163
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs

@ -234,20 +234,21 @@ public class Av1CoefficientsEntropyTests
Assert.Equal(Av1EncoderSuperblockWorkspace.MaximumFinalBlockCount, workspace.FinalBlocks.Length);
Assert.Equal(Av1EncoderSuperblockWorkspace.MaximumPartitionCount, workspace.PartitionTypes.Length);
Assert.Equal(Av1EncoderBlockStruct.StorageSize, Unsafe.SizeOf<Av1EncoderBlockStruct>());
Assert.Equal(0, workspace.FinalBlocks[0].PaletteSize[0]);
Assert.Equal(Av1EncoderPaletteInfo.StorageSize, Unsafe.SizeOf<Av1EncoderPaletteInfo>());
Assert.Equal(0, workspace.PaletteInfo.PaletteSizes[0]);
Assert.Equal(0, workspace.FinalBlocks[^1].QuantizationIndex);
Assert.Equal(Av1FilterIntraMode.AllFilterIntraModes, workspace.FinalBlocks[0].FilterIntraMode);
Assert.Equal(Av1FilterIntraMode.AllFilterIntraModes, workspace.FinalBlocks[^1].FilterIntraMode);
Assert.Equal(0, workspace.PartitionTypes[^1]);
workspace.FinalBlocks[0].PaletteSize[0] = 7;
workspace.PaletteInfo.PaletteSizes[0] = 7;
workspace.FinalBlocks[0].FilterIntraMode = Av1FilterIntraMode.DC;
workspace.FinalBlocks[^1].QuantizationIndex = 255;
workspace.FinalBlocks[^1].FilterIntraMode = Av1FilterIntraMode.Paeth;
workspace.PartitionTypes.Fill(byte.MaxValue);
workspace.Reset();
Assert.Equal(0, workspace.FinalBlocks[0].PaletteSize[0]);
Assert.Equal(0, workspace.PaletteInfo.PaletteSizes[0]);
Assert.Equal(0, workspace.FinalBlocks[^1].QuantizationIndex);
Assert.Equal(Av1FilterIntraMode.AllFilterIntraModes, workspace.FinalBlocks[0].FilterIntraMode);
Assert.Equal(Av1FilterIntraMode.AllFilterIntraModes, workspace.FinalBlocks[^1].FilterIntraMode);
@ -265,28 +266,174 @@ public class Av1CoefficientsEntropyTests
public void EncoderBlocksKeepInlineModeStateWithoutPerBlockAllocations()
{
Av1EncoderBlockStruct[] blocks = new Av1EncoderBlockStruct[2];
Av1EncoderPaletteInfo[] palettes = new Av1EncoderPaletteInfo[2];
// Exercise the inline-array accessors before measuring so one-time runtime generic initialization is
// excluded from the steady-state allocation contract used for every encoded block.
ref Av1EncoderBlockStruct warmupBlock = ref blocks[0];
warmupBlock.PaletteSize[0] = 1;
ref Av1EncoderPaletteInfo warmupPalette = ref palettes[0];
warmupPalette.PaletteSizes[0] = 1;
warmupBlock.PredictionUnit.AngleDelta[(int)Av1PlaneType.Y] = 1;
long before = GC.GetAllocatedBytesForCurrentThread();
ref Av1EncoderBlockStruct block = ref blocks[1];
block.PaletteSize[0] = 3;
block.PaletteSize[1] = 5;
ref Av1EncoderPaletteInfo palette = ref palettes[1];
palette.PaletteSizes[0] = 3;
palette.PaletteSizes[1] = 5;
block.PredictionUnit.AngleDelta[(int)Av1PlaneType.Y] = -2;
block.PredictionUnit.AngleDelta[(int)Av1PlaneType.Uv] = 3;
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(3, blocks[1].PaletteSize[0]);
Assert.Equal(5, blocks[1].PaletteSize[1]);
Assert.Equal(3, palettes[1].PaletteSizes[0]);
Assert.Equal(5, palettes[1].PaletteSizes[1]);
Assert.Equal(-2, blocks[1].PredictionUnit.AngleDelta[(int)Av1PlaneType.Y]);
Assert.Equal(3, blocks[1].PredictionUnit.AngleDelta[(int)Av1PlaneType.Uv]);
Assert.Equal(0, allocated);
}
[Fact]
public void EncoderPaletteMapsUseOneLazyExactSizeOwner()
{
TestMemoryAllocator allocator = new();
allocator.EnableNonThreadSafeLogging();
Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator;
TestMemoryAllocator.AllocationRequest[] allocations;
using (Av1EncoderSuperblockWorkspace workspace = new(configuration))
{
Assert.Single(allocator.AllocationLog);
Assert.Empty(allocator.ReturnLog);
Av1EncoderPaletteMapBuffer maps = workspace.GetPaletteMaps();
Assert.Same(maps, workspace.GetPaletteMaps());
allocations = allocator.AllocationLog.ToArray();
Assert.Equal(2, allocations.Length);
Assert.Equal(typeof(byte), allocations[1].ElementType);
Assert.Equal(Av1EncoderPaletteMapBuffer.StorageLength, allocations[1].Length);
Assert.Equal(AllocationOptions.None, allocations[1].AllocationOptions);
Buffer2DRegion<byte> luma = maps.GetMap(Av1PlaneType.Y, 64, 64);
Buffer2DRegion<byte> chroma = maps.GetMap(Av1PlaneType.Uv, 32, 32);
luma.DangerousGetRowSpan(0)[0] = 3;
chroma.DangerousGetRowSpan(0)[0] = 5;
Assert.Equal(3, luma.DangerousGetRowSpan(0)[0]);
Assert.Equal(5, chroma.DangerousGetRowSpan(0)[0]);
}
Assert.Equal(2, allocator.ReturnLog.Count);
Assert.Equal(
allocations.Select(x => x.AllocationId).Order(),
allocator.ReturnLog.Select(x => x.AllocationId).Order());
}
[Fact]
public void PaletteModeWriterMatchesColorCacheBoundaryAndRoundTrips()
{
const int Width = 16;
const int Height = 72;
const int BlockSizeContext = 0;
Point blockOrigin = new(8, 64);
ObuColorConfig colorConfig = new()
{
IsMonochrome = false,
SubSamplingX = false,
SubSamplingY = false,
BitDepth = Av1BitDepth.EightBit
};
ObuTileGroupHeader tiles = new()
{
TileColumnCount = 1,
TileRowCount = 1
};
tiles.TileColumnStartModeInfo[1] = Width >> Av1Constants.ModeInfoSizeLog2;
tiles.TileRowStartModeInfo[1] = Height >> Av1Constants.ModeInfoSizeLog2;
ObuSequenceHeader sequenceHeader = new()
{
ColorConfig = colorConfig
};
ObuFrameHeader frameHeader = new()
{
AllowScreenContentTools = true,
ModeInfoColumnCount = Width >> Av1Constants.ModeInfoSizeLog2,
ModeInfoRowCount = Height >> Av1Constants.ModeInfoSizeLog2,
TilesInfo = tiles
};
using Av1EncoderPictureBuffer pictureBuffer = new(
Configuration.Default,
sequenceHeader,
frameHeader,
Width,
Height);
Av1PictureControlSet picture = pictureBuffer.Picture;
Av1NeighborArrayUnit<Av1EncoderPaletteInfo> paletteContexts = Assert.Single(picture.PaletteContexts);
ref Av1EncoderPaletteInfo above = ref paletteContexts.Top[paletteContexts.GetTopIndex(blockOrigin)];
above.PaletteSizes[0] = 2;
above.PaletteSizes[1] = 2;
above.SetColors(Av1Plane.Y, [10, 30]);
above.SetColors(Av1Plane.U, [15, 35]);
ref Av1EncoderPaletteInfo left = ref paletteContexts.Left[paletteContexts.GetLeftIndex(blockOrigin)];
left.PaletteSizes[0] = 2;
left.PaletteSizes[1] = 2;
left.SetColors(Av1Plane.Y, [20, 40]);
left.SetColors(Av1Plane.U, [25, 45]);
Av1EncoderPaletteInfo current = default;
current.PaletteSizes[0] = 3;
current.PaletteSizes[1] = 3;
current.SetColors(Av1Plane.Y, [20, 50, 70]);
current.SetColors(Av1Plane.U, [25, 55, 80]);
current.SetColors(Av1Plane.V, [10, 12, 9]);
Av1MacroBlockModeInfo modeInfo = default;
modeInfo.Block = new Av1EncoderBlockModeInfo
{
BlockSize = Av1BlockSize.Block8x8,
Mode = Av1PredictionMode.DC,
UvMode = Av1ChromaPredictionMode.DC
};
Av1MacroBlockD macroBlock = new()
{
Tile = new Av1TileInfo(0, 0, frameHeader),
IsUpAvailable = true,
IsLeftAvailable = true
};
using Av1SymbolEncoder encoder = new(Configuration.Default, 128, BaseQIndex);
Av1TileWriter.WritePaletteModeInfo(
picture.Sequence,
picture,
encoder,
macroBlock,
modeInfo,
ref current,
Av1BlockSize.Block8x8,
blockOrigin,
tileIndex: 0,
hasChroma: true);
using IMemoryOwner<byte> encoded = encoder.Exit();
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex);
Assert.True(decoder.ReadPaletteYMode(BlockSizeContext, neighborContext: 2));
Assert.Equal(3, decoder.ReadPaletteSize(BlockSizeContext, Av1PlaneType.Y));
Span<ushort> decodedY = stackalloc ushort[3];
decoder.ReadPaletteYColors([20, 40], 3, bitDepth: 8, decodedY);
Assert.Equal([20, 50, 70], decodedY.ToArray());
Assert.True(decoder.ReadPaletteUvMode(hasLumaPalette: true));
Assert.Equal(3, decoder.ReadPaletteSize(BlockSizeContext, Av1PlaneType.Uv));
Span<ushort> decodedU = stackalloc ushort[3];
Span<ushort> decodedV = stackalloc ushort[3];
decoder.ReadPaletteUvColors([25, 45], 3, bitDepth: 8, decodedU, decodedV);
Assert.Equal([25, 55, 80], decodedU.ToArray());
Assert.Equal([10, 12, 9], decodedV.ToArray());
decoder.ValidateTrailingBits();
}
[Theory]
[InlineData(false, 6, 4096, 1024, 6144, 256, 64, 384, 36864L)]
[InlineData(true, 2, 16384, 4096, 24576, 1024, 256, 1536, 49152L)]

33
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderModeInfoBufferTests.cs

@ -46,8 +46,12 @@ public class Av1EncoderModeInfoBufferTests
Assert.Equal(allocation.AllocationId, returned.AllocationId);
}
[Fact]
public void PictureBufferPacksAllPictureStateIntoTwoAllocatorOwners()
[Theory]
[InlineData(false, 336)]
[InlineData(true, 3_536)]
public unsafe void PictureBufferPacksAllPictureStateIntoTwoAllocatorOwners(
bool allowScreenContentTools,
int expectedStateStorageLength)
{
const int Width = 16;
const int Height = 16;
@ -79,6 +83,7 @@ public class Av1EncoderModeInfoBufferTests
ObuFrameHeader frameHeader = new()
{
AllowScreenContentTools = allowScreenContentTools,
ModeInfoColumnCount = Width >> Av1Constants.ModeInfoSizeLog2,
ModeInfoRowCount = Height >> Av1Constants.ModeInfoSizeLog2,
TilesInfo = tiles
@ -98,7 +103,7 @@ public class Av1EncoderModeInfoBufferTests
Assert.Equal(6_144, allocations[0].Length);
Assert.Equal(AllocationOptions.Clean, allocations[0].AllocationOptions);
Assert.Equal(typeof(byte), allocations[1].ElementType);
Assert.Equal(336, allocations[1].Length);
Assert.Equal(expectedStateStorageLength, allocations[1].Length);
Assert.Equal(AllocationOptions.Clean, allocations[1].AllocationOptions);
Assert.Empty(allocator.ReturnLog);
@ -112,6 +117,28 @@ public class Av1EncoderModeInfoBufferTests
Assert.Equal(16, picture.CbDcSignLevelCoefficientNeighbors[0].Top.Length);
Assert.Equal(32, picture.TransformFunctionContexts[0].Left.Length);
Assert.Equal(32, picture.TransformFunctionContexts[0].Top.Length);
if (allowScreenContentTools)
{
Av1NeighborArrayUnit<Av1EncoderPaletteInfo> paletteContext = Assert.Single(picture.PaletteContexts);
Assert.Equal(32, paletteContext.Left.Length);
Assert.Equal(32, paletteContext.Top.Length);
Assert.Equal(Av1Constants.ModeInfoSizeLog2, paletteContext.GranularityNormalLog2);
Assert.Equal(0, paletteContext.Left[0].PaletteSizes[0]);
Assert.Equal(0, paletteContext.Top[^1].PaletteSizes[1]);
// The typed palette region starts at its natural 16-bit alignment inside the shared byte owner.
fixed (Av1EncoderPaletteInfo* pointer = paletteContext.Left)
{
Assert.Equal((nuint)0, (nuint)pointer % (nuint)sizeof(ushort));
}
paletteContext.Left[0].PaletteSizes[0] = 3;
Assert.Equal(0, paletteContext.Top[0].PaletteSizes[0]);
}
else
{
Assert.Empty(picture.PaletteContexts);
}
}
Assert.Equal(2, allocator.ReturnLog.Count);

12
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs

@ -1254,6 +1254,18 @@ public class Av1EntropyTests
encoder.GetPaletteColorMapCost(3, Av1PlaneType.Y, 2, 2, region));
}
[Fact]
public void PaletteCacheMergesSortedNeighborColorsWithoutDuplicates()
{
ReadOnlySpan<ushort> above = [1, 3, 5, 7];
ReadOnlySpan<ushort> left = [2, 3, 6, 7];
Span<ushort> cache = stackalloc ushort[2 * Av1Constants.PaletteMaxSize];
int count = Av1PaletteCache.Merge(above, left, cache);
Assert.Equal([1, 2, 3, 5, 6, 7], cache[..count].ToArray());
}
[Theory]
[InlineData(1, 1, 0, 0, 0, 1, 4, 1)]
[InlineData(1, 1, 0, 1, 0, 1, 3, 1)]

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

@ -610,6 +610,112 @@ public class Av1IntraSuperblockEncoderTests
Assert.NotEqual(0, encoded.GetSpan().Length);
}
[Fact]
public void ProductionWriterConsumesPaletteMapAndPublishesPaletteEdges()
{
const int Width = 8;
const int Height = 8;
const int QIndex = 23;
ObuColorConfig colorConfig = new()
{
IsMonochrome = true,
SubSamplingX = true,
SubSamplingY = true,
BitDepth = Av1BitDepth.EightBit
};
ObuTileGroupHeader tiles = new()
{
TileColumnCount = 1,
TileRowCount = 1
};
tiles.TileColumnStartModeInfo[1] = Width >> Av1Constants.ModeInfoSizeLog2;
tiles.TileRowStartModeInfo[1] = Height >> Av1Constants.ModeInfoSizeLog2;
ObuSequenceHeader sequenceHeader = new()
{
ColorConfig = colorConfig
};
ObuFrameHeader frameHeader = new()
{
AllowScreenContentTools = true,
ModeInfoColumnCount = Width >> Av1Constants.ModeInfoSizeLog2,
ModeInfoRowCount = Height >> Av1Constants.ModeInfoSizeLog2,
FrameSize = new ObuFrameSize
{
FrameWidth = Width,
FrameHeight = Height
},
TilesInfo = tiles
};
frameHeader.QuantizationParameters.BaseQIndex = QIndex;
frameHeader.QuantizationParameters.QIndex.Fill(QIndex);
byte[][] payloads = new byte[2][];
for (int mapVariant = 0; mapVariant < payloads.Length; mapVariant++)
{
using Av1EncoderPictureBuffer pictureBuffer = new(
Configuration.Default,
sequenceHeader,
frameHeader,
Width,
Height);
using Av1EncoderCoefficientBuffer coefficients = new(
Configuration.Default,
sequenceHeader,
Width,
Height);
using Av1EncoderSuperblockWorkspace workspace = new(Configuration.Default);
Av1Superblock superblock = new()
{
Workspace = workspace,
TileInfo = new Av1TileInfo(0, 0, frameHeader),
Index = 0
};
Av1PictureControlSet picture = pictureBuffer.Picture;
Av1IntraSuperblockEncoder.Prepare(picture, superblock, Point.Empty);
Av1TileWriter.Av1EntropyCodingContext entropyContext = new()
{
MacroBlock = new Av1MacroBlockD { Tile = superblock.TileInfo },
MacroBlockModeInfo = picture.GetMacroBlockModeInfo(default),
SuperblockOrigin = default
};
PaletteBlockEncoder blockEncoder = new(workspace, QIndex, mapVariant);
using Av1SymbolEncoder writer = new(Configuration.Default, 128, QIndex);
Av1TileWriter.WriteSuperblock(
picture,
entropyContext,
writer,
superblock,
coefficients,
tileIndex: 0,
ref blockEncoder);
using IMemoryOwner<byte> encoded = writer.Exit();
payloads[mapVariant] = encoded.GetSpan().ToArray();
Assert.Equal(1, blockEncoder.Count);
Av1NeighborArrayUnit<Av1EncoderPaletteInfo> paletteContext = Assert.Single(picture.PaletteContexts);
for (int index = 0; index < 2; index++)
{
Assert.Equal(3, paletteContext.Top[index].PaletteSizes[0]);
Assert.Equal(3, paletteContext.Left[index].PaletteSizes[0]);
Assert.Equal([16, 128, 240], paletteContext.Top[index].GetColors(Av1Plane.Y).ToArray());
Assert.Equal([16, 128, 240], paletteContext.Left[index].GetColors(Av1Plane.Y).ToArray());
}
Assert.Equal(0, paletteContext.Top[2].PaletteSizes[0]);
Assert.Equal(0, paletteContext.Left[2].PaletteSizes[0]);
}
// Changing only the selected color indices must change the range-coded tile payload.
Assert.False(payloads[0].SequenceEqual(payloads[1]));
}
[Theory]
[InlineData((int)Av1PredictionMode.Vertical, 0)]
[InlineData((int)Av1PredictionMode.Horizontal, 0)]
@ -1911,7 +2017,8 @@ public class Av1IntraSuperblockEncoderTests
Point blockOrigin,
ushort tileIndex,
ref Av1MacroBlockModeInfo modeInfo,
ref Av1EncoderBlockStruct block)
ref Av1EncoderBlockStruct block,
ref Av1EncoderPaletteInfo paletteInfo)
{
this.costs[this.Count++] = Av1TileWriter.GetLumaModeCost(
writer,
@ -1936,4 +2043,79 @@ public class Av1IntraSuperblockEncoderTests
block.SegmentId = 0;
}
}
/// <summary>
/// Supplies one skipped monochrome palette block to the production tile writer.
/// </summary>
private struct PaletteBlockEncoder : Av1TileWriter.IBlockEncodingHandler
{
private readonly Av1EncoderSuperblockWorkspace workspace;
private readonly int qIndex;
private readonly int mapVariant;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteBlockEncoder"/> struct.
/// </summary>
/// <param name="workspace">The workspace that owns the palette index map.</param>
/// <param name="qIndex">The block quantizer index.</param>
/// <param name="mapVariant">The map pattern selected by the test.</param>
public PaletteBlockEncoder(
Av1EncoderSuperblockWorkspace workspace,
int qIndex,
int mapVariant)
{
this.workspace = workspace;
this.qIndex = qIndex;
this.mapVariant = mapVariant;
this.Count = 0;
}
/// <summary>
/// Gets the number of final blocks visited by the writer.
/// </summary>
public int Count { get; private set; }
/// <inheritdoc/>
public void EncodeBlock(
Av1SymbolEncoder writer,
Av1MacroBlockD macroBlock,
Point blockOrigin,
ushort tileIndex,
ref Av1MacroBlockModeInfo modeInfo,
ref Av1EncoderBlockStruct block,
ref Av1EncoderPaletteInfo paletteInfo)
{
this.Count++;
modeInfo.Block = new Av1EncoderBlockModeInfo
{
BlockSize = Av1BlockSize.Block8x8,
PartitionType = Av1PartitionType.None,
SegmentId = 0,
Skip = true,
TransformSize = Av1TransformSize.Size8x8,
Mode = Av1PredictionMode.DC,
UvMode = Av1ChromaPredictionMode.DC
};
block.HasChroma = false;
block.QuantizationIndex = this.qIndex;
block.SegmentId = 0;
paletteInfo.PaletteSizes[0] = 3;
paletteInfo.SetColors(Av1Plane.Y, [16, 128, 240]);
Buffer2DRegion<byte> map = this.workspace
.GetPaletteMaps()
.GetMap(Av1PlaneType.Y, 8, 8);
for (int row = 0; row < map.Height; row++)
{
Span<byte> mapRow = map.DangerousGetRowSpan(row);
for (int column = 0; column < map.Width; column++)
{
mapRow[column] = this.mapVariant == 0
? (byte)0
: (byte)((row + column) % 3);
}
}
}
}
}

Loading…
Cancel
Save