Browse Source

Fix AV1 tile coefficient contexts

pull/2633/head
James Jackson-South 1 month ago
parent
commit
d813ae7d72
  1. 2
      HEIF_IMPLEMENTATION_PLAN.md
  2. 75
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs
  3. 325
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs
  4. 56
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs

2
HEIF_IMPLEMENTATION_PLAN.md

@ -820,7 +820,7 @@ Encoder verification contract:
- [ ] Implement inter mode search for bounded sequences, including reference selection and the decoder-supported inter tools. - [ ] Implement inter mode search for bounded sequences, including reference selection and the decoder-supported inter tools.
- [~] Current-libaom `av1_quantize_fp_no_qmatrix` arithmetic is implemented as a closed generic forward-quantizer family with Vector512, Vector256, Vector128, and scalar paths, raster-order output, coded 64-point coefficient limits, and scan-order EOB selection. Transform search, coefficient optimization, and lossless behavior remain. - [~] Current-libaom `av1_quantize_fp_no_qmatrix` arithmetic is implemented as a closed generic forward-quantizer family with Vector512, Vector256, Vector128, and scalar paths, raster-order output, coded 64-point coefficient limits, and scan-order EOB selection. Transform search, coefficient optimization, and lossless behavior remain.
- [ ] Implement real rate-distortion selection and make quality and effort change work, size, and output quality. - [ ] Implement real rate-distortion selection and make quality and effort change work, size, and output quality.
- [ ] Implement tile-local entropy coding and CDF update behavior. - [~] The tile writer now publishes one packed coefficient context per covered 4x4 edge unit and derives luma/chroma skip plus DC-sign contexts from the complete transform edges using current-libaom units. Complete tile traversal, initialized picture state, and verified CDF update behavior remain.
- [ ] Implement legal deblocking, CDEF, restoration, super-resolution, and film-grain signaling decisions. - [ ] Implement legal deblocking, CDEF, restoration, super-resolution, and film-grain signaling decisions.
- [~] The coefficient symbol encoder now reuses tile-lifetime level and context workspaces instead of allocating per transform. Every remaining encoder fragment must be audited before it becomes active. - [~] The coefficient symbol encoder now reuses tile-lifetime level and context workspaces instead of allocating per transform. Every remaining encoder fragment must be audited before it becomes active.
- [~] The planar conversion, forward transform, and forward quantizer use descending SIMD dispatch: Vector512, Vector256, Vector128, then scalar. Apply the same rule to every later hot-path family. - [~] The planar conversion, forward transform, and forward quantizer use descending SIMD dispatch: Vector512, Vector256, Vector128, then scalar. Apply the same rule to every later hot-path family.

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

@ -3,7 +3,6 @@
using System.Buffers; using System.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -127,11 +126,6 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
/// </summary> /// </summary>
public required int GranularityTopLeftLog2 { get; set; } public required int GranularityTopLeftLog2 { get; set; }
/// <summary>
/// Gets the number of consecutive values stored for each neighbor-array unit.
/// </summary>
public int UnitSize { get; private set; }
/// <summary> /// <summary>
/// Gets the left-neighbor unit index for a sample position. /// Gets the left-neighbor unit index for a sample position.
/// </summary> /// </summary>
@ -166,20 +160,12 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
/// <summary> /// <summary>
/// Writes one context unit across the selected block edges. /// Writes one context unit across the selected block edges.
/// </summary> /// </summary>
/// <param name="value">The values that make up one context unit.</param> /// <param name="value">The context value to publish.</param>
/// <param name="origin">The block origin in samples.</param> /// <param name="origin">The block origin in samples.</param>
/// <param name="blockSize">The block dimensions in samples.</param> /// <param name="blockSize">The block dimensions in samples.</param>
/// <param name="mask">The neighbor arrays to update.</param> /// <param name="mask">The neighbor arrays to update.</param>
public void UnitModeWrite(ReadOnlySpan<T> value, Point origin, Size blockSize, UnitMask mask) public void UnitModeWrite(T value, Point origin, Size blockSize, UnitMask mask)
{ {
int idx, j;
int count;
int na_offset;
int na_unit_size;
na_unit_size = this.UnitSize;
if ((mask & UnitMask.Top) == UnitMask.Top) if ((mask & UnitMask.Top) == UnitMask.Top)
{ {
// Top Neighbor Array // Top Neighbor Array
@ -196,21 +182,12 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
// bottom row of the source block // bottom row of the source block
// //
// Index = org_x // Index = org_x
na_offset = this.GetTopIndex(origin); int offset = this.GetTopIndex(origin);
int count = blockSize.Width >> this.GranularityNormalLog2;
ref T dst_ptr = ref this.Top[na_offset * na_unit_size];
count = blockSize.Width >> this.GranularityNormalLog2; // One packed value represents each AV1 edge unit. Filling the covered range mirrors the
// contiguous above-context update without retaining a caller-owned span.
for (idx = 0; idx < count; ++idx) this.Top.Slice(offset, count).Fill(value);
{
// Unit sizes are deliberately tiny, so direct ref copies avoid slicing for every neighbor position.
for (j = 0; j < na_unit_size; ++j)
{
dst_ptr = value[j];
dst_ptr = Unsafe.Add(ref dst_ptr, 1);
}
}
} }
if ((mask & UnitMask.Left) == UnitMask.Left) if ((mask & UnitMask.Left) == UnitMask.Left)
@ -230,21 +207,9 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
// right column of the source block // right column of the source block
// //
// Index = org_y // Index = org_y
na_offset = this.GetLeftIndex(origin); int offset = this.GetLeftIndex(origin);
int count = blockSize.Height >> this.GranularityNormalLog2;
ref T dst_ptr = ref this.Left[na_offset * na_unit_size]; this.Left.Slice(offset, count).Fill(value);
count = blockSize.Height >> this.GranularityNormalLog2;
for (idx = 0; idx < count; ++idx)
{
// Unit sizes are deliberately tiny, so direct ref copies avoid slicing for every neighbor position.
for (j = 0; j < na_unit_size; ++j)
{
dst_ptr = value[j];
dst_ptr = Unsafe.Add(ref dst_ptr, 1);
}
}
} }
if ((mask & UnitMask.TopLeft) == UnitMask.TopLeft) if ((mask & UnitMask.TopLeft) == UnitMask.TopLeft)
@ -267,23 +232,9 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
// Index = org_x - org_y // Index = org_x - org_y
Point topLeft = origin; Point topLeft = origin;
topLeft.Offset(0, blockSize.Height - 1); topLeft.Offset(0, blockSize.Height - 1);
na_offset = this.GetTopLeftIndex(topLeft); int offset = this.GetTopLeftIndex(topLeft);
int count = ((blockSize.Width + blockSize.Height) >> this.GranularityTopLeftLog2) - 1;
// Copy bottom-row + right-column this.TopLeft.Slice(offset, count).Fill(value);
// *Note - start from the bottom-left corner
ref T dst_ptr = ref this.TopLeft[na_offset * na_unit_size];
count = ((blockSize.Width + blockSize.Height) >> this.GranularityTopLeftLog2) - 1;
for (idx = 0; idx < count; ++idx)
{
// Unit sizes are deliberately tiny, so direct ref copies avoid slicing for every neighbor position.
for (j = 0; j < na_unit_size; ++j)
{
dst_ptr = value[j];
dst_ptr = Unsafe.Add(ref dst_ptr, 1);
}
}
} }
} }
} }

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

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.ModeDecision; using SixLabors.ImageSharp.Formats.Heif.Av1.ModeDecision;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
@ -747,9 +746,8 @@ internal partial class Av1TileWriter
PartitionContextLookup[(int)blockSize].Above, PartitionContextLookup[(int)blockSize].Above,
PartitionContextLookup[(int)blockSize].Left); PartitionContextLookup[(int)blockSize].Left);
Size size = new(blk_geom.BlockWidth, blk_geom.BlockHeight); Size size = new(blk_geom.BlockWidth, blk_geom.BlockHeight);
Span<Av1PartitionContext> partitionSpan = new(ref partition);
partition_context_na.UnitModeWrite( partition_context_na.UnitModeWrite(
partitionSpan, partition,
blockOrigin, blockOrigin,
size, size,
Av1NeighborArrayUnit<Av1PartitionContext>.UnitMask.Left | Av1NeighborArrayUnit<Av1PartitionContext>.UnitMask.Top); Av1NeighborArrayUnit<Av1PartitionContext>.UnitMask.Left | Av1NeighborArrayUnit<Av1PartitionContext>.UnitMask.Top);
@ -757,26 +755,26 @@ internal partial class Av1TileWriter
{ {
// A skipped block has an all-zero residual, so publish a zero sign/level context over its edges // A skipped block has an all-zero residual, so publish a zero sign/level context over its edges
// and advance coefficient positions without reading transform units. // and advance coefficient positions without reading transform units.
byte dcSignLevelCoefficient = 0;
Span<byte> dcSignSpan = new(ref dcSignLevelCoefficient);
luma_dc_sign_level_coeff_na.UnitModeWrite( luma_dc_sign_level_coeff_na.UnitModeWrite(
dcSignSpan, 0,
blockOrigin, blockOrigin,
size, size,
Av1NeighborArrayUnit<byte>.UnitMask.Left | Av1NeighborArrayUnit<byte>.UnitMask.Top); Av1NeighborArrayUnit<byte>.UnitMask.Left | Av1NeighborArrayUnit<byte>.UnitMask.Top);
if (blk_geom.HasUv) if (blk_geom.HasUv)
{ {
Point chromaOrigin = RoundUv(blockOrigin) >> 1;
Size chromaSize = new(blk_geom.BlockWidthUv, blk_geom.BlockHeightUv);
cb_dc_sign_level_coeff_na.UnitModeWrite( cb_dc_sign_level_coeff_na.UnitModeWrite(
dcSignSpan, 0,
((blockOrigin >> 3) << 3) >> 1, chromaOrigin,
size, chromaSize,
Av1NeighborArrayUnit<byte>.UnitMask.Left | Av1NeighborArrayUnit<byte>.UnitMask.Top); Av1NeighborArrayUnit<byte>.UnitMask.Left | Av1NeighborArrayUnit<byte>.UnitMask.Top);
cr_dc_sign_level_coeff_na.UnitModeWrite( cr_dc_sign_level_coeff_na.UnitModeWrite(
dcSignSpan, 0,
((blockOrigin >> 3) << 3) >> 1, chromaOrigin,
size, chromaSize,
Av1NeighborArrayUnit<byte>.UnitMask.Left | Av1NeighborArrayUnit<byte>.UnitMask.Top); Av1NeighborArrayUnit<byte>.UnitMask.Left | Av1NeighborArrayUnit<byte>.UnitMask.Top);
entropyCodingContext.CodedAreaSuperblockUv += blk_geom.BlockWidthUv * blk_geom.BlockHeightUv; entropyCodingContext.CodedAreaSuperblockUv += blk_geom.BlockWidthUv * blk_geom.BlockHeightUv;
} }
@ -1043,16 +1041,13 @@ internal partial class Av1TileWriter
int coeff1d_offset = entropyCodingContext.CodedAreaSuperblock; int coeff1d_offset = entropyCodingContext.CodedAreaSuperblock;
Span<int> coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.Y).DangerousGetSingleSpan()[coeff1d_offset..]; Span<int> coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.Y).DangerousGetSingleSpan()[coeff1d_offset..];
Av1TransformBlockContext blockContext = default;
Point transformOrigin = blockGeometry.TransformOrigin[tx_depth][txb_itr]; Point transformOrigin = blockGeometry.TransformOrigin[tx_depth][txb_itr];
GetTransformBlockContexts( Av1TransformBlockContext blockContext = GetTransformBlockContexts(
pcs,
Av1ComponentType.Luminance, Av1ComponentType.Luminance,
luma_dc_sign_level_coeff_na, luma_dc_sign_level_coeff_na,
blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin, blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin,
plane_bsize, plane_bsize,
tx_size, tx_size);
blockContext);
Av1TransformType tx_type = blk_ptr.TransformBlocks[txb_itr].TransformType[(int)Av1ComponentType.Luminance]; Av1TransformType tx_type = blk_ptr.TransformBlocks[txb_itr].TransformType[(int)Av1ComponentType.Luminance];
int eob = blk_ptr.TransformBlocks[txb_itr].NzCoefficientCount[0]; int eob = blk_ptr.TransformBlocks[txb_itr].NzCoefficientCount[0];
@ -1063,7 +1058,7 @@ internal partial class Av1TileWriter
Guard.IsTrue(tx_type == Av1TransformType.DctDct, nameof(tx_type), string.Empty); Guard.IsTrue(tx_type == Av1TransformType.DctDct, nameof(tx_type), string.Empty);
} }
int cul_level_y = writer.WriteCoefficients( int culLevelY = writer.WriteCoefficients(
tx_size, tx_size,
tx_type, tx_type,
intraLumaDir, intraLumaDir,
@ -1074,15 +1069,12 @@ internal partial class Av1TileWriter
frameHeader.UseReducedTransformSet, frameHeader.UseReducedTransformSet,
blk_ptr.FilterIntraMode); blk_ptr.FilterIntraMode);
// WriteCoefficients packs the DC sign and cumulative level into one integer; publish its bytes // Only the packed low byte is the AV1 entropy context. Converting the value explicitly keeps
// across the transform edges so the next blocks derive identical entropy contexts. // the update independent of machine endianness and publishes one value per covered edge unit.
Span<int> culLevelSpan = new(ref cul_level_y);
ReadOnlySpan<byte> dc_sign_level_coeff = MemoryMarshal.AsBytes(culLevelSpan);
int transformWidth = blockGeometry.TransformSize[tx_depth].GetWidth(); int transformWidth = blockGeometry.TransformSize[tx_depth].GetWidth();
int transformHeight = blockGeometry.TransformSize[tx_depth].GetHeight(); int transformHeight = blockGeometry.TransformSize[tx_depth].GetHeight();
luma_dc_sign_level_coeff_na.UnitModeWrite( luma_dc_sign_level_coeff_na.UnitModeWrite(
dc_sign_level_coeff, (byte)culLevelY,
blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin, blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin,
new Size(transformWidth, transformHeight), new Size(transformWidth, transformHeight),
Av1NeighborArrayUnit<byte>.UnitMask.Top | Av1NeighborArrayUnit<byte>.UnitMask.Left); Av1NeighborArrayUnit<byte>.UnitMask.Top | Av1NeighborArrayUnit<byte>.UnitMask.Left);
@ -1128,81 +1120,69 @@ internal partial class Av1TileWriter
int tx_depth = mbmi.Block.TransformDepth; int tx_depth = mbmi.Block.TransformDepth;
uint txb_count = 1; uint txb_count = 1;
ObuFrameHeader frameHeader = pcs.Parent.FrameHeader; ObuFrameHeader frameHeader = pcs.Parent.FrameHeader;
int transformWidth = blockGeometry.TransformSize[tx_depth].GetWidth();
int transformHeight = blockGeometry.TransformSize[tx_depth].GetHeight();
for (uint tx_index = 0; tx_index < txb_count; ++tx_index) for (uint tx_index = 0; tx_index < txb_count; ++tx_index)
{ {
Av1TransformSize chroma_tx_size = blockGeometry.TransformSizeUv[tx_depth]; Av1TransformSize chromaTransformSize = blockGeometry.TransformSizeUv[tx_depth];
int transformWidth = chromaTransformSize.GetWidth();
int transformHeight = chromaTransformSize.GetHeight();
Point transformOrigin = blockGeometry.TransformOrigin[tx_depth][tx_index];
Point chromaOrigin = RoundUv(blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin) >> 1;
// Both chroma planes share transform geometry but retain independent coefficient contexts.
Span<int> coefficientBuffer = coeff_ptr.GetPlaneBuffer(Av1Plane.U).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv);
Av1TransformBlockContext blockContext = GetTransformBlockContexts(
Av1ComponentType.Chroma,
cb_dc_sign_level_coeff_na,
chromaOrigin,
blockGeometry.BlockSizeUv,
chromaTransformSize);
Av1TransformType chromaTransformType = blk_ptr.TransformBlocks[tx_index].TransformType[(int)Av1ComponentType.Chroma];
int endOfBlockCb = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[1];
int culLevelCb = writer.WriteCoefficients(
chromaTransformSize,
chromaTransformType,
intraLumaDir,
coefficientBuffer,
Av1ComponentType.Chroma,
blockContext,
(ushort)endOfBlockCb,
frameHeader.UseReducedTransformSet,
blk_ptr.FilterIntraMode);
if (blockGeometry.HasUv) coefficientBuffer = coeff_ptr.GetPlaneBuffer(Av1Plane.V).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv);
{ int endOfBlockCr = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[2];
// Both chroma planes share transform geometry but retain independent coefficient contexts.
Span<int> coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.U).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv);
Av1TransformBlockContext blockContext = default;
Point transformOrigin = blockGeometry.TransformOrigin[tx_depth][tx_index];
GetTransformBlockContexts(
pcs,
Av1ComponentType.Chroma,
cb_dc_sign_level_coeff_na,
RoundUv(blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin) >> 1,
blockGeometry.BlockSizeUv,
chroma_tx_size,
blockContext);
Av1TransformType chroma_tx_type = blk_ptr.TransformBlocks[tx_index].TransformType[(int)Av1ComponentType.Chroma];
int endOfBlockCb = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[1];
int cul_level_cb = writer.WriteCoefficients(
chroma_tx_size,
chroma_tx_type,
intraLumaDir,
coeff_buffer,
Av1ComponentType.Chroma,
blockContext,
(ushort)endOfBlockCb,
frameHeader.UseReducedTransformSet,
blk_ptr.FilterIntraMode);
coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.V).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv);
blockContext = default;
int endOfBlockCr = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[2];
GetTransformBlockContexts(
pcs,
Av1ComponentType.Chroma,
cr_dc_sign_level_coeff_na,
RoundUv(blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin) >> 1,
blockGeometry.BlockSizeUv,
chroma_tx_size,
blockContext);
int cul_level_cr = writer.WriteCoefficients(
chroma_tx_size,
chroma_tx_type,
intraLumaDir,
coeff_buffer,
Av1ComponentType.Chroma,
blockContext,
(ushort)endOfBlockCr,
frameHeader.UseReducedTransformSet,
blk_ptr.FilterIntraMode);
// Publish each plane's packed sign/level summary across its transform edges.
Span<int> culLevelCbSpan = new(ref cul_level_cb);
ReadOnlySpan<byte> dc_sign_level_coeff = MemoryMarshal.AsBytes(culLevelCbSpan);
cb_dc_sign_level_coeff_na.UnitModeWrite(
dc_sign_level_coeff,
RoundUv(transformOrigin) >> 1,
new Size(transformWidth, transformHeight),
Av1NeighborArrayUnit<byte>.UnitMask.Top | Av1NeighborArrayUnit<byte>.UnitMask.Left);
Span<int> culLevelCrSpan = new(ref cul_level_cr); blockContext = GetTransformBlockContexts(
dc_sign_level_coeff = MemoryMarshal.AsBytes(culLevelCrSpan); Av1ComponentType.Chroma,
cr_dc_sign_level_coeff_na.UnitModeWrite( cr_dc_sign_level_coeff_na,
dc_sign_level_coeff, chromaOrigin,
RoundUv(transformOrigin) >> 1, blockGeometry.BlockSizeUv,
new Size(transformWidth, transformHeight), chromaTransformSize);
Av1NeighborArrayUnit<byte>.UnitMask.Top | Av1NeighborArrayUnit<byte>.UnitMask.Left);
} int culLevelCr = writer.WriteCoefficients(
chromaTransformSize,
chromaTransformType,
intraLumaDir,
coefficientBuffer,
Av1ComponentType.Chroma,
blockContext,
(ushort)endOfBlockCr,
frameHeader.UseReducedTransformSet,
blk_ptr.FilterIntraMode);
// Each plane publishes its packed context across the complete chroma transform edges.
cb_dc_sign_level_coeff_na.UnitModeWrite(
(byte)culLevelCb,
chromaOrigin,
new Size(transformWidth, transformHeight),
Av1NeighborArrayUnit<byte>.UnitMask.Top | Av1NeighborArrayUnit<byte>.UnitMask.Left);
cr_dc_sign_level_coeff_na.UnitModeWrite(
(byte)culLevelCr,
chromaOrigin,
new Size(transformWidth, transformHeight),
Av1NeighborArrayUnit<byte>.UnitMask.Top | Av1NeighborArrayUnit<byte>.UnitMask.Left);
entropyCodingContext.CodedAreaSuperblockUv += transformWidth * transformHeight; entropyCodingContext.CodedAreaSuperblockUv += transformWidth * transformHeight;
} }
@ -1218,84 +1198,65 @@ internal partial class Av1TileWriter
/// <summary> /// <summary>
/// Derives coefficient skip and DC-sign contexts from the transform block's above and left neighbors. /// Derives coefficient skip and DC-sign contexts from the transform block's above and left neighbors.
/// </summary> /// </summary>
/// <param name="pcs">The picture coding state.</param>
/// <param name="plane">The luma or chroma component class.</param> /// <param name="plane">The luma or chroma component class.</param>
/// <param name="dcSignLevelCoefficientNeighborArray">The packed DC-sign and coefficient-level neighbor contexts.</param> /// <param name="dcSignLevelCoefficientNeighborArray">The packed DC-sign and coefficient-level neighbor contexts.</param>
/// <param name="blockOrigin">The transform-block origin in samples of the target plane.</param> /// <param name="blockOrigin">The transform-block origin in samples of the target plane.</param>
/// <param name="planeBlockSize">The containing block size on the target plane.</param> /// <param name="planeBlockSize">The containing block size on the target plane.</param>
/// <param name="transformSize">The transform size.</param> /// <param name="transformSize">The transform size.</param>
/// <param name="blockContext">The context object to populate.</param> /// <returns>The coefficient skip and DC-sign contexts selected by both transform edges.</returns>
private static void GetTransformBlockContexts( public static Av1TransformBlockContext GetTransformBlockContexts(
Av1PictureControlSet pcs,
Av1ComponentType plane, Av1ComponentType plane,
Av1NeighborArrayUnit<byte> dcSignLevelCoefficientNeighborArray, Av1NeighborArrayUnit<byte> dcSignLevelCoefficientNeighborArray,
Point blockOrigin, Point blockOrigin,
Av1BlockSize planeBlockSize, Av1BlockSize planeBlockSize,
Av1TransformSize transformSize, Av1TransformSize transformSize)
Av1TransformBlockContext blockContext)
{ {
int dcSignLevelCoefficientLeftNeighborIndex = dcSignLevelCoefficientNeighborArray.GetLeftIndex(blockOrigin); int leftIndex = dcSignLevelCoefficientNeighborArray.GetLeftIndex(blockOrigin);
int dcSignLevelCoefficientTopNeighborIndex = dcSignLevelCoefficientNeighborArray.GetTopIndex(blockOrigin); int topIndex = dcSignLevelCoefficientNeighborArray.GetTopIndex(blockOrigin);
int transformBlockWidth = transformSize.Get4x4WideCount();
sbyte[] signs = [0, -1, 1]; int transformBlockHeight = transformSize.Get4x4HighCount();
int transformBlockWidth; ReadOnlySpan<byte> topContexts = dcSignLevelCoefficientNeighborArray.Top.Slice(topIndex, transformBlockWidth);
int transformBlockHeight; ReadOnlySpan<byte> leftContexts = dcSignLevelCoefficientNeighborArray.Left.Slice(leftIndex, transformBlockHeight);
if (plane != Av1ComponentType.Luminance) int dcSign = 0;
{ int top = 0;
transformBlockWidth = Math.Min(transformSize.GetWidth(), ((pcs.Parent.AlignedWidth / 2) - blockOrigin.X) >> 2); int left = 0;
transformBlockHeight = Math.Min(transformSize.GetHeight(), ((pcs.Parent.AlignedHeight / 2) - blockOrigin.Y) >> 2);
} // Each context packs a coefficient-level class in the low bits and the DC sign class above it.
else // Accumulating both values in one traversal supplies every luma and chroma context without scratch storage.
foreach (byte context in topContexts)
{ {
transformBlockWidth = Math.Min(transformSize.GetWidth(), (pcs.Parent.AlignedWidth - blockOrigin.X) >> 2); byte sign = (byte)(context >> Av1Constants.CoefficientContextBitCount);
transformBlockHeight = Math.Min(transformSize.GetHeight(), (pcs.Parent.AlignedHeight - blockOrigin.Y) >> 2); DebugGuard.MustBeLessThanOrEqualTo(sign, (byte)2, nameof(sign));
} if (sign == 1)
{
short dc_sign = 0; dcSign--;
ushort k = 0; }
else if (sign == 2)
byte sign;
// The high bits encode the DC sign class: zero, negative, or positive. Summing classes over
// both edges selects whether neighboring DC coefficients bias the current sign context.
if (dcSignLevelCoefficientNeighborArray.Top[dcSignLevelCoefficientTopNeighborIndex] != Av1NeighborArrayUnit<byte>.InvalidNeighborData)
{
do
{ {
sign = (byte)(dcSignLevelCoefficientNeighborArray.Top[k + dcSignLevelCoefficientTopNeighborIndex] >> dcSign++;
Av1Constants.CoefficientContextBitCount);
Guard.MustBeLessThanOrEqualTo(sign, (byte)2, nameof(sign));
dc_sign += signs[sign];
} }
while (++k < transformBlockWidth);
top |= context;
} }
if (dcSignLevelCoefficientNeighborArray.Left[dcSignLevelCoefficientLeftNeighborIndex] != Av1NeighborArrayUnit<byte>.InvalidNeighborData) foreach (byte context in leftContexts)
{ {
k = 0; byte sign = (byte)(context >> Av1Constants.CoefficientContextBitCount);
do DebugGuard.MustBeLessThanOrEqualTo(sign, (byte)2, nameof(sign));
if (sign == 1)
{ {
sign = (byte)(dcSignLevelCoefficientNeighborArray.Left[k + dcSignLevelCoefficientLeftNeighborIndex] >> dcSign--;
Av1Constants.CoefficientContextBitCount); }
Guard.MustBeLessThanOrEqualTo(sign, (byte)2, nameof(sign)); else if (sign == 2)
dc_sign += signs[sign]; {
dcSign++;
} }
while (++k < transformBlockHeight);
}
if (dc_sign > 0) left |= context;
{
blockContext.DcSignContext = 2;
}
else if (dc_sign < 0)
{
blockContext.DcSignContext = 1;
}
else
{
blockContext.DcSignContext = 0;
} }
Av1TransformBlockContext blockContext = default;
blockContext.DcSignContext = dcSign > 0 ? 2 : dcSign < 0 ? 1 : 0;
if (plane == Av1ComponentType.Luminance) if (plane == Av1ComponentType.Luminance)
{ {
if (planeBlockSize == transformSize.ToBlockSize()) if (planeBlockSize == transformSize.ToBlockSize())
@ -1304,33 +1265,7 @@ internal partial class Av1TileWriter
} }
else else
{ {
int top = 0;
int left = 0;
k = 0;
if (dcSignLevelCoefficientNeighborArray.Top[dcSignLevelCoefficientTopNeighborIndex] !=
Av1NeighborArrayUnit<byte>.InvalidNeighborData)
{
do
{
top |= dcSignLevelCoefficientNeighborArray.Top[k + dcSignLevelCoefficientTopNeighborIndex];
}
while (++k < transformBlockWidth);
}
top &= Av1Constants.CoefficientContextMask; top &= Av1Constants.CoefficientContextMask;
if (dcSignLevelCoefficientNeighborArray.Left[dcSignLevelCoefficientLeftNeighborIndex] !=
Av1NeighborArrayUnit<byte>.InvalidNeighborData)
{
k = 0;
do
{
left |= dcSignLevelCoefficientNeighborArray.Left[k + dcSignLevelCoefficientLeftNeighborIndex];
}
while (++k < transformBlockHeight);
}
left &= Av1Constants.CoefficientContextMask; left &= Av1Constants.CoefficientContextMask;
blockContext.SkipContext = Av1SymbolContextHelper.GetTransformBlockSkipContext(top, left); blockContext.SkipContext = Av1SymbolContextHelper.GetTransformBlockSkipContext(top, left);
} }
@ -1339,36 +1274,12 @@ internal partial class Av1TileWriter
{ {
// Chroma contexts use only the presence of nonzero levels on each edge, plus an offset // Chroma contexts use only the presence of nonzero levels on each edge, plus an offset
// that distinguishes a transform smaller than its containing plane block. // that distinguishes a transform smaller than its containing plane block.
short ctx_base_left = 0; int contextBase = (left != 0 ? 1 : 0) + (top != 0 ? 1 : 0);
short ctx_base_top = 0; int contextOffset = planeBlockSize.GetPelsLog2Count() > transformSize.ToBlockSize().GetPelsLog2Count() ? 10 : 7;
blockContext.SkipContext = contextBase + contextOffset;
if (dcSignLevelCoefficientNeighborArray.Top[dcSignLevelCoefficientTopNeighborIndex] !=
Av1NeighborArrayUnit<byte>.InvalidNeighborData)
{
k = 0;
do
{
ctx_base_top +=
(dcSignLevelCoefficientNeighborArray.Top[k + dcSignLevelCoefficientTopNeighborIndex] != 0) ? (short)1 : (short)0;
}
while (++k < transformBlockWidth);
}
if (dcSignLevelCoefficientNeighborArray.Left[dcSignLevelCoefficientLeftNeighborIndex] !=
Av1NeighborArrayUnit<byte>.InvalidNeighborData)
{
k = 0;
do
{
ctx_base_left += dcSignLevelCoefficientNeighborArray.Left[k + dcSignLevelCoefficientLeftNeighborIndex] != 0 ? (short)1 : (short)0;
}
while (++k < transformBlockHeight);
}
int ctx_base = ((ctx_base_left != 0) ? 1 : 0) + ((ctx_base_top != 0) ? 1 : 0);
int ctx_offset = planeBlockSize.GetPelsLog2Count() > transformSize.ToBlockSize().GetPelsLog2Count() ? 10 : 7;
blockContext.SkipContext = (short)(ctx_base + ctx_offset);
} }
return blockContext;
} }
/// <summary> /// <summary>

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

@ -16,6 +16,62 @@ public class Av1CoefficientsEntropyTests
{ {
private const int BaseQIndex = 23; private const int BaseQIndex = 23;
[Fact]
public void NeighborArrayWritesEveryCoveredFourByFourEdgeUnit()
{
using Av1NeighborArrayUnit<byte> neighbors = new(
Configuration.Default,
leftSize: 8,
topSize: 8,
topLeftSize: 16)
{
GranularityNormalLog2 = 2,
GranularityTopLeftLog2 = 2
};
neighbors.UnitModeWrite(
37,
new Point(8, 4),
new Size(16, 8),
Av1NeighborArrayUnit<byte>.UnitMask.Top | Av1NeighborArrayUnit<byte>.UnitMask.Left);
Assert.Equal(new byte[] { 0, 0, 37, 37, 37, 37, 0, 0 }, neighbors.Top.ToArray());
Assert.Equal(new byte[] { 0, 37, 37, 0, 0, 0, 0, 0 }, neighbors.Left.ToArray());
}
[Theory]
[InlineData((int)Av1ComponentType.Luminance, 5)]
[InlineData((int)Av1ComponentType.Chroma, 12)]
public void WriterDerivesTransformContextFromCompleteFourByFourEdges(
int componentType,
int expectedSkipContext)
{
using Av1NeighborArrayUnit<byte> neighbors = new(
Configuration.Default,
leftSize: 8,
topSize: 8,
topLeftSize: 16)
{
GranularityNormalLog2 = 2,
GranularityTopLeftLog2 = 2
};
// The high bits carry positive, positive, and negative DC signs. The low bits select
// the high-above and low-left coefficient classes used by the luma skip-context table.
neighbors.Top[2] = (2 << Av1Constants.CoefficientContextBitCount) | 4;
neighbors.Top[3] = 2 << Av1Constants.CoefficientContextBitCount;
neighbors.Left[1] = (1 << Av1Constants.CoefficientContextBitCount) | 1;
Av1TransformBlockContext context = Av1TileWriter.GetTransformBlockContexts(
(Av1ComponentType)componentType,
neighbors,
new Point(8, 4),
Av1BlockSize.Block16x16,
Av1TransformSize.Size8x8);
Assert.Equal(2, context.DcSignContext);
Assert.Equal(expectedSkipContext, context.SkipContext);
}
[Fact] [Fact]
public void RoundTripZeroEndOfBlock() public void RoundTripZeroEndOfBlock()
{ {

Loading…
Cancel
Save